# Redis 的过期键删除策略(惰性 + 定期)

# 标准答案

Redis 采用 惰性删除(Lazy Deletion)定期删除(Periodic Deletion) 结合的方式管理过期键:

  1. 惰性删除:仅在访问键时检查是否过期,过期则删除,避免额外 CPU 资源消耗,但可能导致大量过期键占用内存。
  2. 定期删除:Redis 每秒运行 10 次(默认),随机抽取部分过期键检查并删除,降低内存占用,但可能存在漏删情况。
    如果过期键未被及时删除,且内存达到 maxmemory 限制,则触发 内存淘汰策略(如 LRU、LFU)。

# 答案解析

# 🔹 1. 过期键存储方式

Redis 不会主动删除已过期键,而是依赖两种策略:

  • 每个 key 可能有过期时间(EXPIRE),过期时间存储在 dict->expires 哈希表。
  • key 过期后仍存于主数据结构中(dict->main),但 expires 记录的过期时间会影响后续访问与删除策略。

示例:

SET key "value"
EXPIRE key 10  # 设置10秒后过期
1
2

Redis 并不会立即删除 key,而是等到:

  1. 用户访问 key 时,触发惰性删除。
  2. 定期任务抽样检测,发现过期则删除。
  3. 内存达到上限时,触发淘汰策略。

# 🔹 2. 过期键删除策略

# 2.1 惰性删除(Lazy Deletion)

  • 原理
    只有当客户端访问某个键时,Redis 才会检查其是否过期,如果已过期,则删除该键并返回 nil
  • 优点
    • 不消耗额外 CPU 资源,仅在访问时检查,避免对 CPU 造成额外负担。
  • 缺点
    • 可能导致大量过期数据堆积,如果某些键从未被访问,就不会被删除,占用大量内存。

示例:

SET key "hello"
EXPIRE key 10  # 10秒后过期
# 等待 15 秒后执行:
GET key  # 访问时发现过期,立即删除并返回 nil
1
2
3
4

源码分析(惰性删除实现)

robj *lookupKeyRead(redisDb *db, robj *key) {
    expireIfNeeded(db, key); // 先检查 key 是否过期,若过期则删除
    return lookupKey(db, key); // 再执行正常查询
}
1
2
3
4

结论

  • 只有 访问该 key 时才会触发删除,不会主动扫描内存删除无访问的过期键。

# 2.2 定期删除(Periodic Deletion)

  • 原理

    • Redis 每秒执行 10 次(默认 hz = 10),从每个数据库(db)中随机抽取部分键,检查是否过期并删除。
    • 每次最多检查 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP = 20 个键(默认值)。
    • 如果删除率低(如 <25%),Redis 增加检查频率,确保过期键被删除。
  • 优点

    • 定期清理内存,减少过期数据堆积
    • 降低单次删除的 CPU 影响,避免一次性删除过多键影响 Redis 响应速度。
  • 缺点

    • 抽样方式可能漏删部分键,如果某些过期键未被抽中,就可能继续占用内存。

源码分析(定期删除实现)

void databasesCron(void) {
    for (int i = 0; i < server.dbnum; i++) {
        expireCycleTryExpire(server.db+i, ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP);
    }
}
1
2
3
4
5
  • databasesCron() 是 Redis 定期任务(10ms 执行一次)。
  • expireCycleTryExpire() 在每个数据库中 随机选 20 个键 检查过期。
  • 如果 删除率 <25%,则增加检查频率,确保删除效率。

结论

  • 定期删除策略 可以 主动减少过期数据,但因随机抽样,无法保证所有过期键都能及时删除。

# 🔹 3. 过期键未及时删除的后果

如果某些过期键 既不被访问(惰性删除无效),又未被随机抽样到(定期删除无效),它们仍然会占用 Redis 内存。

# Redis 解决方案:内存淘汰策略

当 Redis 内存达到 maxmemory 限制,Redis 触发 内存淘汰机制

  • 淘汰策略(默认 noeviction
    • volatile-lru最近最少使用(LRU),在设置了过期时间的键中,淘汰最久未使用的键。
    • allkeys-lru:在所有键中,淘汰最久未使用的键。
    • volatile-ttl:优先淘汰快要过期的键
    • noeviction:如果内存满了,直接返回错误(不会删除键)。

示例:

CONFIG SET maxmemory 100mb
CONFIG SET maxmemory-policy allkeys-lru
1
2

这样,当 Redis 内存达到 100MB 时,会自动删除最久未使用的数据

# 🔹 4. 最佳实践与优化方案

  1. 合理配置 hz 参数(定期删除频率)

    • hz 控制定期删除执行频率,默认 hz=10(每秒 10 次)。
    • 大数据量场景下,可以适当提高(如 hz=50),提升定期删除效率:
      CONFIG SET hz 50
      
      1
  2. 避免过多 "僵尸" 过期键

    • 尽量访问可能过期的 key,触发惰性删除。
    • 例如 定期访问所有缓存键,主动触发删除:
      SCAN 0 MATCH * COUNT 1000
      
      1
  3. 适当调整 maxmemory-policy

    • 如果 Redis 用于缓存,建议采用 LRU 淘汰策略
      CONFIG SET maxmemory-policy allkeys-lru
      
      1
    • 这样,当内存不足时,Redis 优先淘汰最久未使用的键,保证热点数据可用。

# 🔍 深入追问

  1. 为什么 Redis 采用 "惰性删除 + 定期删除",而不是 "定时删除"?
  2. 定期删除的频率是否可调?如何调整?
  3. Redis 如何保证定期删除不会影响主线程性能?
  4. 如何优化 Redis 在大规模数据场景下的过期键删除?
  5. 过期键的删除会触发 AOF 持久化吗?如何优化?

# 相关面试题

  • Redis 如何处理过期键?
  • 过期键与内存淘汰策略的区别?
  • Redis 为什么不用定时器主动删除过期键?
  • 如何优化 Redis 的过期键删除?
  • Redis 的 LRU 淘汰策略是如何实现的?

总结:

  • 惰性删除:低 CPU 消耗,但可能造成内存占用过高。
  • 定期删除:控制过期键增长,但抽样可能漏删。
  • 内存淘汰:作为最终保障,确保 Redis 不会 OOM。