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,检查并删除其中已过期的。
具体流程是这样的:
- 从
expires字典中随机取 20 个 key - 删除其中已过期的 key
- 如果过期 key 比例超过 25%,重复步骤 1
- 否则,等待下一轮循环
这个算法很聪明——它不会全表扫描,而是用「概率抽样」的方式,在 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 从创建到消亡,经历了哪些关卡:
4.6 实战建议:如何配置你的 TTL 策略
说了这么多理论,最后给几条实战建议:
- 不要所有 key 都用同一个 TTL。热点数据设长一点,冷门数据设短一点。我习惯按业务维度分层:用户会话 30 分钟,商品详情 5 分钟,配置信息 1 小时。
- 用
SET key value EX seconds替代 EXPIRE。原子操作,避免 SET 和 EXPIRE 之间出现竞态条件。 - 监控
expired_keys指标。如果这个值突然飙升,说明你的 TTL 设置可能太短,或者有大量 key 在同一时间过期。 - 内存淘汰策略选
allkeys-lru。除非你有特殊需求,否则这是最省心的选择。
最后说一句:TTL 不是银弹。它解决的是「数据生命周期管理」的问题,但解决不了「数据一致性」和「缓存穿透」。下一层,我们得聊聊更复杂的东西——但那是后话了。