Redis过期时间机制详解:从命令到踩坑实战

发布时间:2026/9/18 6:04:25
Redis过期时间机制详解:从命令到踩坑实战 如果你在一个稍微有点规模的业务系统里待过一定遇见过类似这种诡异的问题Redis里的某个key明明设置了过期时间数据却迟迟没有消失另一个key没有设置过期时间结果内存一天天涨上去最后把整个实例拖垮。多数情况下问题的根源都指向同一个东西——Redis的过期时间机制。这个功能看起来不过是给key设一个TTL这么简单但真正用起来从设置命令、读取剩余时间、理解删除策略到利用过期事件驱动业务每个环节都有讲究。这篇文章把我这些年折腾Redis过期时间的经验完整梳理了一遍从命令细节到踩坑实录再到事件通知适合所有被缓存过期问题困扰过的开发者参考。1. 过期时间不只是缓存清理它是系统稳定性的底线1.1 我为什么专门研究过期时间之前维护过一个做营销活动的服务用户每天可以领一张优惠券我们把用户维度的活动状态写进Rediskey设计成activity:{userId}:{date}当时犯了个错误只想着设置过期时间让数据第二天自动消失结果大量key的过期时间设置在了同一时刻。活动整点开始的那一秒Redis的CPU使用率瞬间冲到90%多大量请求超时最后排查下来就是过期的key在同一秒内密集触发删除导致实例卡顿。那次之后我彻底意识到过期时间从来不是一个锦上添花的优化项而是直接影响系统可用性的核心机制。把过期时间用对你的缓存层才称得上稳定用不对轻则数据错乱重则线上事故。1.2 过期时间在项目里扮演的三个关键角色抛开理论单从实际项目出发过期时间至少承担着以下三类职责每一类都值得认真设计第一缓存数据的一致性保障。缓存和数据库之间天然存在数据不一致的风险给缓存key设置过期时间是成本最低的对账手段。业务数据更新后即使缓存没有主动失效到了过期时间也会自动拉回到数据库的最新状态。没有过期时间的缓存一旦某次更新逻辑漏掉了主动删除脏数据可能永远留在缓存里。第二临时凭证和限流的生命周期管理。登录token、短信验证码、秒杀接口的限流计数这些数据天然有时效性。验证码5分钟有效、限流窗口60秒重置用过期时间来实现再自然不过。这类场景还经常需要动态续期——用户连续操作时刷新token的有效期这就需要用到EXPIRE这类命令的续期能力。第三分布式锁和任务调度的自动释放。Redis实现分布式锁时必须给锁设置过期时间防止持有锁的服务宕机后造成死锁。任务队列里的延迟任务也经常借助过期时间来实现定时触发的效果。没有过期时间很多分布式场景根本没法安全落地。另外还有一个容易被忽略的作用防止冷数据堆积。即使业务中没有主动清理逻辑设置了合理过期时间的key也能在生命周期结束后自动释放内存避免无效key长期占用空间。2. 设置过期时间的五类命令和它们之间的细微差异Redis里能给key设置过期时间的命令不止一个很多人只知道EXPIRE和SETEX但实际生产中根据不同场景选择完全不同的命令会带来完全不同的效果。这一节把常用的设置方式全部过一遍。2.1 最常用的EXPIRE与它的毫秒级变体EXPIRE key seconds是最基础的设置过期时间命令以秒为单位。它作用于已存在的key返回值表示设置是否成功返回1说明设置成功返回0说明key不存在或操作未生效。 SET user:1001 zhangsan EXPIRE user:1001 300 (integer) 1与秒级对应的还有PEXPIRE key milliseconds精度更高适用于需要更短过期时间的场景比如接口限流窗口设置为500毫秒 SET rate:1001 1 PEXPIRE rate:1001 500 (integer) 1这里有个隐藏技巧EXPIRE/PEXPIRE是可以对已经存在但没设置过期时间的key直接补设的也可以对已有过期时间的key重新修改剩余时间。这个特性在滑动续期场景中非常有用。比如用户登录后每次有操作就重新执行一次EXPIRE让token的过期时间不断顺延用户只要持续活跃就不会被迫重新登录。2.2 SETEX与PSETEX创建即设置一步到位SETEX key seconds value和PSETEX key milliseconds value是写值设过期时间的原子组合操作。设计它们的主要目的是避免SET和EXPIRE分两步执行可能出现的中间状态如果设置完值、还没来得及设过期时间时服务崩溃这个key就成了永久key白白占用内存。 SETEX sms:13800138000 300 123456 OK需要注意的是SETEX在设置新值的同时会覆盖掉这个key上原有的过期时间如果key原本有TTLSETEX执行完会重新开始计算。另外它无法单独用来续期——续期该用EXPIRE。2.3 SET命令的EX/PX/EXAT/PXAT选项更细粒度的控制从Redis 2.6.12开始SET命令本身集成了过期时间参数这也是官方最推荐的做法之一 SET user:1001 zhangsan EX 300 OK SET user:1001 zhangsan PX 300000 OK SET user:1001 zhangsan EXAT 1750000000 OK SET user:1001 zhangsan PXAT 1750000000123 OK四个选项的区别在于单位和参照基准EX seconds秒级相对过期时间PX milliseconds毫秒级相对过期时间EXAT timestamp-seconds绝对过期时间需要一个Unix秒级时间戳PXAT timestamp-milliseconds绝对过期时间需要毫秒级时间戳EXAT和PXAT非常适用于业务端已经算好一个过期时刻的场景例如这个验券二维码到今天晚上24点失效——你可以在凌晨算出时间戳直接写死到key里无需再换算剩余秒数。2.4 EXPIREAT与PEXPIREAT指定过期时刻而非时长与SET EXAT类似的还有独立的EXPIREAT key timestamp和PEXPIREAT key timestamp-ms命令不过它们针对的是已存在的key作用等价于根据一个绝对时间点来设置过期。 EXPIREAT user:1001 1750000000 (integer) 1这里有一个细节值得注意如果你传入的是一个已经过去的时刻Redis会立刻删除这个key。所以这类命令也可以当作指定条件删除来用——把过期时刻等于当前时间效果等同于DEL。2.5 GETEX命令读取的同时修改过期时间GETEX是Redis 6.2引入的命令在读取key的值的同时给它设置一个新的过期时间。它有四个选项类似SET的EX/PX/EXAT/PXAT还有一个特殊参数PERSIST用来移除过期时间。 GETEX user:1001 EX 600 zhangsan GETEX user:1001 PERSIST zhangsan这个命令最大的应用价值在于读取即续期的原子场景比如热点配置的集中管理每次配置被读取时自动延长有效期活跃的配置一直不会消失不活跃的配置自然冷却过期不用再写两步操作。2.6 命令选型横向对比为了让你在实际项目中快速决策我把这些设置方式整理成了一张对照表命令/方式单位适用key状态是否会覆盖原值典型场景EXPIRE秒已存在否已存在key的续期PEXPIRE毫秒已存在否需要毫秒级精度的续期EXPIREAT绝对秒已存在否按固定时刻过期PEXPIREAT绝对毫秒已存在否高精度固定时刻过期SETEX秒存在/不存在是一步完成写入过期PSETEX毫秒存在/不存在是一步完成写入毫秒过期SET EX秒存在/不存在是官方推荐的标准写法SET PX毫秒存在/不存在是需要毫秒精度写入SET EXAT绝对秒存在/不存在是按固定时刻过期写入SET PXAT绝对毫秒存在/不存在是高精度固定时刻过期写入GETEX秒/毫秒/绝对/清除已存在否但可清TTL读取并续期一个经常有人搞混的细节对已有key执行SET命令不带EX系列参数会把这个key上已经设置的过期时间清除掉。也就是说如果你先SET了一个带过期时间的key之后又用SET更新了value但没带过期参数这个key就变成永久key了。生产环境里这种问题非常隐蔽排查起来也费劲后面我会专门讲。3. TTL族命令把剩余生命期读出来的正确姿势设置好了过期时间接下来就是读取。Redis提供TTL和PTTL两个命令分别以秒和毫秒为单位查看剩余过期时间。读法看起来简单但返回值的语义其实有不少坑。3.1 TTL与PTTL的返回值语义 SET user:1001 zhangsan EX 300 OK TTL user:1001 (integer) 297 PTTL user:1001 (integer) 296998返回值分三种情况正数key存在且有过期时间返回剩余秒数/毫秒数-1key存在但没有设置过期时间属于永久key-2key不存在很多人只关注正数的情况容易忽略-1和-2的区分。这个区分在实际业务里很有用用TTL key 0判断一个key是否存在且尚在有效期内比EXISTS key更安全。因为EXISTS只判断存在性不判断是否已经过期一个逻辑已过期但还没被删除的keyEXISTS还是返回1。3.2 精确判断过期没不能只看EXISTS这里我要详细说一下Redis删除过期key是惰性的一个key到了过期时间物理上不一定立即消失。所以 SET user:1001 zhangsan EX 1 OK sleep 2 EXISTS user:1001 (integer) 1 # key还在但已经过期了 TTL user:1001 (integer) -2 # TTL已经知道它过期了看到区别了吗key过了TTL之后EXISTS可能仍然返回1但TTL返回的是-2。因为在Redis内部key的真正死亡要等删除动作发生。所以如果你想判断一个key是否对业务生效应该优先看TTL/PTTL的返回值而不是EXISTS。这也是我做了很久才注意到的细节。3.3 批量查看过期时间的高效姿势生产环境偶尔需要排查哪些key快过期了、哪些key是永久key用TTL一条条查肯定不行。我一般用SCAN配合TTL或者直接写一段Lua脚本批量处理。一个简单的bash循环示例redis-cli --scan --pattern activity:* | while read key; do echo $key $(redis-cli ttl $key) done但这样每条key都发起一次网络请求效率不高。更推荐用Redis的pipeline批量执行redis-cli --scan --pattern activity:* | redis-cli --pipe这段只是示例实际生产里更好的做法是用Lua脚本一次性完成扫描和TTL查询local cursor 0 local result {} repeat local scan redis.call(SCAN, cursor, MATCH, ARGV[1], COUNT, 100) cursor scan[1] for _, key in ipairs(scan[2]) do local ttl redis.call(TTL, key) table.insert(result, {key, ttl}) end until cursor 0 return result使用EVAL执行时传入匹配模式就能拿到一批key的剩余过期时间一次调用搞定在key数量较大的情况下仍然很快。3.4 关于过期时间精度的重要补充在Redis 7.0之前TTL命令返回的秒数是向下取整的也就是说一个key实际剩余1.9秒时TTL可能返回1。不要用TTL等于0来判断刚好过期它可能还有不到1秒的存活时间。需要精确判断时务必用PTTL。Redis 7.0之后过期时间的设计做了优化底层用了更精确的过期跟踪方式但对上层TTL/PTTL命令的语义影响不大你仍然应该以PTTL为准做精确判断。4. 过期key的拆迁队删除策略与内存淘汰的边界设置和读取都理清了接下来要回答一个很多人困惑的问题过期时间到了key真的会被立刻删除吗答案是不一定。Redis的过期删除不是实时扫描全库的它采用了两套策略配合惰性删除和定期删除。4.1 惰性删除用的时候才清理惰性删除很好理解当有请求访问某个key时Redis会先检查这个key是否已过期如果已过期就立即删除并返回空结果。 SET code:1001 123456 EX 5 OK sleep 6 GET code:1001 (nil) # 访问时发现已过期删除并返回nil这种方式的好处是CPU开销最小只在访问时检查坏处是没有被访问的过期key会一直占用内存。如果一个key设置过期后永远没人访问它就永远赖在内存里不走。4.2 定期删除兜底的主动清理为了解决上面这个永远没人访问的问题Redis还会周期性地主动抽查一部分key检查是否过期过期则删除。这个动作由后台每秒执行固定次数的循环完成每轮会从设置了过期时间的key中随机抽取一批来检查。但注意随机抽取这四个字——它不是扫描全库而是抽样检查。所以仍然可能存在少量已过期key暂时没被抽查到、继续留在内存里的情况。这也是上一节里EXISTS返回1而TTL返回-2现象的底层原因。4.3 内存淘汰策略和过期时间的关系当内存占用达到maxmemory上限时Redis会根据配置的淘汰策略maxmemory-policy进行内存回收。这里有个关键区分淘汰策略处理的是所有key不只是已过期的key。常见淘汰策略包括noeviction不淘汰写命令直接报错allkeys-lru从所有key中按LRU淘汰volatile-lru只从设置了过期时间的key中按LRU淘汰allkeys-random从所有key中随机淘汰volatile-random只从设置了过期时间的key中随机淘汰volatile-ttl从设置了过期时间的key中按剩余时间从短到长淘汰这里要特别提醒如果你的实例配置了maxmemory并使用了volatile-xxx这类策略业务上一定要给key设置过期时间否则这些key永远不会被淘汰。之前见过一个事故Redis内存被打满后大量请求失败原因就是把淘汰策略配成了volatile-lru但很多关键的缓存key没有设置过期时间导致内存完全不可回收。4.4 为什么设置了过期时间内存却还在涨这是后台经常遇到的经典问题。设置了过期时间的key理论上到期后会被删除内存为什么还会持续增长原因有三个第一大量key同时过期之前内存会先增长到峰值。比如一批key设置了7天过期7天内这些key都有效内存就一直处于被占满的状态。第二惰性删除存在内存释放滞后。只要key没被访问、也没被定期删除抽中内存就一直被占用。在高写入低读取的场景下尤其明显。第三删除key的内存碎片不一定立即归还操作系统。Redis释放内存后jemalloc分配器会复用这些空间但RSS常驻内存不一定会立即下降。这是正常现象不代表内存泄漏。理解这些你就能明白过期时间不是内存管理的全部它还需要配合主动清理、内存淘汰策略以及监控告警一起使用。5. 实战中最常见的五个过期时间坑与完整绕过方案这一节我直接把实战中最容易踩的坑列出来每个坑都按现象 → 排查 → 解决方案的顺序讲方便你遇到类似问题时直接参照。5.1 坑一SET更新value时过期时间被悄悄清掉现象业务上明明给key设置了24小时过期第二天发现这个key还在甚至变成了永久key。根因中途有逻辑用不带过期参数的SET命令更新了value。Redis的SET默认会清除key上已有的过期时间。 SET user:1001 zhangsan EX 300 OK SET user:1001 lisi # 没带EX/PX参数 OK TTL user:1001 (integer) -1 # 变成永久key了排查链路先用TTL确认变成了-1再用OBJECT IDLETIME user:1001查看key的空闲时间对比最近一次业务更新数据的时间基本能定位到是哪段代码执行的SET。解决所有更新key的地方统一封装在SET时显式带上过期时间参数或者用SET user:1001 lisi KEEPTTL——Redis 6.0以后支持KEEPTTL选项可以在更新value的同时保留原有剩余过期时间 SET user:1001 lisi KEEPTTL OK5.2 坑二用TTL判断key是否存在误把-1当成有数据现象代码里写了类似if (ttl 0) { 用缓存 }的逻辑结果没有过期时间的永久key永远命中缓存。根因TTL返回-1表示key存在但没设过期时间如果判断条件只写了ttl 0会让永久key永远走不到重新加载数据的逻辑。排查链路检查代码中所有TTL判定的分支给测试key手动SET一个不带过期时间的值观察命中行为是否异常。解决用TTL判断业务数据有效性的逻辑建议统一改成# 有效 key存在且未设置过期时间 或 剩余时间大于0 TTL key ! -2或者更严格些期望所有缓存key都有过期时间那判断条件就应该写成ttl 0。具体选哪种取决于业务设计里是否允许永久key存在。5.3 坑三大量key设置同一过期时刻触发缓存雪崩现象整点时刻Redis CPU飙升、请求延迟上升、大量请求穿透到数据库。根因一批key的过期时间在业务逻辑里是固定算出来的比如所有用户的活动数据统一设置当天24点过期那么午夜0点之前那一瞬间数十万key同时到期触发集中删除。排查链路看监控里expired_keys指标是否存在瞬间暴涨的尖峰再查看业务代码中过期时间是否都来自同一个基准时间计算。解决在过期时间上增加随机偏移量打散过期时刻# 基础过期时间 随机0-300秒的偏移 SET activity:1001 data EX 86400 EXAT 1750000000推荐的标准做法是在基础过期时间上叠加一个随机值让key的过期时间均匀分布在一个时间区间内而不是全部集中在同一个点。5.4 坑四主从架构下过期key在主库已删、从库还在现象主从切换后新主库上读取到了应该已经过期的数据。根因Redis的主从复制中过期删除操作并不是直接把DEL命令同步给从库而是依赖从库自己执行惰性删除或定期删除。Redis Master删除一个过期的key时会向从库发送DEL命令而从库收到DEL后会删除对应的key。但如果key在主库过期时从库因为分区等原因没有立即收到DEL或主库的删除本身就是惰性触发的那么从库上这个key可能还在。排查链路对比主从两个实例上同一key的TTL返回值检查从库是否在过期时间窗口内返回了旧数据。解决从业务层面降低影响主从切换后缓存数据宁可穿透到数据库重新加载也不要信任从库可能过期的数据。可以在从库读逻辑中额外判断TTL把TTL为-2的key直接视为不存在多数时候更简单的方法是——待Redis 7.x版本之后主从之间的过期同步机制已经做了很多优化尽量把Redis版本升到7.0以上。5.5 坑五Redis Cluster模式下单key过期时间在迁移后失效现象对Cluster中的某个key设置了过期时间key迁移到另一个slot后过期时间莫名丢失。根因Cluster的key迁移slot迁移过程中如果使用了不带过期参数的RESTORE-ASKING等命令或者迁移完成后在目标节点重建key时没有保留TTL就会出现过期时间丢失的情况。这种情况多出现在使用某些客户端工具做数据迁移或扩缩容时。排查链路对比迁移前后同key的TTL值查看迁移工具的版本和参数很多老版本工具默认不迁移TTL。解决尽量使用支持TTL迁移的官方工具或新版本客户端迁移完成后跑一遍全量校验脚本用SCANTTL对比源节点和目标节点的剩余过期时间偏差超过阈值就重新迁移。生产环境更推荐的做法是迁移后不清缓存、让缓存自然过期重建避免人工迁移TTL带来的各种隐患。6. 让过期时间变成业务信号Keyspace过期事件通知过期时间不只是用来清理数据的它还能成为业务逻辑的触发器。Redis的Keyspace Notifications机制可以在key过期时发布事件通知订阅方收到通知后执行业务逻辑。听上去很美好但实际用起来有不少细节要处理。6.1 配置与订阅要启用过期事件通知需要修改redis.conf中的notify-keyspace-events参数或者用CONFIG SET命令动态开启 CONFIG SET notify-keyspace-events Ex OK其中E表示启用key事件通知x表示只发送过期事件。配置好后在客户端订阅__keyevent0__:expired频道0代表db0就能收到所有key过期的通知。用redis-cli订阅的效果 SUBSCRIBE __keyevent0__:expired当有key过期时会收到类似消息1) message 2) __keyevent0__:expired 3) order:1001注意第三个元素是key的名字而不是value。如果你需要拿到key对应的业务数据必须在key过期前把数据另存一份或者把关键信息放到key名里面。这是很多人第一次用时的困惑点。6.2 延迟问题过期通知并非实时过期事件通知是在key被真正删除时才发布的而删除可能是惰性删除或定期删除触发所以通知天然存在延迟。如果某个过期key一直没被访问也没有被定期删除抽中它的过期通知可能延迟很久甚至延迟到下一次抽查才发布。因此不要把过期通知当作高实时性的业务触发器用。比如用户下单后15分钟未支付就自动关单这种场景直接依赖过期通知做用户可能等了30分钟订单才被关闭体验很差。6.3 更好的替代方案真正可靠的延迟任务方案我更推荐用以下三种Redis Streams 延迟队列生产者把任务写入Stream消费者用XREADGROUP阻塞读取配合一个专门的待延迟ZSetscore为计划执行时间戳做延迟调度这种方案可控性强、消息不丢失。Redisson的RDelayedQueue基于Redis的分布式延迟队列实现已经封装好了使用成本低适合在Java生态中使用。定时扫表 状态机最传统的方案定期从数据库扫描超时订单状态机驱动关单。虽然多查一次库但逻辑最直白、最不容易出错。过期通知适合拿来做哪些事我实际用下来比较靠谱的场景包括统计类指标的清理通知、清理本地缓存的联动信号、以及对实时性要求不高的数据补偿。凡是晚几秒也能接受的场景用过期通知没问题凡是必须准点执行的场景别用它。把过期时间管理沉淀成一套习惯文章写到这儿核心内容基本讲完了。如果你问我这些年最大的体会是什么我会说Redis的过期时间从来不是一个设置了就完事的参数它是一个贯穿数据读写、故障排查、架构设计的系统性问题。我个人的做法是在每个项目里把过期时间的使用沉淀成一套规范所有缓存key必须有明确的TTL所有更新value的操作必须显式处理过期时间要么KEEPTTL要么重新指定所有判断key有效性的逻辑统一用TTL而不是EXISTS所有批量key的过期时间必须加随机偏移。这些规则看着琐碎但在线上救过我很多次。最后再分享一个小技巧给key命名时把过期时间相关的信息带上比如cache:user:info:{id}:ttl300这样所有同事在Redis Desktop Manager里看到key就能直接知道它的生命周期策略排查问题时能少走很多弯路。过期时间的坑大多不是Redis本身造成的而是使用的人对它的理解还停留在表面。希望这篇文章能帮你把这块短板补上。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询