先把结论放在最前面Redis的过期时间不是某一个命令的事而是一整套从写入、校验、清理到淘汰的完整体系。把过期时间设置理解透意味着你同时掌握了内存管理、缓存一致性、分布式锁乃至主从复制的很多底层逻辑。这篇文章我会结合自己实际踩过的坑把Redis过期时间的原理、操作方式、设计技巧和排查方法一次讲清楚适合刚接触Redis的初学者也适合写了好几年缓存却偶尔“丢了key”的开发者。1. 为什么说过期时间是Redis里最值得花心思的部分1.1 过期时间解决的三个核心问题先说最直接的如果Redis里的key没有过期时间会怎么样内存会越占越多最终触发OOM或者淘汰策略把不该淘汰的数据挤掉。比如一个用户会话Session key如果永久保存几千万用户登录一次内存直接就爆了。过期时间就是Redis对“数据生命周期”的管理手段让我们可以告诉Redis“这个数据只活60秒时间到了你就自己处理掉”。第二个问题是数据时效性。缓存的目的就是让数据“快”但数据源变了之后缓存里的旧值就成了脏数据。设置合理的过期时间相当于给数据一个“保鲜期”过期之后主动失效下一次读取再从数据库拉新值回填。比如商品库存、热点文章阅读量这类数据如果不过期用户看到的永远是旧版本。第三个问题是业务层面的服务治理。很多场景我们需要让某个动作在一段时间内只能做一次典型的比如短信验证码、接口防重复提交、分布式锁。这些需求本质都是“给状态一个倒计时”如果没有过期时间就得靠定时任务去清理复杂度会高很多。只要用到Redis做这类场景设置过期时间就是整个方案的地基。1.2 从热点关键词看这个问题的覆盖面我看了一下后台的关键词redis缓存、redis分布式锁、redis持久化、redis集群这些词都排得很靠前说明大家搜索Redis的时候基本离不开这几类问题。但你会发现几乎所有这些场景的落地最后都会绕回到“这个key的过期时间怎么设”。缓存治理要设过期时间不然缓存项无限增长分布式锁要设过期时间不然锁持有者宕机就死锁了持久化要考虑过期时间在RDB/AOF里怎么存恢复后还能不能继续生效集群模式下过期key的广播删除、主从一致性也和过期时间强相关。所以这篇文章虽然标题是“Redis设置过期时间”实际我聊的是一张完整的知识网络。把这些概念串起来之后你再回头去看很多Redis问题思路会清晰很多。2. 过期删除机制Redis怎么处理到期的key2.1 Redis不会立刻删除过期的key很多人有个误解认为只要设置了过期时间到了那个时间点key就“瞬间消失”。实际上Redis对过期key的删除是异步的、延迟的它有三条策略配合工作主动删除、惰性删除和内存淘汰。主动删除是每隔100ms随机抽取一批设置了过期时间的key检查是否过期如果过期就删除。这里有两个关键点官方配置文件里的hz就是控制这个检查频率的默认是10也就是每秒执行10次周期任务另外它是随机抽样而不是全量扫描所以过期key很多的时候单靠主动删除是清不完的。惰性删除则是在每次访问key的时候先检查它是否已经过期如果过期就直接删除并返回空。这保证了你在读一个key的时候绝对不会拿到已经过期的旧值。两条策略配合称为“定期删除惰性删除”的双策略。第三层才是内存淘汰。如果内存达到maxmemory上限Redis会根据配置的淘汰策略比如allkeys-lru、volatile-ttl这些去选一些key删除。注意这里的删除机制本身也是一种兜底但它不是“只删过期的”而是内存不够了才动很多人把这个阶段和过期删除混在一起排问题的时候容易绕弯。2.2 主从模式下过期删除有一个容易踩的坑在Redis 3.2版本之前主从复制的时候主库删除一个过期key会向从库发送DEL命令但在3.2之前从库对过期key的处理是被动等待主库通知也就是说如果从库被外部直接读取即使key已过期从库也不会主动删除而是可能返回旧数据。从3.2开始从库也会维护自己的逻辑时钟读到过期key时返回nil并异步发起本地删除主库依然会发送DEL命令做兜底。这一点在你排查“为什么从库读到了已过期的数据”时要先确认版本的。另外在Redis 7.0里针对大key、Hash/Set等容器类型做了进一步优化当容器里的大部分字段过期后会更快地回收整个key。我在升级到7.0之后明显感觉容器类型混合过期的场景省心很多。2.3 过期时间不会导致立即删除对你意味着什么这意味着当大量key同时到达过期时间时Redis可能出现短暂的CPU升高和内存占用波动因为清理是批量异步做的。我在压测的时候遇到过一个业务量很大的key分片里几百个key同一秒过期那一瞬间内存没立刻降下来但CPU会有一个小的尖峰因为主动删除线程需要遍历抽样。这是一个很实际的问题后面第4节讲缓存雪崩的时候你会看到为什么大家都要把过期时间打散其实就是因为“同时过期”带来的删除压力和缓存重建压力都是双重的。3. 实操从简单到进阶的过期时间设置方式3.1 最常用的三种设置方式第一种是在写入key的同时指定过期时间用的是SET命令的EX或PX参数。EX的单位是秒PX的单位是毫秒。示例# 设置字符串key10秒后过期 SET user:1001:token abc123 EX 10 # 设置字符串key5000毫秒后过期 SET user:1001:token abc123 PX 5000第二种是针对已经存在的key设置过期时间用EXPIRE命令。PX选项对应毫秒同时也可以设置NX、XX、GT、LT这些条件选项只在某种条件下更新过期时间。比如只给没有过期时间的key设置过期时间用EXPIRE mykey 60 NX这个在批量治理存量数据时非常有用。# 对已存在的key设置60秒过期 EXPIRE user:1001:token 60 # 给key设置一个很久远的过期时间等价于取消过期 PERSIST user:1001:token第三种是SETEX、PSETEX这两个命令把设置值和设置过期时间合并成一个原子操作。SETEX user:1001:token 60 abc123效果等同于SET EXPIRE但更重要的是原子性如果在SET之后、EXPIRE之前进程崩溃key就成了永不过期的孤儿数据。所以任何时候设置key带上过期时间我都是建议用原子方式不要分开写。还有个细节EXPIRE的返回值是1表示设置成功0表示key不存在。我第一次用这个命令排查问题时发现连续执行两次EXPIRE第二次返回0当时以为是过期时间出问题了后来才发现是第一次执行后key已经过期被删掉第二次自然就不存在了。3.2 查看剩余时间TTL和PTTL排查过期问题最常用的就是TTL返回剩余秒数PTTL返回剩余毫秒数。有几种特殊返回值返回值含义大于0离过期还剩多少秒-1key存在但没有设置过期时间-2key不存在或者已经过期被删除这个表几乎所有Redis面试都会考但实践中更有价值的用法是TTL从大往小递减如果发现某个key的TTL突然跳到-1说明它的过期时间被清掉了最常见的元凶是执行了PERSIST或者SET一个新值但没带EX参数SET会清除过期时间这点一定要记住。命令行下还可以通过DEBUG OBJECT命令查看key的剩余存活时间但这是调试命令生产环境慎用。3.3 过期时间适用于哪些数据类型这一点经常被忽视。过期时间不是字符串key专属的Hash、List、Set、ZSet这些数据类型也都可以设置过期时间。需要留意的是Redis的过期粒度是整个key而不是某个字段或者某个元素。也就是说你要让Hash里某个field单独过期必须用Redis 7.4引入的Hash Field TTL功能或者自己在field值里存过期时间做逻辑过期。我见过不少项目用Hash存用户信息每个field想单独控制过期时间然后把整个Hash的过期时间设成动态值结果发现要么字段生命周期被整体重置要么为了精细化反而搞出一堆兼容代码。这类需求建议要么接受全key粒度要么把单字段拿出来单独存字符串key要么在值里带一个timestamp自己做逻辑过期。3.4 批量设置和批量清理的注意点Redis官方并没有直接提供“批量设置过期时间”的命令但你可以用MULTI事务或Lua脚本批量操作。如果只是针对一批key设置相同的过期时间下面的Lua脚本可以直接参考local keys KEYS for i, key in ipairs(keys) do redis.call(EXPIRE, key, ARGV[1]) end return #keys用EVAL调用时把key列表和过期秒数传进去。这个脚本在分批清理历史缓存的时候很实用但要注意一次不要传太多keyRedis是单线程的脚本跑太久会阻塞其他命令。我一般一个脚本控制在几千个key以内配合延时调用。批量删除过期key则不建议用KEYS去匹配再删除生产环境里KEYS命令在数据量大时会阻塞实例。正确姿势是使用SCAN命令分批迭代每批拿到key之后检查TTL和业务规则再决定是否删除。这个坑我早期踩过KEYS一条命令把整个Redis打挂过后来直接写成了SCAN管道处理的脚本稳多了。3.5 应用代码里的标准写法以Java的Spring Data Redis为例最标准的写法是直接用RedisTemplate// 写入并设置过期时间 redisTemplate.opsForValue().set(user:1001:token, abc123, 10, TimeUnit.SECONDS);Python的redis-py对应写法r.set(user:1001:token, abc123, ex10)注意一个常见问题用GenericCommand之类的客户端给已存在的key设置过期时间时要确认key确实存在。很多客户端有setIfAbsent和setIfPresent的区别配合过期时间使用的时候要看清参数名避免出现“明明设置了过期时间却永久保存”的情况。3.6 分布式锁里的过期时间分布式锁是过期时间最常见的进阶场景。基本思路是SET lock_key unique_value NX PX 30000拿到锁的线程在业务执行完后主动删除lock_key释放锁如果线程崩溃锁也会在30秒后自动过期不会死锁。这里有一个教科书级的坑删除锁的时候不能直接DEL否则可能删掉别人刚获取的锁。正确做法是先用Lua脚本比较当前lock_key的值是不是之前写入的那个unique_value确认是自己持有的锁再删if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end关于过期时间的取值也有讲究锁的过期时间必须大于业务最长执行时间否则业务还没跑完锁就失效了另一个线程就能进来重复执行。更稳妥的方案是引入看门狗机制在锁快过期时自动续期但这个方案复杂度会上一个台阶一般业务里把超时时间设置成业务RT的3-5倍就够用了。4. 缓存场景下的过期时间设计4.1 缓存穿透、缓存击穿和缓存雪崩每个用Redis做缓存的团队几乎都绕不开这三个问题。它们都和过期时间强相关缓存穿透是查询一个肯定不存在的数据每次请求都打到数据库。解决方案是把空值也缓存起来并设置一个较短的过期时间比如30-60秒避免空值长期占用内存缓存击穿是某个热点key过期的一瞬间大量请求同时去数据库查询。解决方案是互斥锁或者把热点key设置为逻辑永不过期由后台任务在过期前主动刷新缓存雪崩是大量key在同一时间段过期集体失效导致数据库压力倍增。解决方案就是不要把过期时间设成固定值而是加一个随机扰动。实际项目中这三个问题经常同时出现。比如一个商品详情接口如果商品不存在会穿透如果商品太热门会击穿如果商家批量上架了一批商品导致同时间缓存集体过期又会雪崩。你能做的最简单的一件事就是给每一个key的过期时间加上一个随机值而不是所有key都设同样的秒数。4.2 过期时间的随机扰动怎么做一个稳妥的写法是在基础TTL上叠加一个[jitter, 2*jitter]区间内的随机数long ttl 3600 ThreadLocalRandom.current().nextLong(300, 600); redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);这样即使同一批key的基础过期时间相同实际过期时间点也会被分散开数据库收到的回源压力就被平滑了。我见过一个真实案例一个活动页把几十个配置项的缓存全部设置成10分钟过期结果每到10分钟的整点数据库连接数就飙升单位监控图上出现规则的“锯齿”。后来改成基础600秒加随机120-240秒之后锯齿就消失了。所有做缓存的系统都应该把“过期时间尽量打散”写成一条默认规则。4.3 逻辑过期让过期时间“看起来不存在”如果业务对数据一致性要求很高无法接受缓存到期后秒级回源带来的延迟波动可以考虑逻辑过期。逻辑过期的做法是缓存里存两个值一个是业务数据一个是过期时间戳。读缓存的时候先看时间戳如果没到过期时间直接返回如果到了返回旧数据的同时异步发消息去重建缓存。代码示例一段伪代码性质的Pythondef get_user_info(user_id): data redis.get(fuser:{user_id}) if data: payload json.loads(data) if payload[expire_at] time.time(): return payload[value] # 触发异步刷新由后台任务执行reload_cache(user_id) return load_from_db_and_cache(user_id)逻辑过期看似“绕过”了Redis的天然过期机制但它的好处是读取时永远不阻塞、不击穿。代价是缓存里会短暂存在一个过期值如果你能接受秒级甚至分钟级的最终一致性逻辑过期是很成熟的做法。4.4 缓存与数据库的一致性过期时间不是万能解药先说明一个事实任何缓存系统都无法只靠过期时间保证强一致性。数据库更新了缓存里还留着旧数据在过期时间到达之前读到的就是旧值。常用的手段是旁路缓存策略先更新数据库然后主动删除缓存下次读的时候再把新值回填。如果删除缓存失败还可以用延迟双删也就是更新数据库后先删一次缓存过几百毫秒再删一次避免并发请求期间读到旧值。在这个流程里过期时间是最后的兜底保险而主动删除是主要策略。很多人把过期时间当成缓存一致性工具这其实是个误区。过期时间真正擅长的是垃圾回收和生命周期管理一致性要靠主动失效和消息通知来保证。5. 常见问题与排查实录5.1 设置了过期时间但key一直不消失这是我被问得最多的问题。第一种可能是key经过定期删除和惰性删除都还没被清掉因为定期删除是随机抽样如果过期key量很大某一次周期任务可能没抽到它。但这种情况下只要key后续被访问惰性删除就会立刻清掉它。第二种可能是你用了RESTORE或者从RDB中恢复数据在早期的Redis版本里RDB文件保存了过期时间但恢复时可能出现一些边缘情况不过现在主流版本已经处理得很好了。第三种可能才是重点SET命令覆盖了原来的key但没有重新指定过期时间。SET操作默认会清除key原来的TTL所以你会看到key明明之前设置了10分钟过期执行了一次SET后再查TTL就变成-1。这是语义问题不是Redis bug。解决方案是在覆盖写的时候把过期时间一并带上。第四种可能是过期时间设置的数值太大超过了Redis内部对TTL的表示范围。TTL底层是一个带符号的64位整型以毫秒为单位。虽然理论上支持几十年但如果你在EXPIRE里传了一个非常大的值超过当前时间戳对应的最大远期Redis会把它当作一个绝对Unix时间戳来处理而不是相对秒数。这个用法很绕我建议永远只传相对时间不要传绝对时间戳。5.2 重启之后key还在持久化对过期时间有什么影响Redis重启不丢数据是因为有RDB快照和AOF日志。RDB文件里保存了每个key的剩余过期时间恢复的时候会把这份过期时间重新加载。所以正常情况下没到期的key重启后还在而且还会继续倒计时。到了期的key呢RDB在生成快照的时候不会包含已过期的keyAOF重写时也会把已过期的key过滤掉。理论上持久化不会让已经过期的key重新复活。真正可能“复活”过期key的是AOF文件的加载顺序问题。如果Redis在崩溃前写过一条设置过期时间的AOF指令但这条指令对应的key在日志里之后就有过期的处理不同版本处理方式有差异。一般情况下主流版本5.0以上对这块的处理已经很成熟了如果你的集群还停留在3.x、4.x遇到“重启后读到不该存在的过期数据”优先检查版本。我建议生产环境至少Redis 6.x起步部分已到EOL的版本在过期机制上确实存在一些历史遗留逻辑不值得踩坑。5.3 主从/集群环境下过期时间的同步问题当Redis以主从方式部署时主库负责维护过期key的删除删除后向从库发送DEL命令。从库的过期时间表是独立维护的它会基于自己的逻辑时钟判断key是否过期在读取时返回nil。这个机制在Redis 3.2之后已经完善了。如果你用的是3.2之前的版本从库读过期key可能返回旧值这也是一个需要关注的风险项。在Redis Cluster分片环境下不同节点各自管理自己的过期时间表不存在跨节点的过期协调问题。但如果你在集群模式里使用了多个key的操作比如MGET或Lua脚本遇到某些key带过期时间而某些不带行为会更复杂建议尽量减少跨slot的多key操作。5.4 大key和过期时间管理大key比如一个包含百万字段的Hash在过期时由于删除操作会阻塞主线程Redis 4.0引入了UNLINK命令。但这个命令不是设置过期时间的替代品而是手动删除时用的。对于一个非常大的key即使它设置了过期时间到了过期时刻执行删除时同样可能产生短暂的阻塞因为删除本质上是释放内存。Redis 6.0和7.0对过期大key的删除已经做了不少优化比如7.0增加了异步过期删除的线程最直接的表现是过期key删除时不会再长时间阻塞主线程。不过我在实际运维中还是建议如果是超大key就不要指望靠过期时间清理了应该在业务侧做主动分批删除比如HSCAN配合HDEL逐步拆。5.5 客户端命令超时与过期时间很多搜索词里提到「redis command timed out」这类问题虽然根因不一定是过期时间但有一种常见情况是大量key在同一时刻过期清理线程占用了CPU导致客户端命令排队时间变长。这种情况在高并发系统里真的会出现。排查方法很简单在监控里看Redis的CPU曲线和过期key总量如果规律性出现“整点超时”基本就是过期key集中清理导致。解决方案在前面已经提到了给过期时间加随机扰动降低同一时刻过期key的数量。另外一个辅助手段是调大hz参数让定期删除频率变高但代价是CPU开销变大一般不建议轻易动。5.6 一个实际的排查案例我之前维护过一个订单缓存系统现象是高峰期偶发查询返回null数据库压力飙升。检查后发现业务代码里用SET写缓存时没带EX参数然后另起一个函数去执行EXPIRE设置过期时间。看起来没毛病但实际上SET那一步到EXPIRE之间如果并发线程抢先读取到了这个暂时“永不过期”的key就会在缓存里放一个不带过期时间的脏值后续EXPIRE执行时如果key被覆盖TTL就会被清掉缓存就永久生效了。这个bug的根因就是“设置值”和“设置过期时间”没有做成原子操作。后来把全部改成SETEX/设置命令带过期参数问题消除。这类问题非常隐蔽排查看代码根本看不出逻辑错误只有把Redis操作当作原子单元去看才能发现问题。6. 过期时间调优与取舍参数、策略和业务模型6.1 定期删除相关参数的实际调整建议hz参数默认是10表示每秒执行10次后台任务定期删除过期key、更新各类统计、关闭超时客户端连接等任务都依赖它。如果过期key数量特别大可以提高hz比如到100让清理更及时。但注意这个参数调大的代价是CPU使用率上升而且每次周期任务本身也要消耗时间。如果你用的是Redis 6.0以上版本还可以关注dynamic-hzRedis会根据连接数自动调整hz一般不建议关掉。另一个参数是maxmemory和淘汰策略。在实际运营中我建议每个Redis实例都配置maxmemory宁可触发淘汰也不要把机器内存打满。淘汰策略推荐volatile-lru或allkeys-lru具体选哪个取决于你的key是否都带过期时间。如果业务上只使用带TTL的key优先volatile-ttl它倾向于淘汰剩余时间短的key。注意淘汰策略也是影响过期时间设计的关键点它决定了内存不足时哪些key会先被牺牲。6.2 不同数据类型和场景的过期时间推荐值我整理了一个自身的参考表适合大部分业务起步值场景建议TTL说明短信验证码5分钟太短用户来不及输入太长安全风险大用户登录Token30分钟-2小时配合滑动续期策略商品详情缓存15分钟-1小时适合活动期调整配置类缓存10-30分钟配合版本号或内容hash分布式锁30-60秒按业务RT的3-5倍调整防重复提交标记2-10秒按接口实际耗时设定热点榜单5-15分钟定期刷新的场景这个表不是一个标准答案但它体现了一个原则过期时间要根据业务对时效性的容忍度来定。容忍度越高的业务TTL可以越长回源频率越低对一致性要求高的业务TTL要短甚至不用缓存。6.3 续期和取消过期时间有些场景要求在用户活跃时不断延长过期时间比如“用户7天没登录就踢下线”可以用EXPIRE user:last_active 604800用户每次操作都刷新这个key。这个操作非常轻量代价几乎可以忽略但在实现时要注意调用频率。如果每次请求都刷新高并发下会产生大量无关紧要的写操作可以考虑把刷新频率降低到每分钟一次或只在关键操作时刷新。取消过期时间用PERSIST。还有一个容易遗忘的坑SET操作带上KEEPTTL选项可以保留原key的剩余时间。这个选项在某些场景非常好用比如你想修改key的value但不想影响它的过期计划SET user:1001:name new_name KEEPTTL这个命令在Redis 6.0之后可用。它解决了一个常见问题更新缓存内容时不再需要先读TTL再重新设置避免了两次命令之间的时间差。6.4 过期时间在Redis 7.x的变化和新特性Redis 7.0是我觉得值得升级的版本它引入了Function、ACL改进和过期机制的优化。在过期时间这个领域7.0开始对大key的过期删除做了异步化改进解决了清理过期大key时阻塞主线程的问题。7.4则引入了Hash字段级别的TTL允许Hash内单个字段设置独立过期时间之前的整个大版本里Hash只能整个key过期。如果你正在处理一个管理大量用户扩展信息的Hash不用再为字段单独过期去设计复杂的逻辑过期方案了。这个特性目前需要7.4以上版本才能体验等你在生产环境能升级到7.4或者8.0的时候这个特性会是很实用的解药。在过渡期逻辑过期还是最通用的方案。7. 一些压箱底的经验坦白讲我早期用Redis的时候觉得过期时间就是个“附加参数”写到SET后面就完事了。真正开始做高并发业务后才发现过期时间的设计直接决定了系统的下限。把过期时间打好散、设好兜底、用对原子命令很多线上事故都不会发生。最后分享两个小技巧一是排查过期相关问题时别只盯着代码逻辑先去看Redis节点的过期key总数和删除频率用INFO命令里的expired_keys指标就能快速判断当前实例的清理压力二是在压测或者演练时主动制造“大量key同时过期”的场景观察系统的QPS曲线这样能在上线前就发现雪崩隐患。我几乎每次做缓存治理都会带着团队演一次这个实验远比纸上谈兵的回源策略靠谱得多。