news 2026/10/10 7:25:23

Redis过期时间详解:从TTL设置到缓存淘汰与实战避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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和PX,EXAT和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 30

NX表示只有这个 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/no

5.7 监控和预防建议

除了会排查,更重要的是预防。强调几个我养成习惯的做法:

一是所有缓存 key 的 TTL 设计要在需求评审阶段就确定下来,不要上线之后才补。补 TTL 意味着要走一次发布流程,而且很容易遗漏某些分支。

二是线上环境定期做 key 扫描,用SCAN配合TTL找出那些"永久 key",评估它们是否应该设置过期时间。Redis 的SCAN是增量迭代,别用KEYS *,否则主线程会被拉爆。

三是监控体系里加一个"过期 key 删除量"的指标,观察是否有瞬间激增。如果有,大概率是某批 key 同时过期,这时候要回头看业务代码是否设置了过于集中的过期时间。

四是在代码层面做统一封装,给缓存中间件配置一个默认 TTL 和统一续期方法,避免每个人各写各的,规则不一致。

写在最后

从让我交了不少学费的"大 key 过期抖动",到后来逐步摸清的惰性删除、定期删除、内存淘汰三者关系,再到分布式锁、限流、缓存雪崩这些场景里过期时间的精妙设计,我最大的体会是:Redis 里的每一个参数都不是孤立存在的,过期时间表面上只是一个数值,背后却牵动着内存成本、访问延迟、数据一致性和系统稳定性。

在实践里,我会给自己定几条铁律:新增缓存必须默认带上过期时间;覆盖值时必须考虑是否要重置 TTL;大 key 必须拆小并且开启异步淘汰;所有过期时间宁可设短一点,也不要在该失效的时候还赖着不走。Redis 是内存里的生意,过期时间就是这门生意的止损线,你给它一份主动权,它还你一份安稳。

希望这篇关于"Redis 设置过期时间"的实操梳理能帮你少踩几个坑。如果你也在项目中遇到过什么诡异的过期时间问题,不妨按着上面的排查思路再走一遍,大概率能在 TTL 的返回值里找到答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/10 7:25:23

同步发电机突然三相短路Simulink仿真:从暂态分析到时间常数提取

同步发电机突然三相短路这个课题,是我最近完整跑了一遍的经典电学暂态仿真项目。说实话,做之前以为就是搭个模型、扔个故障、看波形,做之后才意识到里面藏着“三个时间常数、四个电流分量”这一整套电机暂态分析的核心逻辑。这个课题既能用来…

作者头像 李华
网站建设 2026/10/10 7:24:04

LLM工具可靠交付的44道质量门禁:从幻觉溯源到部署一致性

1. 工具交付的最后一公里,卡在最土的问题上去年,我所在的某实验室接手了一个内部科研项目:基于大模型做文献结构化抽取。前期跑 demo、写论文、做汇报都很顺,但到了真正交付给课题组每天使用时,问题一下子全冒出来了—…

作者头像 李华
网站建设 2026/10/10 7:24:03

GESP八级真题详解:树形DP求解树上旅行问题

1. 题目拆解与背景分析1.1 这道题在考什么先说结论:2025年6月GESP C八级这道“树上旅行”,不是一道纯粹靠背模板就能过的题。它把树形结构、深度优先遍历、状态设计与动态规划几个核心考点揉在了一起,表面看是“在树上走一走”,实…

作者头像 李华
网站建设 2026/10/10 7:24:03

某鱼item_get接口原理与高效调用实践

1. 为什么“item_get”不是万能钥匙——从某鱼商品详情接口的命名陷阱说起刚接触某鱼开放平台的开发者,第一眼看到item_get这个接口名,十有八九会下意识认为:“哦,这是个标准的、通用的商品详情获取接口,和淘宝的taoba…

作者头像 李华
网站建设 2026/10/10 7:19:22

全国机场吞吐量排名数据获取与清洗全攻略(2006-2024)

这些年我一直在做民航相关的数据整理工作,最常被问到的一个问题是:“全国的机场吞吐量排名到底去哪儿查最靠谱?”说实话,这题看起来简单,真正动手做过的人才知道里面坑有多深。单说“旅客吞吐量”这个指标,…

作者头像 李华