Redis过期时间:从缓存雪崩到分布式锁的实战指南

📅 发布时间:2026/10/3 11:00:24
Redis过期时间:从缓存雪崩到分布式锁的实战指南
一条故障短信引发的思考过期时间不是“锦上添花”大概半年前我们一个运营后台在晚高峰突然慢得像幻灯片接口响应从几十毫秒飙到两秒以上。查了一圈数据库慢日志里全是同一个大表的查询而这个表的数据在 Redis 里明明有缓存。诡异的是缓存命中也正常但就是有几台机器在兜底查库。后来定位到原因缓存 key 的过期时间设成了“固定值”凌晨某个时间点一过上万个 key 在同一秒过期缓存系统瞬间被打穿流量全数打到数据库上。这个事故让我对“redis设置过期时间”这件事有了完全不一样的认识。很多人觉得 SETEX 一条命令的事有什么好研究的但真正在线上跑过、被坑过之后就明白过期时间绝不是一个键值对的“生存倒计时”而是 Redis 内存治理、缓存一致性、高可用设计里最核心的枢纽之一。这篇文章不打算复述文档而是从我实际排查和设计过的场景出发把设置过期时间这件事掰开揉碎命令怎么选、底层删除机制怎么走、哪些隐藏坑是文档不会告诉你的、以及分布式锁、缓存雪崩、验证码这类真实场景里过期时间该怎么用。无论你是刚接触 Redis 的新手还是已经在线上维护 Redis 集群的工程师这篇都能给你一些可以“抄作业”的经验。1. 五种设置过期时间的方式各有各的脾气先捋一遍最基础的命令。表面上看都是“让 key 过期”但每条命令的语义、适用场景、潜在问题完全不一样。1.1 EXPIRE 与 PEXPIRE最直白的“剩余生存时间”EXPIRE key seconds # 单位秒 PEXPIRE key ms # 单位毫秒用法很简单给一个已经存在的 key 设置生存时间。比如你用一个 hash 存用户购物车操作过程中不想再用 SET 覆盖它就可以单独调用 EXPIREredis SET cart:1024 item_a,item_b OK redis EXPIRE cart:1024 1800 (integer) 1返回 1 表示设置成功返回 0 说明 key 不存在Redis 压根没给你设置。一个很容易犯的错是对不存在的 key 调用 EXPIRERedis 不会报错但也不会做任何事。有些人在代码里先逻辑判断再逐字写如果 key 已经被并发删了EXPIRE 表面成功实际失败后续这个 key 就变成永不过期直到内存被打爆才发现。PEXPIRE 就是毫秒版。普通业务用秒级就够了但像限流、秒杀、分布式锁这种需要精细控制的场景毫秒级反而更顺手。后面讲分布式锁时会专门提到。1.2 SETEX 与 SET EX把“值”和“过期时间”一起托付SETEX key seconds value SET key value EX seconds SET key value PX milliseconds这两条是整个 Redis 里最推荐日常使用的写法。理由很简单一个命令完成“写值 设过期”两件事原子性天然有保障。设想一下用 SET EXPIRE 分两步写SET verify_code:13800138000 482913 EXPIRE verify_code:13800138000 60如果程序在 SET 之后、EXPIRE 之前崩了这个验证码就成了永不消失的常驻内存垃圾。虽然重启能清掉但线上 Redis 哪能随便重启相比之下 SETEX 一条线到位少了中间态也少了出问题的窗口。另一个实用技巧是SET key value EX seconds NX这在分布式锁里几乎是标配写法。NX 表示“只有当 key 不存在时才能设置成功”直接解决了“加锁 过期时间”两步操作的原子性问题。通常项目里我会建议能用一个命令解决的绝不用两个命令能用 SETEX 的绝不用 SET EXPIRE。1.3 EXPIREAT 与 PEXPIREAT指定“过期时刻”的另类方案EXPIREAT key unix-timestamp-seconds PEXPIREAT key unix-timestamp-milliseconds这两个命令接收的是 Unix 时间戳而不是相对时长。比如让某个 key 在今晚 23:59:59 过期EXPIREAT activity:banner 1702396799用得少但也不是没用。比如运营活动在固定时间点结束用时间戳直接表达业务语义好处是过期时刻不受设置时刻影响。即使你在 23:00 设置业务方要求 23:59:59 失效调用的语义非常清晰。不过这里有个天然的坑Redis 服务器时间必须和业务服务器时间保持一致。如果 Redis 所在机器的时钟被 NTP 校准或者被人为拨快了那这个 key 可能会提前消失。分布式环境下我会优先用相对时间EXPIRE/SETEX少用绝对时间戳避免“时间不同步”这类玄学问题。1.4 PERSIST、TTL 与剩余时间的观测方法有设置自然有取消和查询。TTL key # 返回剩余秒数 PTTL key # 返回剩余毫秒数 PERSIST key # 移除过期时间key 变为永久TTL 返回负数时很多人会搞混TTL 返回 -1key 存在但没有设置过期时间永久有效TTL 返回 -2key 不存在或已经过期被删除排查线上问题的时候我习惯先 TTL 一把看看是不是“忘记设过期时间”了。如果一个核心缓存 key 的 TTL 常年是 -1那基本可以断定代码里漏了 SETEX或者用了 SET 之后再没调用 EXPIRE。这种“隐形内存泄漏”比显式写错逻辑更难抓因为你光看业务功能一切正常要等到内存报警才意识到问题。1.5 一个容易被忽略的细节EXPIRE 对“正在被操作”的 key 的影响如果某个 key 当前正被 Lua 脚本、事务MULTI/EXEC操作此时执行 EXPIRERedis 会等这些操作完成后再应用过期时间。这个行为对大多数人透明但在高并发场景下如果逻辑依赖“这个 key 立即失效”要注意它可能比你预期晚几毫秒才真正过期。此外对一个已经有过期时间的 key 再次执行 EXPIRE新值会直接覆盖旧值。这本身不是问题但我在踩坑时就遇到过缓存预热任务每 5 分钟跑一次每次都重新 SETEX 一个 10 分钟的过期时间导致 key 永远删不掉因为预热频率比过期时间短。所以在设计缓存时要先想清楚谁是“设置过期时间的主人”——是写入方负责还是读取方负责。2. 过期之后的清理机制Redis 不是一个到点就删的闹钟很多新手的误解是设了过期时间Redis 会在那一刻“叮”一下把 key 删掉。但真实情况是Redis 的删除策略分三层而且没有一层是“精确到毫秒的定时删除”。2.1 惰性删除不查就不删省事但留隐患惰性删除的核心逻辑是当客户端访问一个 key 时Redis 先检查它是否过期过期就删除返回空没过期就正常处理。这个策略的好处是节省 CPU不浪费任何资源去扫描根本不会被访问的 key。坏处也很明显如果一个 key 设了过期时间但从此没人再访问它就会一直躺在内存里。比如一个优惠券 key 设了 7 天过期但用户没再打开过页面那这个 key 在 7 天后也不会被主动删掉而是继续占着内存直到某次被访问才被发现“哦你过期了”。所以光靠惰性删除内存迟早会出问题。Redis 需要第二个机制。2.2 定期删除后台巡检团但不会全量扫定期删除是 Redis 内部一个周期任务默认每秒 10 次由hz参数控制会抽查一部分设置了过期时间的 key如果发现有过期的就直接删掉。它不会遍历全库而是采用随机抽查的方式每次默认检查 20 个 key如果其中过期的比例超过 25%就继续再抽一批直到比例降下来。这样设计的聪明之处在于定期删除只针对有过期时间的 key而且让清理速度跟上写入速度。我在实际运维中遇到过的情况是某个业务一次性往 Redis 里写了几十万个短时 key比如每 30 秒刷新一次的埋点数据如果都是 30 秒过期那 Redis 的定期删除任务会在过期集中爆发时忙一阵但也能扛住。2.3 内存淘汰策略过期时间的最后一道闸门即使惰性删除 定期删除都没来得及清理Redis 还有一层兜底内存淘汰策略。当你设置了maxmemoryRedis 内存用满后会根据maxmemory-policy来淘汰数据其中几种策略和过期时间直接相关策略含义noeviction内存满了直接报错不淘汰任何 keyvolatile-lru只在“设置了过期时间”的 key 里按 LRU 淘汰volatile-ttl只在“设置了过期时间”的 key 里按剩余 TTL 从短到长淘汰allkeys-lru / allkeys-lfu在所有 key 里淘汰不管有没有过期时间这个设计有一个很微妙的含义设置的过期时间不仅是业务逻辑也是 Redis 内存治理的一部分。在内存快满时volatile-ttl 策略会优先把 TTL 最短的 key 淘汰掉这跟业务希望“短命 key 先走”的目标完全一致。我见过不少团队把 maxmemory-policy 设成 allkeys-lru导致有些本应保留 1 小时的临时 key 被淘汰而真正该淘汰的缓存却因为刚被访问过而存留下来。2.4 大 key 过期时的阻塞风险一个很容易踩的线上坑这是全文最重要的一个实战警告如果一个大 key比如一个存了几十万个元素的 hash 或 list到了过期时间Redis 在大幅删除它时会造成主线程短暂阻塞。原因很简单Redis 是单线程模型删除一个巨型对象需要遍历其中的所有元素来释放内存这个遍历过程会占据主线程其他所有命令都得排队等。我碰上过一次真实事故一个 list 类型的 key 积攒了大几十万条数据到点过期结果 Redis 突然抖了一下所有读写请求毛刺明显业务方直接收到超时告警。Redis 4.0 之后提供了解决方案lazy-free 机制。开启lazyfree-lazy-expire yes后过期 key 的删除动作会放到后台线程异步执行主线程不会被卡住。但要注意异步删除也有代价内存释放不及时瞬间内存占用会短暂升高。所以最根本的做法还是别让 key 长得太大超过几 MB 的对象就该考虑拆分或者压缩。3. 反直觉的冷知识过期时间在持久化、主从、集群里的表现这部分内容平常人不遇到线上事故不会在意但一旦遇到就是大事。3.1 主从复制里过期时间怎么传在主从架构下从库不会自己“独立思考”去删除过期 key。主库负责执行删除然后把 DEL 命令同步给从库。这意味着从库在内存里可能短暂存在“已经在主库过期但还没被删”的 key。但这带来一个有意思的细节客户端读从库时如果读到这么一个 key从库会返回什么Redis 3.2 之后从库在返回数据之前会先按自己的逻辑判断 key 是否过期如果过期会返回空结果即便内存里还有这个 key。所以主从不会出现“主库删了从库还返回旧数据”的不一致问题。不过这个判断机制依赖从库的时钟时钟漂移依然可能造成边界差异。3.2 RDB 与 AOF持久化的文件里过期 key 会被怎么处理先说 RDB生成 RDB 快照时Redis 会跳过已经过期的 key加载 RDB 时如果是主从架构的从库不会删除过期 key而是等主库的 DEL 命令同步这个逻辑是为了防止从库加载过快导致主从数据不一致。再来说 AOF当 Redis 以 AOF 方式持久化时key 过期不会立刻产生一条“删除”日志但等到它真正被删除惰性或定期Redis 会追加一条 DEL 日志到 AOF。用aof-use-rdb-preamble开启混合持久化后重写 AOF 文件时同样会把已过期的 key 过滤掉。这些细节在灾难恢复演练中尤其重要。如果你用 RDB 文件恢复数据一定要知道RDB 到点保存的瞬间那些“即将过期但还没过期”的 key 会被一并持久化恢复之后它们依然带过期时间继续倒计时而不是恢复成永久 key。3.3 集群模式下对 TTL 的一致性与时钟依赖Redis Cluster 里key 的过期时间本质上还是沿着主从链复制逻辑和单机主从一样。但要小心的是不同节点的系统时钟如果偏离过大“相对时间”会随着这次时钟偏差产生误差。相对时间在 Redis 内部转换成绝对时间戳来比较服务器时钟回拨会让一批 key 生命周期整体拉长或缩短。我在云环境里就遇到过宿主 NTP 同步导致 Redis 节点时钟小范围跳跃结果一批短时 key 早过期了几秒限流策略突然全部失效。所以无论单机、主从还是集群都建议把服务器的时钟同步NTP纳入巡检项。这听起来和“Redis 过期时间”没关系但在分布式环境下它就是有直接关系。4. 真实场景拆解过期时间在项目里怎么设计才科学命令和原理只是底料真正见功力的是场景设计。我挑三个最常见的项目场景展开说。4.1 缓存雪崩防御给过期时间加一点“抖动”回到文章开头那个事故。全部缓存 key 在固定时长的同一秒到期请求同时穿透到数据库这就是经典的缓存雪崩。防御做法不是“不要设过期时间”而是给过期时间加一个随机的偏移量。比如基础过期时间 10 分钟实际设置时随机增加 0 到 60 秒SETEX product:detail:1024 (600 random.randint(0, 60)) product_json这样所有 key 的失效时间被摊开数据库压力就不会瞬间集中。理论上随机偏移越多分散效果越好但对过期精度要求高的场景要控制抖动范围。另一个思路是多级过期热点数据用一个较短的 TTL 后台定时刷新非热点用长 TTL。我给一个数据报表系统做过这个方案核心报表缓存 30 秒过期、冷门报表 10 分钟过期配合后台每 20 秒预热一次核心报表数据库负载非常平稳。4.2 分布式锁过期时间设多少才不翻车分布式锁是“过期时间”最容易翻车的场景。常规写法SET lock:order:1024 unique_id NX EX 30这里 EX 30 意思是锁最多持有 30 秒到期自动释放。这个 30 秒怎么定设太短业务还没执行完锁就没了另一个线程拿到锁两份逻辑同时跑数据错乱。设太长如果持有锁的节点崩溃了其他线程要等很久才能拿到锁系统可用性变差。网上讨论很多但实际项目里不能拍脑袋。我的经验是分三步先评估业务临界耗时正常情况下完成临界区的最长时间。比如写一条订单 扣库存线上 P99 耗时是 800ms那锁 3~5 秒就非常安全。再考虑意外情况。如果下游数据库抖动临界区执行时间拉长锁就不够用。完整的方案是“自动续期”Redis 官方推荐的 Redisson 里的看门狗Watchdog就是这个思路默认锁 30 秒如果业务没执行完后台每 10 秒自动续期 30 秒。锁的持有者挂了看门狗也随之停止锁会在原过期时间之后自动释放。所以**“设置过期时间”在分布式锁里不是一成不变的参数而是“超时保护”和“业务时长”之间的一场持续博弈。** 我见过太多团队把锁时长设成 10 秒、30 秒就再也不管结果业务偶发慢查询导致锁提前释放线上出现重复下单。如果有条件优先引入 Redisson 这类自带续期方案的客户端如果自己实现一定记得加续期循环逻辑。4.3 验证码与限流短过期时间的“精度战争”验证码、短信防刷、接口限流这三类场景对过期时间的要求是“短、准、不容易并发穿透”。短信验证码我建议用 SETEX 有效期 60~120 秒并配合每设备/每号码的发送间隔锁间隔锁用 60 秒过期。这里有个容易漏的细节客户端可能在验证码过期的最后一秒提交请求服务端校验时 key 刚好没了导致验证失败。优化做法是校验接口允许 TTL 剩余 3 秒之内的验证码“再宽限一次”或者生成验证码时把过期时间从 60 秒拉长到 65 秒提前预留给网络传输的耗时。接口限流的滑动窗口Redis 官方示例用的是 ZSET 按时间戳记录请求过期时间设置成窗口长度的两倍例如每分钟限流 100 次就存储 120 秒内的记录ZREMRANGEBYSCORE key 0 (now - 60) ZADD key now request_id EXPIRE key 120这样窗口外的记录会被清走窗口本身的清理逻辑依赖过期时间兜底。如果 EXPIRE 漏了ZSET 里会堆积无穷无尽的旧时间戳内存会缓慢上涨。这里 TTL 的作用从“业务时效”变成了“内存上限保护”本质是一致的所有有限生命周期的数据都应该显式设置过期时间。4.4 数据预热与延迟删除过期时间的高级玩法还有一种玩法是把“设置过期时间”当成一个轻量级任务调度器。比如延迟通知把任务内容写进一个 key设置 60 秒过期然后订阅 Redis 的过期事件keyspace notifications在收到过期消息时执行异步操作。这种方案可以替代部分简单的延迟队列不过要非常注意Redis 过期事件不是精确的可能延迟几百毫秒甚至更久大 key 用这个方案删除和事件触发时机不可控集群模式下过期事件只在 key 所在节点触发消费端要做好分布式处理我建议延迟任务场景如果要求精确直接使用专业消息队列如果只是“大致过了 5 分钟后要做个清理”用 key 过期事件做辅助没问题。5. 实操复盘一次缓存 key 不定期丢失的排查过程最后分享一个让我学到最多的排查案例完整链路走一遍你会对过期时间有更立体认知。现象线上有些商品详情页偶尔出现“缓存失效、回源数据库”的日志但频率不高查缓存命中率也没明显下降。我们最初以为只是正常过期后来发现商品 key 的过期时间设的是 1 小时怎么可能每隔 20 分钟就丢一次排查第一步看 key 是否存在。通过 Redis 客户端连接直接 GET发现 key 有时存在有时不存在而且不存在的时刻和业务日志时间对不上。排查第二步检查有没有别的程序删 key。用MONITOR命令跟踪一段时间发现不是我们项目代码产生的 DEL而是另一个团队做了“全量缓存清理”的定时任务他们会按业务前缀批量删除“他们认为可清理”的 key。你设置的过期时间在别人眼里可能一文不值。排查第三步检查内存淘汰。执行INFO stats查看evicted_keys指标发现持续增长。说明 Redis 内存接近 maxmemory触发了淘汰而淘汰策略正好是不管 TTL 长短的 allkeys-lru。有些商品 key 虽然在有效期内但因为没有被近期访问就被当作 LRU 牺牲品清理了。修复方案分两层一是和那个团队约定清理白名单不允许跨领域批量删 key二是把 Redis 内存策略改成 volatile-lru确保只淘汰带过期时间的 key而且业务核心缓存全部改成 SETEX保证它们都在“可淘汰池”里但优先级相对靠后。这个案例给我的教训就是在共享的 Redis 实例里“过期时间”不只属于你还属于整个集群的治理体系。设置过期时间之前至少要确认三件事有没有人会主动删我的 key、内存淘汰策略是什么、我的 key 是否是淘汰优先级偏高的对象。6. 把过期时间用明白之后我的一些体会踩过这些坑之后我对 Redis 过期时间的态度从“一条 SETEX 搞定”变成了“一套系统设计”。如果只留三条最核心的经验我会写第一所有写入 Redis 的数据要么设置过期时间要么有明确的永久语义 定期清理机制。不要让任何 key 在无意中变成“永久数据”这是内存治理最基本的底线。第二过期时间既是业务逻辑也是运维策略。设计时要考虑缓存雪崩、内存淘汰、主从复制、大 key 阻塞这些看起来离业务很远、实际上随时会反噬业务的底层机制。第三分布式环境里过期时间的可靠性依赖时钟和服务协作。同一套代码在不同机器上表现不同优先用相对时间确保 NTP 同步并对“提前过期”“延迟过期”都有容错。如果你现在正要给 Redis key 设置过期时间先别急着手写命令花十分钟想清楚这几个问题这个 key 生命周期应该多长到点后删除会不会造成缓存雪崩如果 Redis 内存满了它会不会被提前淘汰分布式锁场景下锁过期了业务没跑完怎么办想清楚这些你再回来写 SETEX心里会踏实很多。