Redis过期key为何还能取出来?惰性删除与定期删除机制解析

发布时间:2026/10/22 19:58:50
Redis过期key为何还能取出来?惰性删除与定期删除机制解析 1. 一个让新手抓狂的经典疑问“我明明给这个 key 设置了 10 秒过期怎么过了半小时去查它还在”这句话我在技术交流群里见过不下几十次。提问的人往往带着一种“我是不是踩到 Redis 的 bug 了”的困惑而围观的人里有人跟着附和“对对对我也遇到过”也有人甩出一句“你肯定是用错命令了”就没了下文。这个问题的答案其实并不复杂但它牵扯出来的东西远比表面看起来要多——过期策略、内存回收、主从同步、持久化恢复每一个环节都可能让你对“过期”这两个字的理解产生偏差。先把结论摆在前面Redis 的 key 过期之后正常情况下你是取不出 value 的返回的一定是 nil。但“正常情况下”这四个字恰恰是很多人踩坑的地方。因为 Redis 的过期机制不是“到点就删”这么简单它采用的是一种惰性删除加定期删除的组合策略。这意味着一个 key 在逻辑上已经过期了但它占用的内存可能还没有被立即释放甚至在特定场景下你依然能“感知”到它的存在。这篇文章适合所有正在用 Redis 做缓存、做分布式锁、做会话管理的开发者。不管你是刚接触 Redis 的新手还是已经用了两三年但没深究过底层机制的老手我都会从实际现象出发把过期 key 的完整生命周期拆开讲清楚。你会看到为什么有时候 key 明明过期了却还占着内存为什么主从架构下过期行为会不一致为什么持久化文件恢复后会出现“幽灵 key”以及最重要的——怎么排查和避免这些问题。2. 过期策略的底层逻辑拆解2.1 为什么 Redis 不采用“到点即删”的方案要理解过期 key 的行为首先得明白 Redis 为什么没有选择“每个 key 到期时立刻删除”这种最直观的方案。假设你有一个 Redis 实例里面存了 100 万个 key每个 key 的过期时间各不相同。如果采用“到点即删”的策略你就需要一个定时器在每个 key 到期的那一刻精确触发删除操作。这意味着 Redis 需要维护 100 万个定时器每个定时器都要占用内存和 CPU 资源。更致命的是当大量 key 在同一时刻过期时删除操作会集中爆发导致 Redis 主线程被阻塞其他请求全部排队等待。对于一个追求低延迟的内存数据库来说这是不可接受的。所以 Redis 选择了一条更务实的路线不保证 key 在过期瞬间被删除但保证过期的 key 不会被返回给客户端。这两句话的区别很关键。前者说的是内存回收的时机后者说的是数据可见性的保证。Redis 把“逻辑过期”和“物理删除”拆成了两件事逻辑过期是即时的物理删除是延迟的、异步的、分批的。这个设计思路和很多系统里的“标记删除”类似。就像图书馆里一本书已经超期了系统里标记它“不可借阅”但书还摆在架子上等管理员有空的时候再去把它撤下来。你作为读者去借这本书系统会告诉你“这本书已超期借不了”而不是让你把书拿走。2.2 惰性删除用到你的时候再检查惰性删除的核心思想非常朴素当一个 key 被访问时顺便检查它是否过期如果过期了就删掉然后返回 nil。这个策略的好处是显而易见的。它把删除操作的成本分摊到了每次访问上不需要额外的定时器也不会出现集中删除导致的卡顿。而且如果一个 key 过期之后再也没人访问那它就一直占着内存——听起来是坏事但从另一个角度看既然没人访问删除它也不会带来任何收益反而浪费了 CPU 去执行删除操作。惰性删除的实现藏在expireIfNeeded这个函数里。每次客户端通过GET、HGET、LRANGE等命令访问一个 key 时Redis 都会先调用这个函数检查 key 是否过期。如果过期了它会做几件事把这个 key 从主字典中删除同时删除对应的过期字典条目然后根据配置决定是否将删除操作同步给从节点或写入 AOF 文件。这里有一个容易被忽略的细节惰性删除只对“被访问到的 key”生效。如果你有 10 万个 key 同时过期但之后没有任何客户端访问它们那这些 key 就会一直留在内存里。这就是为什么有时候你会看到 Redis 的内存使用量居高不下但DBSIZE显示的 key 数量却在减少——因为那些过期的 key 还没被物理删除。注意惰性删除的检查发生在命令执行之前。也就是说如果你对一个已过期的 key 执行INCRRedis 会先删除它然后当作一个不存在的 key 重新创建值为 1。这个行为在实现计数器时需要特别小心。2.3 定期删除每隔一段时间主动清理一批光靠惰性删除显然不够。如果大量过期 key 永远不被访问内存就会被白白占用。所以 Redis 还引入了定期删除机制作为惰性删除的补充。定期删除的逻辑是Redis 默认每秒执行 10 次可以通过hz参数调整每次从过期字典中随机抽取 20 个 key检查它们是否过期删除其中已过期的 key。如果这 20 个 key 中有超过 25% 是过期的那就再抽 20 个继续删直到过期 key 的比例降到 25% 以下或者执行时间超过 25 毫秒为止。这个设计有几个精妙之处。首先随机抽样避免了遍历整个过期字典带来的性能开销。其次25% 的阈值让清理力度可以根据过期 key 的密度动态调整——如果过期 key 很多就多删几轮如果很少就少删几轮。最后25 毫秒的时间上限防止定期删除任务占用过多 CPU 时间影响正常请求的处理。但这也意味着定期删除并不保证所有过期 key 都能被及时清理。在最坏的情况下如果一个 key 过期了但既没有被访问也没有被定期删除抽中它可能会在内存中存活很长时间。这个“很长时间”可能是几分钟也可能是几小时取决于过期 key 的总量和抽样运气。2.4 两种策略的配合与边界惰性删除和定期删除的组合本质上是在内存回收的及时性和CPU 资源的消耗之间做权衡。惰性删除保证了过期 key 不会被返回给客户端定期删除保证了过期 key 最终会被清理掉。两者配合覆盖了绝大多数场景。但这个组合有两个边界情况值得注意。第一个边界是内存压力下的主动清理。当 Redis 的内存使用达到maxmemory限制时它会根据配置的淘汰策略如volatile-lru、allkeys-lru等主动淘汰 key。如果淘汰策略是volatile-*系列Redis 会优先从设置了过期时间的 key 中淘汰如果是allkeys-*系列则从所有 key 中淘汰。这个机制和过期删除是独立的但会影响你对“key 为什么消失了”的判断。第二个边界是从节点上的过期行为。Redis 的主从架构中从节点不会主动删除过期 key。当从节点收到读请求时即使 key 在逻辑上已经过期它也会正常返回 value直到主节点同步过来一个DEL操作。这个设计是为了保证主从数据的一致性——如果从节点自行删除而主节点还没删就会出现主从数据不一致的情况。所以如果你直接连到从节点上读数据可能会读到“已经过期”的 key。3. 那些让你误以为“过期还能取出来”的场景3.1 主从架构下的读写分离陷阱这是最常见的一种情况。很多团队为了分担读压力会配置主从架构让读请求走从节点。这时候如果你在从节点上查询一个已经过期的 key很可能会拿到 value。原因前面提到了从节点不主动删除过期 key它等待主节点同步DEL命令。而主节点的删除时机取决于惰性删除和定期删除的触发情况。如果这个 key 一直没被访问定期删除也没抽中它那主节点就不会发出DEL命令从节点自然也就一直保留着这个 key。我见过一个真实的案例某业务用 Redis 做分布式锁锁的过期时间是 30 秒。主从架构下加锁写主节点查锁读从节点。结果在主节点已经判定锁过期并删除之后从节点上依然能查到锁的存在导致业务逻辑判断失误。这个问题的根源不在于 Redis 的过期机制有 bug而在于读写分离场景下从节点的数据延迟被过期机制放大了。解决办法有两个一是分布式锁的读写都走主节点不要走从节点二是如果必须读从节点要在应用层做额外的过期时间判断不能完全依赖 Redis 的过期机制。3.2 持久化恢复后的“幽灵 key”RDB 和 AOF 是 Redis 的两种持久化方式它们在处理过期 key 时的行为有所不同。对于 RDB 文件Redis 在生成快照时会对 key 做一次过滤已经过期的 key 不会被写入 RDB 文件。所以从 RDB 恢复时理论上不会出现已过期的 key。但这里有一个例外如果主节点在生成 RDB 期间某个 key 还没有过期但 RDB 生成完成后、从节点加载 RDB 之前这个 key 过期了那从节点加载后就会得到一个“已经过期”的 key。不过从节点不会主动删除它所以它会一直存在直到主节点同步DEL命令。对于 AOF 文件情况更复杂一些。AOF 记录的是写命令当一个 key 过期被删除时Redis 会向 AOF 中追加一条DEL命令。但如果 Redis 在删除这个 key 之前就宕机了AOF 中就没有这条DEL命令。重启后Redis 加载 AOF 文件这个 key 会被重新创建而且它的过期时间戳是过去的某个时间点。这时候如果你去查这个 key惰性删除会发现它已经过期然后删除它并返回 nil。所以从 AOF 恢复后你可能会在极短的时间内“看到”这个 key但一旦访问就会被删除。提示如果你在 Redis 重启后立刻用KEYS *扫描可能会看到一些已经过期的 key。这不是 bug而是持久化恢复和过期机制配合的正常表现。等定期删除跑几轮之后它们就会消失。3.3 客户端缓存与连接池的干扰还有一种情况问题不在 Redis 本身而在客户端。很多 Redis 客户端如 Jedis、Lettuce都支持连接池和客户端缓存。如果你开启了客户端缓存或者用了某些带有本地缓存功能的中间件那你在应用层拿到的可能是缓存中的旧数据而不是 Redis 的真实状态。这时候即使 Redis 中的 key 已经过期你的应用依然能“取出来” value。另外连接池的复用也可能导致问题。如果上一个请求在连接上设置了某种状态比如SELECT切换了数据库或者MULTI开启了事务但没有正确关闭下一个请求复用这个连接时可能会读到意料之外的数据。虽然这跟过期机制没有直接关系但它会造成类似的“key 明明过期了却还能取出来”的困惑。排查这类问题时最直接的办法是绕过客户端用redis-cli直接连到 Redis 实例上查询。如果redis-cli返回 nil而你的应用能拿到 value那问题一定在客户端侧。3.4 时间戳精度与时钟回拨Redis 的过期时间是基于 Unix 时间戳的毫秒值。当一个 key 被设置过期时间时Redis 会把“当前时间 过期秒数”计算成一个绝对的时间戳存储在过期字典中。之后每次检查过期都是拿当前时间戳和这个存储的时间戳做比较。这里有一个潜在的问题如果服务器时钟发生了回拨当前时间戳变小了那原本已经过期的 key 可能会重新变成“未过期”状态。虽然这种情况在实际生产环境中比较少见但在虚拟机迁移、NTP 同步异常等场景下是有可能发生的。Redis 本身对时钟回拨没有特别的防护机制。它假设服务器的时钟是单调递增的。所以如果你的环境里存在时钟回拨的风险建议在应用层做额外的过期判断或者使用 Redis 的TIME命令来获取服务器时间而不是依赖本地时间。4. 过期 key 的完整生命周期与实操验证4.1 一个 key 从设置到消失的全过程为了把上面的理论串起来我们用一个具体的例子来跟踪一个 key 的完整生命周期。假设我们在一个主从架构的 Redis 集群中执行以下操作# 在主节点上设置一个 10 秒后过期的 key SET session:abc123 user_data EX 10从这一刻开始这个 key 会经历以下几个阶段阶段一正常存活期0 到 10 秒。key 存在于主节点和从节点的字典中过期字典中记录了它的过期时间戳。任何客户端查询都能拿到user_data。阶段二逻辑过期但未被删除10 秒之后删除操作触发之前。从时间戳上看这个 key 已经过期了。但主节点还没有执行删除操作可能是因为没有客户端访问它惰性删除未触发定期删除也还没抽中它。此时在主节点上查询惰性删除触发key 被删除返回 nil。在从节点上查询从节点不主动删除返回user_data。内存中key 依然占用着内存。阶段三主节点删除并同步某个时刻。当主节点通过惰性删除或定期删除清理了这个 key 之后它会向从节点发送DEL命令或者UNLINK命令取决于配置。从节点收到后删除本地 key。阶段四完全消失。主从节点上都不再存在这个 key内存被释放。这个过程中最容易被误解的就是阶段二。很多人以为“过期”是一个瞬间发生的事情但实际上它是一个过程而且主从之间的进度还不一致。4.2 用命令行验证过期行为理论说再多不如自己动手验证一遍。下面是我常用的几个验证步骤你可以直接在测试环境里复现。验证惰性删除# 设置一个 5 秒过期的 key SET test:lazy hello EX 5 # 立即查询能拿到值 GET test:lazy # 输出: hello # 等待 6 秒 # 再次查询返回 nil GET test:lazy # 输出: (nil) # 确认 key 已被删除 EXISTS test:lazy # 输出: (integer) 0这个验证很简单但要注意如果你在等待期间用DEBUG SLEEP阻塞了 Redis或者用KEYS *扫描过可能会提前触发删除。所以最好在一个干净的实例上做这个测试。验证定期删除# 批量设置 1000 个 1 秒过期的 key for i in $(seq 1 1000); do redis-cli SET test:batch:$i value EX 1 done # 等待 2 秒 sleep 2 # 查看剩余 key 数量 DBSIZE # 输出可能不是 0而是几十到几百不等 # 再等几秒再次查看 sleep 5 DBSIZE # 输出会进一步减少但可能仍不为 0这个实验说明定期删除不是一次性的而是分批进行的。DBSIZE返回的数量会逐渐趋近于 0但不会瞬间归零。验证主从延迟这个实验需要主从环境。在主节点上设置一个短过期 key然后在从节点上查询# 在主节点执行 SET test:replica data EX 3 # 立即在从节点查询 GET test:replica # 输出: data # 等待 5 秒后在从节点查询 GET test:replica # 可能输出: data如果主节点还没同步 DEL # 也可能输出: (nil)如果主节点已经同步了 DEL这个实验的结果取决于主节点的删除时机所以每次可能不一样。但如果你反复做几次一定能观察到从节点返回已过期 key 的情况。4.3 关键参数配置与调优建议Redis 提供了一些和过期机制相关的配置参数理解它们的作用可以帮助你更好地控制过期行为。参数默认值作用调优建议hz10定期删除的执行频率每秒次数过期 key 多时可适当调大但会增加 CPU 开销maxmemory0不限制内存上限建议设置为物理内存的 70%-80%maxmemory-policynoeviction内存淘汰策略缓存场景建议用 allkeys-lru 或 volatile-lrulazyfree-lazy-expireno是否异步删除过期 key大 key 较多的场景建议设为 yesreplica-read-onlyyes从节点是否只读保持默认即可其中lazyfree-lazy-expire这个参数值得特别说明。当它设为no时删除过期 key 是同步操作如果 key 对应的 value 很大比如一个包含百万元素的 Hash删除操作会阻塞 Redis 主线程。设为yes后Redis 会把删除操作放到后台线程执行避免阻塞。这个参数在 Redis 4.0 之后才支持如果你的版本较老需要升级。注意hz的值不建议超过 100。虽然调大可以让定期删除更频繁但每次定期删除都会占用 CPU 时间。如果hz设得过高反而会影响正常请求的处理。一般来说hz设为 10 到 20 之间就够用了。5. 常见问题排查与避坑指南5.1 问题速查表下面这张表整理了我在实际工作中遇到过的与过期 key 相关的问题以及对应的排查思路和解决方法。现象可能原因排查方法解决方案从节点读到已过期的 key主从同步延迟对比主从节点的 TTL读写都走主节点或应用层判断过期内存占用高但 key 数量少过期 key 未被及时删除用INFO memory查看内存用DBSIZE查看 key 数调大hz或手动触发清理重启后出现已过期的 keyAOF/RDB 恢复检查持久化文件和过期时间等待定期删除清理或手动删除删除大 key 导致卡顿同步删除阻塞主线程用SLOWLOG查看慢查询开启lazyfree-lazy-expire时钟回拨导致 key 不过期服务器时间异常检查 NTP 同步状态修复时钟应用层加过期判断客户端缓存导致读到旧数据客户端缓存未失效绕过客户端直接用 redis-cli 查询关闭客户端缓存或缩短缓存时间5.2 几个容易踩的坑坑一用KEYS *排查过期问题。KEYS *会遍历所有 key包括那些已经过期但未被删除的 key。这不仅会返回误导性的结果还会因为遍历操作触发惰性删除改变 Redis 的状态。排查过期问题时应该用SCAN配合TTL命令或者直接用redis-cli --scan。坑二在从节点上执行写操作。虽然从节点默认是只读的但如果你把replica-read-only设为了no就可以在从节点上写入。这时候设置的过期 key 不会同步到主节点会造成数据不一致。除非有特殊需求否则不要修改这个配置。坑三忽略UNLINK和DEL的区别。DEL是同步删除UNLINK是异步删除。在 Redis 4.0 之后过期 key 的删除默认走UNLINK路径如果开启了lazyfree-lazy-expire。如果你在监控中看到DEL命令的调用量突然增加可能是因为某些客户端在手动删除 key而不是 Redis 的过期机制在起作用。坑四把TTL返回 -1 当成永不过期。TTL返回 -1 表示 key 存在但没有设置过期时间返回 -2 表示 key 不存在。但如果你在一个已经过期的 key 上执行TTL惰性删除会先删除它然后返回 -2。所以看到 -2 不一定代表 key 从来没存在过也可能是它刚刚过期被删了。5.3 监控与告警建议对于生产环境建议对以下几个指标做监控过期 key 数量通过INFO stats中的expired_keys指标可以了解单位时间内过期 key 的删除数量。如果这个值持续很高说明过期 key 很多可能需要调大hz。内存使用率通过INFO memory中的used_memory和used_memory_rss可以了解内存的实际占用情况。如果内存使用率持续高于 80%需要考虑扩容或调整淘汰策略。主从延迟通过INFO replication中的master_repl_offset和slave_repl_offset可以计算主从之间的同步延迟。如果延迟过大从节点上读到过期 key 的概率会增加。慢查询通过SLOWLOG GET可以查看执行时间较长的命令。如果发现大量删除操作出现在慢查询日志中说明可能存在大 key 的同步删除问题。这些指标可以通过 Redis 自带的INFO命令获取也可以接入 Prometheus 等监控系统做可视化展示。关键是要设置合理的告警阈值比如内存使用率超过 85% 时触发告警主从延迟超过 1 秒时触发告警。6. 几个值得记住的实操心得关于 Redis 过期 key 这个话题我在实际项目中积累了一些经验有些是踩坑之后才明白的有些是看源码时发现的细节。这里挑几个最有价值的分享出来。第一不要依赖 Redis 的过期机制做精确的时间控制。Redis 的过期是“最终一致”的不是“强一致”的。如果你需要精确到秒级的过期控制应该在应用层做判断把 Redis 的过期时间设得比实际需要的时间长一些然后在应用层用时间戳做二次校验。比如分布式锁的场景锁的过期时间设为 30 秒但应用层在获取锁之后记录一个获取时间每次操作前检查是否超过 25 秒超过就主动释放并重新获取。第二大 key 的过期删除一定要用异步方式。我见过一个案例某业务用 Redis 存了一个包含几十万元素的 Hash过期时间设为 1 小时。每次这个 key 过期时Redis 主线程都会卡顿几百毫秒导致大量请求超时。后来开启了lazyfree-lazy-expire问题才解决。如果你不确定自己的 key 是不是大 key可以用MEMORY USAGE key命令查看单个 key 的内存占用超过 10KB 的 key 就值得关注了。第三主从架构下过期 key 的读取问题要从架构层面解决。不要试图通过调参数来消除主从延迟因为延迟是分布式系统的固有属性。正确的做法是对一致性要求高的读请求走主节点对一致性要求不高的读请求走从节点并在应用层做好兜底判断。如果业务无法接受从节点读到过期数据那就干脆不要用读写分离所有请求都走主节点。第四定期删除的hz参数不要盲目调大。很多人遇到过期 key 清理不及时的问题第一反应就是把hz调到 100 甚至更高。但这样做会增加 CPU 开销而且效果不一定好。因为定期删除的瓶颈不在于执行频率而在于每次抽样的数量和阈值。如果过期 key 的密度很低调大hz也只是让 Redis 更频繁地做无用功。更好的做法是结合业务特点合理设置 key 的过期时间避免大量 key 在同一时间集中过期。第五用SCAN代替KEYS做 key 扫描。这个建议不仅适用于过期 key 的排查也适用于所有需要遍历 key 的场景。SCAN采用游标方式分批返回不会阻塞 Redis 主线程也不会一次性返回大量数据。虽然SCAN不保证返回所有 key在扫描过程中如果有 key 被删除或新增可能会漏掉或重复但对于排查问题来说已经足够了。第六关注 Redis 版本对过期机制的改进。Redis 6.0 之后过期机制做了一些优化比如引入了expireIfNeeded的异步版本减少了删除操作对主线程的阻塞。Redis 7.0 进一步优化了定期删除的抽样算法提高了清理效率。如果你的项目还在用 Redis 3.x 或 4.x建议评估升级到 6.x 或 7.x可以获得更好的过期处理性能。最后再分享一个小技巧如果你怀疑某个 key 的过期行为异常可以用DEBUG OBJECT key命令查看它的内部编码和过期时间戳。这个命令会返回 key 的引用计数、编码方式、过期时间等信息对于排查过期问题很有帮助。不过要注意DEBUG命令在生产环境默认是禁用的需要在配置文件中开启enable-debug-command才能使用。在测试环境用完之后记得把它关掉。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询