【Redis从入门到精通】第27篇:过期键的骨牌效应——AOF/RDB/复制中的过期处理 上一篇【第26篇】Redis过期键机制——TTL的生死时钟是怎么走的下一篇【第28篇】数据库通知——Redis的事件订阅机制一个键过期了你以为它只是从内存中消失天真。它在RDB里留下了痕迹在AOF里留下了遗言在主从复制中还可能留下一个鬼影。引言过期不是终点上一篇我们了解了Redis过期键的基本机制——惰性删除加定期删除。但这只是内存中的故事。一旦涉及到持久化和主从复制过期键的行为就变得复杂得多。一个过期键在RDB文件中是否存在AOF日志里写了什么从库会不会读到过期数据这些问题看似独立实则环环相扣——一个过期键的骨牌效应可以影响到整个Redis系统的数据一致性。全景图过期键在Redis各层面的行为在深入每个子系统之前先看一张全景流程图了解过期键在Redis各层面的完整行为┌──────────────┐ │ 过期键被删除 │ │ (惰性/定期) │ └──────┬───────┘ │ ┌───────────────────┼───────────────────┐ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ RDB持久化 │ │ AOF持久化 │ │ 主从复制 │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ ▼ ▼ ▼ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ BGSAVE时 │ │ 删除后追加 │ │ 主库删除后 │ │ 跳过已过期键 │ │ DEL命令到AOF │ │ 传播DEL给从库 │ │ │ │ │ │ │ │ 载入时 │ │ AOF重写时 │ │ 从库不主动删 │ │ 主库过滤 │ │ 跳过已过期键 │ │ 等主库DEL命令 │ │ 从库不过滤 │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────┘这张图概括了过期键在三大子系统中的行为。接下来我们逐一拆解。RDB持久化中的过期键处理RDB是Redis的快照持久化方案。它将某一时刻的内存数据完整地保存到磁盘。过期键在RDB的保存和载入两个阶段有不同的处理逻辑。BGSAVE/SAVE生成RDB时跳过过期键当执行BGSAVE或SAVE命令生成RDB文件时Redis会遍历所有数据库的键空间。对于每个键如果它在过期字典中存在且已经过期RDB文件中不会包含这个键。生成RDB文件的遍历过程 │ ▼ ┌──────────────────────────┐ │ 遍历键空间中的每个键 │ │ │ │ key: user:1 │ │ ├─ 检查过期字典 │ │ ├─ 未过期 → 写入RDB │ │ │ │ │ key: session:abc │ │ ├─ 检查过期字典 │ │ ├─ 已过期 → 跳过 │ │ │ │ │ key: config:main │ │ ├─ 不在过期字典中 │ │ └─ 写入RDB │ └──────────────────────────┘这样做的好处是RDB文件不会包含无用的过期数据恢复时也不需要再处理过期键。载入RDB时主从有别RDB文件的载入逻辑取决于Redis实例的角色角色过期键处理方式原因主服务器过滤过期键主服务器是权威数据源过期就该删除从服务器不过滤全部载入等待主服务器的DEL命令来同步删除为什么从服务器要照单全收因为从服务器的数据必须和主服务器保持一致。如果在RDB载入时自行删除过期键可能导致主从数据不一致比如一个键在主服务器上还没过期但从服务器提前删了。┌─────────────────────────────────────────────┐ │ RDB载入流程 │ │ │ │ 主服务器载入RDB │ │ ┌────────────────────┐ │ │ │ 遍历RDB中的每个键 │ │ │ │ 检查是否已过期 │ │ │ │ 过期 → 丢弃 │ │ │ │ 未过期 → 载入内存 │ │ │ └────────────────────┘ │ │ │ │ 从服务器载入RDB │ │ ┌────────────────────┐ │ │ │ 遍历RDB中的每个键 │ │ │ │ 全部载入不做过滤 │ │ │ │ 等主服务器同步DEL │ │ │ └────────────────────┘ │ └─────────────────────────────────────────────┘踩坑提示如果主服务器长时间没有向从服务器发送DEL命令比如网络抖动从服务器上可能存在大量已过期但未被删除的键。这就是从库脏读问题的根源。AOF持久化中的过期键处理AOFAppend Only File记录的是所有写操作命令。过期键在AOF中的处理分为三种场景。场景一AOF写入时当一个客户端读取一个已过期的键时触发惰性删除或者定期删除清理了过期键时Redis会在AOF缓冲区追加一条DEL命令# 假设 key session:abc 已过期# 客户端执行 GET session:abc# Redis发现键已过期执行惰性删除# AOF中追加DEL session:abc重要AOF文件不会记录对过期键的读操作。如果你GET了一个已过期的键AOF中只会记录DEL不会记录这次GET因为GET本身不是写操作。场景二AOF重写时AOF重写是为了压缩AOF文件体积。重写时Redis会遍历当前数据库中的所有键只记录未过期的键AOF重写过程 │ ▼ ┌──────────────────────────┐ │ 遍历键空间中的每个键 │ │ │ │ key: user:1 │ │ ├─ 未过期 → 生成SET命令 │ │ │ │ │ key: cache:xyz │ │ ├─ 已过期 → 跳过 │ │ │ │ │ key: order:100 │ │ ├─ 未过期有TTL │ │ ├─ 生成SETEX命令 │ │ └─ 或 SET PEXPIREAT │ └──────────────────────────┘AOF重写生成的命令保证了重放后的数据与当前内存状态一致——已过期的键自然不会出现在重写后的AOF文件中。场景三AOF重放时当Redis从AOF文件恢复数据时如果遇到带有过期时间的键会在载入时设置对应的过期时间。如果AOF重放过程中某个键的过期时间已经到达Redis会在载入完成后通过正常的过期删除策略来处理。三种AOF场景的过期键处理对比AOF场景过期键处理说明AOF写入追加DEL命令键被删除时追加不记录读操作AOF重写跳过过期键重写后的文件不含过期数据AOF重放正常载入后过期策略生效载入后由惰性/定期删除处理主从复制中的过期键处理主从复制是Redis高可用的基石但过期键在主从架构中的行为常常让人困惑。核心规则从库不主动删除从服务器不主动删除过期键哪怕这个键已经过期很久了。只有收到主服务器发来的DEL命令从服务器才会删除这个键。┌───────────┐ DEL key ┌───────────┐ │ 主服务器 │ ─────────────►│ 从服务器 │ │ │ │ │ │ key过期了 │ │ key还在 │ │ 惰性/定期删除│ │ 等主库指令 │ │ 发送DEL给从 │ │ 收到DEL才删 │ └───────────┘ └───────────┘这个设计的原因是主从复制必须保证数据一致性。如果从服务器自行删除过期键可能出现以下问题主服务器上键还没过期从服务器已经删了 → 从服务器缺少数据主服务器的DEL命令到达时从服务器发现键已经不在了 → 可能导致一致性问题如果网络短暂中断从服务器自行删除的键可能与主服务器不同步从库脏读问题这种设计带来的最大问题是从库脏读——客户端从从库读到了过期数据# 主服务器上 key session:abc 在 12:00:00 过期# 12:00:01 客户端从从库读取从库GET session:abcold_value# 过期了1秒但从库还返回了# 直到主库的DEL命令传播过来# 从库才会删除这个键Redis 3.2的改进从Redis 3.2版本开始从服务器在读取键时会检查过期时间。如果键已过期从服务器不会执行删除操作但会返回空值给客户端。这样既保证了从库不主动删除过期键避免一致性问题又避免了返回过期数据。Redis 3.2 之前 vs 之后 │ Redis 3.2 │ Redis 3.2 ──────────────┼─────────────────────┼────────────────────── 从库GET过期键 │ 返回过期数据脏读 │ 返回空值不脏读 从库是否删除 │ 不删除 │ 不删除 DEL命令传播 │ 主库发送后从库才删 │ 主库发送后从库才删踩坑提示即使Redis 3.2解决了读脏数据的问题从库上的过期键仍然占据内存。如果主库的DEL命令延迟从库的内存使用可能高于主库。实际案例从库读到过期数据的排查假设你的应用从从库读到了过期数据排查步骤如下确认Redis版本如果低于3.2升级是根本解决方案检查主从延迟使用INFO replication查看master_link_status和复制偏移量差异检查网络主从之间的网络延迟可能导致DEL命令传播慢临时方案对敏感数据可以在从库手动执行DEL命令不影响主从复制一致性# 步骤1检查主从状态127.0.0.1:6379INFO replication# Replicationrole:slave master_host:10.0.0.1 master_port:6379 master_link_status:up# 连接正常master_sync_left_bytes:0# 同步完成# 步骤2检查特定键127.0.0.1:6379TTL suspicious_key(integer)-1# 从库认为永不过期# 步骤3在主库检查主库TTL suspicious_key(integer)-2# 主库已删除# 结论DEL命令还没传播过来Redis Cluster中的过期键处理Redis Cluster中每个主节点负责管理一部分哈希槽hash slot。过期键的处理逻辑在每个主节点上独立执行每个主节点独立维护自己的过期字典惰性删除和定期删除在各节点上独立运行过期后产生的DEL命令只传播给该主节点的从节点┌─────────────────────────────────────────┐ │ Redis Cluster │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Node A │ │ Node B │ │ Node C │ │ │ │ 槽0-5460│ │5461-10922│ │10923-16383│ │ │ │ │ │ │ │ │ │ │ │ 独立过期 │ │ 独立过期 │ │ 独立过期 │ │ │ │ 管理 │ │ 管理 │ │ 管理 │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ │ │ │ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │ │ │从节点A │ │从节点B │ │从节点C │ │ │ │等主库DEL│ │等主库DEL│ │等主库DEL│ │ │ └─────────┘ └─────────┘ └─────────┘ │ └─────────────────────────────────────────┘需要注意在Cluster模式下如果客户端请求的键所在的槽发生了迁移过期键的处理可能在迁移过程中产生竞争。不过Redis Cluster通过迁移协议保证了键的过期处理只由当前槽的拥有者负责。Lua脚本中的过期键Lua脚本在Redis中以原子方式执行这意味着脚本执行期间不会被中断包括过期删除。即使脚本执行过程中某个键过期了定期删除也不会在脚本执行期间清理它。惰性删除仍然生效如果脚本内部访问了一个已过期的键惰性删除会正常触发。-- Lua脚本中访问过期键localvalredis.call(GET,may_expire_key)-- 如果 may_expire_key 已过期这里val会是false-- 惰性删除在脚本内部也会触发-- 但是脚本执行期间定期删除不会运行-- 所以大量过期键不会在脚本执行期间被清理踩坑提示如果你的Lua脚本执行时间很长比如超过100毫秒期间积累的过期键无法被定期删除清理可能导致内存短暂升高。所以Lua脚本应该尽量简短。过期键计数监控监控过期键的状况是运维的重要一环。INFO keyspace命令可以查看每个数据库的过期键统计127.0.0.1:6379INFO keyspace# Keyspacedb0:keys100000,expires80000,avg_ttl3456789db1:keys5000,expires1000,avg_ttl987654各字段含义字段含义keys当前数据库的键总数expires设置了过期时间的键数量avg_ttl当前设置了过期时间的键的平均剩余TTL毫秒关注指标expires/keys比值如果很低比如10%说明大部分键没有TTL有内存泄漏风险avg_ttl趋势如果持续下降说明大量键即将过期需要警惕批量过期已过期但未删除的键数量可以通过expires - (实际存在的带TTL键数)来估算更精细的监控可以使用SCAN命令配合TTL检查# 粗略统计即将过期TTL 60秒的键数量redis-cli SCAN0COUNT1000|\xargs-I{}redis-cli TTL{}|\grep-c^[0-9]$也可以利用Prometheus Grafana配合Redis Exporter来可视化监控过期键指标。过期键在三大子系统中的行为总结场景RDBAOF主从复制生成/写入时跳过已过期键追加DEL命令主库删除后传播DEL载入/重放时主库过滤从库不过滤按命令重放从库不主动删除重写时不适用跳过已过期键不适用数据一致性RDB中无过期数据AOF中有DEL记录依赖主库DEL传播风险点载入时从库不过滤基本无风险从库脏读/内存占用总结过期键在Redis各子系统中的行为就像骨牌一样环环相扣RDB生成时跳过过期键载入时主库过滤、从库不过滤AOF删除时追加DEL重写时跳过过期键——双保险保证数据正确主从复制从库不主动删除等主库DEL命令。Redis 3.2解决了脏读但不解决内存占用Cluster各节点独立管理过期互不干扰Lua脚本执行期间定期删除暂停惰性删除照常理解这些行为差异是排查主从数据不一致、从库内存异常等问题的关键。下一篇我们将讨论与过期键密切相关的功能——键空间通知看看Redis是如何让客户端感知到键的生死的。上一篇【第26篇】Redis过期键机制——TTL的生死时钟是怎么走的下一篇【第28篇】数据库通知——Redis的事件订阅机制