Redis过期时间详解:从TTL设置到缓存淘汰与实战避坑指南 1. 先搞清楚为什么要给 Redis 数据加过期时间我刚开始用 Redis 的时候其实没太把过期时间当回事觉得无非就是存个值、读个值顶多再清一清。直到有一次线上服务半夜告警内存快被打满我上去一查堆了上千万个没有任何业务价值的临时 key那一刻才意识到expire 不是锦上添花是保命用的。从业务视角看Redis 里很多数据本质上都是临时存在登录验证码、短信验证码有效期 5 分钟过期就扔会话 token、临时授权票据最多存几小时一个活动页的配置拉取后想在 1 小时内复用过了就不许用旧的秒杀接口的防重标记30 秒内不许同一个用户重复点分布式锁锁必须有个持有上限否则客户端崩了锁就永远解不开。这些场景都有一个共同点数据只在某个时间段内有效。如果只靠程序里手动去删删漏了、忘记删了Redis 里的垃圾数据只会越积越多内存迟早被拖垮。所以让数据自己消亡才是正确的路子——这就是过期时间存在的意义。从运维角度看给 key 设置过期时间还有一个隐性作用它是数据生命周期管理的一部分。一个完整的 Redis 使用规范通常要求所有非永久业务 key 都必须设置 TTL并且要有合理的上限。因为 Redis 本身是纯内存数据库内存不像磁盘可以随便堆它是有物理成本上限的。如果一个 key 永远不删除那它占用的内存就永远不会还给你等于给你整个集群背了一个隐性包袱。另一个容易被忽略的原因数据新鲜度。缓存最怕的不是缓存没有而是缓存里有脏数据却没人知道它是脏的。如果你缓存了一份商品库存不设置过期时间那数据库里库存已经扣了 20 件Redis 还告诉你还剩 50 件这就是事故。过期时间本质上是给数据定了保质期过了保质期强制作废强制重新去源头拉新数据这是保障一致性的兜底手段。所以写这本书的第一节先给大家定个调不是所有数据都该永驻内存凡是只在一段时间内有效的数据都要设置过期时间。这个习惯养成之后你的 Redis 内存会稳定很多出问题的概率也会小很多。2. 两种核心路径建 key 时设置 vs 建完之后再设置2.1 创建 key 时直接带上过期时间如果你在业务代码里写的是这种套路SET login_code 8848 EXPIRE login_code 300第一步存值第二步单独设置过期时间逻辑上当然没错但是中间间隔的那几毫秒里如果服务挂了这个 key 就永远没有过期时间了变成永久 key。更稳妥的做法是把两步合成一步用SET命令的扩展参数SET login_code 8848 EX 300这条命令的语义是写入login_code值等于8848同时在当前时间基础上加上 300 秒作为过期时间写入和设置过期是原子的不存在中间态。SET的过期参数家族其实有四个EX seconds以秒为单位设置过期时间PX milliseconds以毫秒为单位设置过期时间EXAT timestamp设置一个绝对的 Unix 秒级时间戳到期PXAT timestamp设置一个绝对的 Unix 毫秒级时间戳到期。日常用的最多的是EX和PXEXAT和PXAT适合那种固定到某个具体时间点失效的需求比如这个优惠券今晚 0 点作废。还有一个小细节SET命令在 Redis 2.6.12 版本之前其实不支持这些参数不过现在主流的 Redis 版本早就超过这个底线了所以可以放心用。在SET里还有幂等的场景是用NX参数即只有当这个 key 不存在时才写入这套组合我们后面讲分布式锁会再提到。2.2 给已有 key 单独设置过期时间如果你拿到的数据是从别的地方写入的或者你需要动态调整一个已有 key 的生存时长那就得用专门处理过期时间的命令族。这一族命令包括EXPIRE key seconds按秒设置PEXPIRE key milliseconds按毫秒设置EXPIREAT key timestamp设一个绝对时间戳秒PEXPIREAT key timestamp设一个绝对时间戳毫秒。这四个命令的核心逻辑完全一致只是时间单位不同。用法上它们可以覆盖已有 key 的过期时间也可以给一个原本永不过期的 key 加上一个 TTL。它们的返回值也很有意思返回1说明设置成功key 的过期时间已经生效返回0说明这个 key 根本不存在或者你给的时间参数不合法比如传了负数设置失败。有个很实用的记忆技巧如果你想快速设置一个 10 秒后过期写成EXPIRE somekey 10如果你想设置成到某个具体时间点过期就先用date命令算出对应的时间戳再用EXPIREAT。Redis 里时间戳统一是 Unix 时间戳这点和很多系统 API 一致。在很多语言客户端里比如 Java 的Jedis或者Lettuce对应的 API 也封装了这些命令大家在业务层可以直接调用不需要去裸敲命令行。2.3 一个很多人忽视的坑非字符串类型过期的是整个 key这句话要划重点Redis 的过期时间作用在 key 上不是作用在 key 里面的元素上。举个例子你有一个hash里面存了用户购物车信息每个字段是一个商品又比如你有一个list里面存了一串消息记录。你对这个 key 执行了EXPIRE cart 300那么 300 秒后整个 key 连同里面所有的字段和值全部被 Redis 删除不存在说哪个字段先过期哪个字段后过期。这听起来很简单但实际开发里很多人会踩坑想把 hash 里某个旧字段自动清理掉于是对这个字段对应的 key 再加一个独立的 key 去控制时间结果 hash 本身越积越大。Redis 的过期机制是针对整个 key 维度的不是像数据库的行级过期。如果你需要让某个聚合结构里的元素单独过期那么通常有两条路把每个元素拆成独立的 key各自设置 TTL用程序做定时扫描清理。前者思路更符合 Redis 的哲学——一个 key 就是一个独立的小对象。比如你要存文章 A 的评论 ID 列表同时又想让每条评论 10 分钟后过期那就别用一个大 list 去存而是把每条评论单独做 key。这个取舍直接影响你后面内存的利用率和清理的复杂度。2.4 TTL 命令的返回值三个状态的含义设置好过期时间之后你总得知道它还剩多久吧TTL命令就是干这个的。它的用法简单TTL somekey返回值根据情况分三种-2这个 key 不存在或者它已经过期被删掉了-1这个 key 存在但没有设置过期时间也就是永久 key大于等于 0剩余存活秒数。对应还有一个PTTL返回的是剩余毫秒数。这两个命令是排查问题的利器。比如你怀疑某个 key 是不是忘了设置过期时间敲一下TTL返回-1就说明中招了如果你发现某个 key 的 TTL 一直不变那要警惕是否有人反复给它续期。我个人的习惯是每次部署完一个缓存相关的功能都会在测试环境用TTL抽查几个 key验证一下过期时间是否真的生效。别看这只是个不起眼的小检查它曾经帮我发现过错把 600 秒写成了 6000 秒这种低级但致命的配置错误。3. 深入原理Redis 到底是怎么把过期 key 清掉的3.1 惰性删除只要没人访问就先留着很多人以为 Redis 有个后台线程像闹钟一样一到点就把过期的 key 全部清除。这是个常见的误区。实际上 Redis 采用的是惰性删除 定期删除的组合策略而且默认配置下还有一个内存淘汰作为兜底。惰性删除的机制通俗讲就是你用到的这一刻才给你判断死活。当你请求一个 key 时Redis 会先去检查这个 key 是否设置了过期时间如果设置过并且判断当前时间已经超过了过期时间戳那这个 key 就当场被判定为已过期立刻删除然后对你返回查无此 key。这种策略的好处是对 CPU 的消耗很小因为你只检查你访问的那些 key不会去扫描全库缺点是如果大量过期的 key 一直没人访问它们就占着内存不走等内存不够用的时候再统一爆发清理。3.2 定期删除一种妥协的平衡方案为了不让惰性删除造成过期 key 堆积成山Redis 还搞了一个定期删除机制每 100 毫秒左右随机抽取一部分设置了过期时间的 key检查它们是否过期过期就删掉。注意随机抽取这四个字它不是全量扫描否则 Redis 的 CPU 早就扛不住了。抽多少、怎么抽这个比例是 Redis 内部根据 server 状态动态算的不用你去调。定期删除的目的就是在 CPU 消耗和内存清理之间找一个平衡点既不给 CPU 太大压力又能定期回收一部分内存。3.3 内存淘汰策略最后的兜底方案再往下走如果惰性删除和定期删除都没能顶住内存压力内存满了怎么办答案是内存淘汰策略也就是maxmemory-policy。这个配置决定了 Redis 在内存达到上限时按什么规则强制挤掉一些 key 来腾空间。常见的几个策略volatile-lru从已设置过期时间的 key 里淘汰最近最少使用的allkeys-lru从所有 key 里淘汰最近最少使用的volatile-random从已设置过期时间的 key 里随机淘汰allkeys-random随便淘汰volatile-ttl从已设置过期时间的 key 里挑剩余存活时间最短的优先淘汰noeviction内存满了直接报错不淘汰任何 key默认策略但在生产环境通常不建议。这里给一个重要的实战建议如果你给大部分 key 都设置了过期时间volatile-lru是比较推荐的策略因为它优先保证有 TTL 的数据能被公平处理而没有设置过期时间的重要数据不容易被误清。如果你是作为纯缓存使用、允许任何 key 被清掉allkeys-lru也很常见因为它淘汰选择面更大命中率往往更高。3.4 大 key 过期的时候真的会卡住吗这是很多人关心的问题。一个 key 如果存了几百 MB 的数据当确定它已过期需要删除时删除动作本身是要消耗资源的。如果这个删除是同步的那 Redis 主线程就可能会阻塞一小段时间期间所有请求都排队表现就是卡了一下。好在 Redis 4.0 以后引入了异步删除的机制通过unlink命令以及对应的lazyfree配置可以把大 key 的释放放到后台线程里去做。特别是对于过期删除的行为有个配置叫lazyfree-lazy-expire默认是no也就是说默认情况下过期 key 的删除仍然是同步的。如果你的业务里存在大 hash、大 list、大 set 这类超大 key并且会被设置过期时间建议把lazyfree-lazy-expire设成yes让过期删除变成异步能显著降低主线程阻塞风险。不要问我怎么知道的线上大zset一过期整个服务抖动几秒钟那种感觉谁经历谁知道。3.5 关于懒删除的一些补充在 Redis 的演进中很多细节在逐步改进。比如 Redis 7.0 之后针对 expire 相关的异步释放机制又做了进一步优化。但不管怎样核心原理不变主线程尽量只做轻量级的判断重活交给后台线程。如果你在做集群规划和容量设计记得把大型缓存对象的过期策略一并考虑进去不要只盯着命令本身。4. 业务实战过期时间在几个典型场景里的落地4.1 验证码 / 短信验证码验证码这个场景核心需求就是两个不能重放同一验证码只能成功消费一次、必须在一定时间内有效。代码上最常见的做法是SET sms_code:13800138000 246810 EX 300存 300 秒用户输入之后立刻GET校验通过马上DEL。这里有个经验教训千万不要只设置过期时间而不设置一次性逻辑否则验证码可能会被多次使用。就算过期时间给了 5 分钟攻击者如果 1 分钟之内拿到验证码并反复重复使用也是有风险的。所以正确姿势是过期时间是兜底业务校验靠主动删除。如果用户频繁请求发送验证码还要额外加一个防刷标志位。比如用一个sms_send_limit:13800138000key设置为 60 秒过期存在就拦截请求。这种防刷 key 往往也需要精确的过期时间控制。4.2 分布式锁Redis 实现分布式锁的经典套路是SET lock:order_123 uuid-xxxx NX EX 30NX表示只有这个 key 不存在时才写入也就是抢锁EX 30表示锁在 30 秒后自动释放防止持锁客户端崩溃后死锁。拿到锁的客户端在业务处理完后应该用DEL主动释放。这里有个经典陷阱锁的过期时间设得比业务执行时间短导致业务还没做完锁自己先释放了另一个客户端又拿到了锁两个客户端同时操作共享资源这就出大问题了。所以使用分布式锁时除了设置合理的过期时间还应该引入续期机制——比如看门狗线程在原锁快过期时自动续期保证业务执行期间锁一直有效。而且在删除锁时必须校验这个锁是不是自己当初抢到的那个一般用 Lua 脚本比较 value相同才删避免误删别人的锁。4.3 限流场景限流的本质是单位时间内最多允许多少次请求。Redis 里可以这样设计用户每一次操作对某个 key 做INCR如果是第一次操作同时给它设置一个窗口期的过期时间比如MULTI INCR rate_limit:user_123 EXPIRE rate_limit:user_123 60 EXEC这个套路有一个经典 bug如果第 1 次 INCR 是 1执行了 EXPIRE然后第 10 次请求时又调了一次 INCR虽然 key 已经存在且过期时间没有重置但如果你每次请求都执行EXPIRE就会把过期时间不断往后推导致窗口期被无限延长。所以正确做法是只有第一次 INCR 时才设置过期时间后续请求只做INCR。常见的标准写法是用事务或者 Lua 脚本保证如果计数器不存在则初始化为 0 并设置过期时间。限流和过期时间的关系非常微妙稍不留神就会把固定窗口做成滑动窗口的无效版本。4.4 缓存穿透、击穿、雪崩里的过期时间调校这三个概念是缓存系统中的高频名词每一个都和过期时间有直接关系。缓存穿透查了一个根本不存在的数据每次请求都打到数据库。解决思路之一是把空结果也缓存下来并且给它设一个很短的过期时间比如 30~60 秒。这样同一个不存在的数据短时间内不会反复穿透到数据库。过期时间要短因为空结果的时效性本身不长业务上可能很快就产生了新数据。缓存击穿某个热点 key 过期的一瞬间有大量请求同时涌向数据库重新加载。解决办法是互斥锁或者让这个 key 的过期时间设置得尽量不集中、不统一例如给过期时间加一个随机偏移量。比如原本 300 秒偏成 300 random(0, 30) 秒。缓存雪崩大量 key 在同一时间段集体过期导致请求全部打到数据库。解决办法和击穿类似让过期时间在基础值上做随机抖动把集体过期的峰值打散DB 的压力就能明显缓和。具体做法很简单SET cache:config value EX (300 random(0, 60))或者统一用EXPIRE再叠加一个随机数。不要小看这个抖动在几十万 key 的规模下它能让数据库的峰值 QPS 直接降一个量级。4.5 一个实战小技巧用 GETEX 同时取值和续期Redis 6.2 版本之后GETEX命令能在取值的同时重新设置一个过期时间。这特别适合滑窗续期的场景比如用户访问了购物车你希望购物车里的数据再延长 30 分钟有效期那么直接GETEX cart:user_123 EX 1800一条命令就把值取出来、把过期时间续上了不用先GET再EXPIRE两条命令。少一次网络往返效率和安全都更高。在实际工程里还有很多类似的复合命令比如SET带过期参数、GETEX带过期参数、INCR加过期等等。掌握这些复合命令能让代码更优雅也能避免先做 A 再做 B的中间态。5. 我踩过的坑过期时间的常见问题与排查手册5.1 坑一SET 覆盖写入导致过期时间被清掉这是新手最容易踩的坑。你原来设置了一个 key给它设了 300 秒过期过了一会你又用普通的SET key value去覆盖它的值。这里有个关键细节** 如果SET没有带任何过期参数覆盖之后这个 key 就变成永久的了**原来的过期时间直接丢失。这个问题的隐蔽性在于你不查 TTL 根本发现不了。等到某个半夜内存告警你一个个排查才会发现这些本该过期的 key 全变成了永久居民。避免的方法很简单所有覆盖 key 值的操作要么使用带过期参数的SET要么在覆盖之后再单独执行一次EXPIRE。另外在审查代码时全局搜索所有SET命令凡是用于业务缓存的都要确认其过期参数。5.2 坑二RENAME 命令会让过期时间被覆盖RENAME命令也会影响过期时间。如果目标 key 原本不存在那重命名后的 key 会保留源 key 的 TTL但如果目标 key 已经存在重命名后目标 key 的过期时间会被源 key 的覆盖而原本目标 key 的过期时间直接丢失。这种情况一般出现在枚举、刷数据、临时切换数据源等操作中。如果你们代码里有RENAME记得检查它会不会破坏其他数据的过期策略。最好的习惯还是重命名之后的 key重新EXPIRE一下确保设定符合预期。5.3 坑三大 key 过期导致主线程阻塞前面提过如果一个大 key 被判定过期同步删除会阻塞 Redis 主线程。我在一次线上事故中就遇到过一个购物车 hash里面塞了几十万个字段过期时间到了Redis 在做删除时瞬间阻塞了 1 秒多整个集群请求全部超时。排查手法是看redis.log里的延迟告警以及SLOWLOG里是否有expire相关的慢命令。解决方向就是开启lazyfree-lazy-expire yes并把大 key 拆小别让单个 key 无限膨胀。5.4 坑四服务器时间漂移导致过期时间不准确EXPIREAT和PXAT依赖服务器的时间戳。如果你们 Redis 集群的服务器系统时间不同步个别机器快了或慢了几秒在分布式锁、限流这类对时间敏感的场景里就可能出现锁提前失效或者过期时间被无限拉长的怪现象。排查方法很简单在所有 Redis 节点上统一跑date命令看时间差。生产环境一定要配置好网络时间同步服务这个看似基础的东西一旦出问题排查起来非常烧脑。5.5 坑五大量 key 同时过期引发的雪崩这不是一个 bug而是设计疏忽。比如系统启动时统一往 Redis 里写几万个 key都设成当天有效到了午夜零点上万 key 同秒过期数据库瞬间被一波读写打懵。这就是典型的缓存雪崩。解决办法前面已经说了加随机过期时间、做分组分批缓存预热。另外可以观察监控里过期 key 的分布情况如果发现有波峰状的集中过期曲线就要及时调整过期时间策略。5.6 排查工具与命令速查最后整理一个排查过期问题时常用到的命令表方便大家按图索骥需求命令返回值/说明查看 key 剩余秒数TTL key-2 不存在-1 永久0 剩余秒数查看 key 剩余毫秒数PTTL key同上单位毫秒设置秒级过期EXPIRE key seconds返回 1 成功0 则 key 不存在或参数非法设置毫秒级过期PEXPIRE key millis同上设置到某个时间点过期EXPIREAT key timestamptimestamp 为 Unix 秒级时间戳创建时直接设过期SET key val EX seconds原子操作取并续期GETEX key EX seconds6.2 版本支持取消过期时间PERSIST key返回 1 表示取消成功0 表示原本没有过期时间查看内存淘汰策略CONFIG GET maxmemory-policy返回当前策略查看异步过期配置CONFIG GET lazyfree-lazy-expire返回 yes/no5.7 监控和预防建议除了会排查更重要的是预防。强调几个我养成习惯的做法一是所有缓存 key 的 TTL 设计要在需求评审阶段就确定下来不要上线之后才补。补 TTL 意味着要走一次发布流程而且很容易遗漏某些分支。二是线上环境定期做 key 扫描用SCAN配合TTL找出那些永久 key评估它们是否应该设置过期时间。Redis 的SCAN是增量迭代别用KEYS *否则主线程会被拉爆。三是监控体系里加一个过期 key 删除量的指标观察是否有瞬间激增。如果有大概率是某批 key 同时过期这时候要回头看业务代码是否设置了过于集中的过期时间。四是在代码层面做统一封装给缓存中间件配置一个默认 TTL 和统一续期方法避免每个人各写各的规则不一致。写在最后从让我交了不少学费的大 key 过期抖动到后来逐步摸清的惰性删除、定期删除、内存淘汰三者关系再到分布式锁、限流、缓存雪崩这些场景里过期时间的精妙设计我最大的体会是Redis 里的每一个参数都不是孤立存在的过期时间表面上只是一个数值背后却牵动着内存成本、访问延迟、数据一致性和系统稳定性。在实践里我会给自己定几条铁律新增缓存必须默认带上过期时间覆盖值时必须考虑是否要重置 TTL大 key 必须拆小并且开启异步淘汰所有过期时间宁可设短一点也不要在该失效的时候还赖着不走。Redis 是内存里的生意过期时间就是这门生意的止损线你给它一份主动权它还你一份安稳。希望这篇关于Redis 设置过期时间的实操梳理能帮你少踩几个坑。如果你也在项目中遇到过什么诡异的过期时间问题不妨按着上面的排查思路再走一遍大概率能在 TTL 的返回值里找到答案。