4、Redis TTL实战:过期策略、EXPIRE命令、TTL命令与内存淘汰机制

聊到 Redis 的 TTL,很多人第一反应就是「设置个过期时间嘛,有啥难的」。嗯,我当年也是这么想的。直到线上一个缓存 key 明明设了 5 分钟过期,结果 3 秒就没了——我才意识到,TTL 背后那套机制,远比想象中复杂。

这一章,咱们就把 Redis 的过期策略、核心命令、以及内存淘汰机制彻底捋一遍。你想想看,一个 key 从创建到被删除,中间经历了什么?谁来决定它什么时候死?

核心要点:Redis 的 TTL 不是「定时炸弹」,而是「被动+主动」双引擎驱动的过期策略。理解这个,你才能精准控制缓存的生命周期。

4.1 过期策略:被动删除 vs 主动删除

Redis 的过期删除,说白了就两种方式:惰性删除和定期删除。两者配合,缺一不可。

4.1.1 惰性删除(Lazy Deletion)

什么叫惰性?就是「你不找我,我就不删」。每次客户端访问一个 key 时,Redis 会先检查它是否设置了过期时间,如果已经过期,直接返回 nil,顺手把这个 key 删掉。

这种策略的好处很明显——CPU 开销极低。只有被访问到的过期 key 才会被清理,不会浪费资源去扫描那些「无人问津」的 key。

但问题也来了:如果一个 key 过期了,却再也没有人访问它呢?它会一直占着内存,直到地老天荒。这就是所谓的「内存泄漏」风险。

我曾经踩过的坑:线上有个 session 缓存池,大量 key 设置了 30 分钟过期,但用户量下降后,这些 key 几乎不再被访问。结果内存只增不减,最后 OOM 了。惰性删除在这种场景下,完全失效。

4.1.2 定期删除(Active Expiration)

为了解决惰性删除的「懒病」,Redis 引入了定期删除机制。它每隔 100ms 左右,随机抽取一批设置了过期时间的 key,检查并删除其中已过期的。

具体流程是这样的:

  1. 从 expires 字典中随机取 20 个 key
  2. 删除其中已过期的 key
  3. 如果过期 key 比例超过 25%,重复步骤 1
  4. 否则,等待下一轮循环

这个算法很聪明——它不会全表扫描,而是用「概率抽样」的方式,在 CPU 和内存之间找平衡。我个人觉得,这是 Redis 设计里最优雅的部分之一。

小提示:定期删除的周期是固定的,但每次执行的时间上限是 25ms。如果过期 key 太多,可能会阻塞主线程。我建议你在写入大量过期 key 时,适当分散过期时间,避免「雪崩式删除」。

4.2 EXPIRE 命令:设置过期时间的正确姿势

EXPIRE 是 Redis 最基础的 TTL 命令。它的语法很简单:

EXPIRE key seconds

设置成功后返回 1,key 不存在则返回 0。但这里有几个细节,我建议你记牢:

  • 单位是秒,不是毫秒。如果你需要毫秒级精度,用 PEXPIRE
  • 过期时间从命令执行时开始计算,不是从 key 创建时
  • 重新 SET 会清除过期时间——这是最常见的坑

避坑指南:我曾经在项目中用 SET key value 更新了一个带 TTL 的缓存,结果它变成了永久 key。后来我改用 SETEX 或者 SET key value EX seconds,才彻底解决这个问题。

除了 EXPIRE,还有几个变种命令:

命令 说明 精度
EXPIRE 设置秒级过期 秒
PEXPIRE 设置毫秒级过期 毫秒
EXPIREAT 指定 Unix 时间戳过期(秒) 秒
PEXPIREAT 指定 Unix 时间戳过期(毫秒) 毫秒

我个人习惯用 EXPIREAT 来做「固定时间点过期」,比如每天凌晨 0 点刷新缓存。这样更直观,也更容易控制。

4.3 TTL 命令:如何查看 key 的剩余寿命

TTL 命令用来查询一个 key 还有多久过期:

TTL key

返回值有三种情况:

  • 大于 0:剩余秒数
  • -1:key 存在,但没有设置过期时间(永久 key)
  • -2:key 不存在(可能已过期被删除,或从未存在)

注意:TTL 返回 -2 并不代表 key「刚刚被删除」,它可能早就被惰性删除或定期删除清理掉了。所以,不要依赖 TTL 来做精确的「是否存在」判断。

还有一个 PTTL 命令,返回毫秒级精度。调试时我更喜欢用 PTTL,尤其是排查那些「秒级过期」的 key 时,能看得更细。

4.4 内存淘汰机制:当内存不够时,谁先死?

过期策略管的是「已过期」的 key。但内存满了怎么办?这时候就需要内存淘汰机制出场了。

Redis 提供了 8 种淘汰策略,我整理了一张表:

策略 含义 适用场景
noeviction 不淘汰,写操作直接报错 数据不能丢的场景
allkeys-lru 从所有 key 中淘汰最近最少使用的 通用缓存,最常用
volatile-lru 从设置了 TTL 的 key 中淘汰 LRU 混合存储(永久 key + 缓存)
allkeys-random 随机淘汰任意 key 数据访问均匀,无热点
volatile-random 从设置了 TTL 的 key 中随机淘汰 缓存层,允许部分丢失
volatile-ttl 淘汰 TTL 最小的 key(即将过期) 优先保留「新鲜」数据
allkeys-lfu 淘汰访问频率最低的 key 热点数据明显,冷热分明
volatile-lfu 从设置了 TTL 的 key 中淘汰 LFU 缓存层,关注访问频率

我个人最常用的是 allkeys-lru。为什么呢?因为它简单粗暴——不管你有没有 TTL,只要内存不够,就淘汰最不常用的。配合合理的 TTL 设置,基本能覆盖 90% 的缓存场景。

经验之谈:如果你用了 volatile-* 系列策略,一定要确保大部分 key 都设置了 TTL。否则内存满了,但可淘汰的 key 很少,Redis 会陷入「无 key 可删」的尴尬,最终退化为 noeviction 行为。

4.5 知识体系总览

下面这张图,把本章的核心逻辑串起来了。你可以看到,一个 key 从创建到消亡,经历了哪些关卡:

Redis TTL 与过期策略知识体系 Redis TTL 与过期策略知识体系 Redis 过期机制 过期策略 命令操作 内存淘汰 惰性删除 定期删除 EXPIRE TTL PEXPIRE allkeys-lru volatile-ttl 核心逻辑:惰性删除兜底 + 定期删除主动清理 + 内存淘汰保底 三者配合,确保 Redis 不会因为过期 key 而耗尽内存

4.6 实战建议:如何配置你的 TTL 策略

说了这么多理论,最后给几条实战建议:

  • 不要所有 key 都用同一个 TTL。热点数据设长一点,冷门数据设短一点。我习惯按业务维度分层:用户会话 30 分钟,商品详情 5 分钟,配置信息 1 小时。
  • 用 SET key value EX seconds 替代 EXPIRE。原子操作,避免 SET 和 EXPIRE 之间出现竞态条件。
  • 监控 expired_keys 指标。如果这个值突然飙升,说明你的 TTL 设置可能太短,或者有大量 key 在同一时间过期。
  • 内存淘汰策略选 allkeys-lru。除非你有特殊需求,否则这是最省心的选择。

最后说一句:TTL 不是银弹。它解决的是「数据生命周期管理」的问题,但解决不了「数据一致性」和「缓存穿透」。下一层,我们得聊聊更复杂的东西——但那是后话了。