起一主一从两个Redis实例,禁用主库后台过期扫描,验证从库对一个已过期的key是真的删除了数据,还是只在读取时假装它不存在。
建立一对主从复制关系,让主库禁用后台过期扫描,只能靠被动过期。
操作本机起两个Redis实例:master(19379)和replica(19380) REPLICAOF让replica跟随master 主库: DEBUG SET-ACTIVE-EXPIRE 0 -- 禁用主库的后台清理 主库: SET repltest v1 PX 300 sleep 1秒(远超300ms TTL),完全不访问这个key
用DEBUG OBJECT绕开正常的过期屏蔽逻辑,检查物理存储。
结果主库: DEBUG OBJECT repltest → :Value at:0x80c290d88 refcount:1 encoding:embstr ...(还在!) 从库: DEBUG OBJECT repltest → :Value at:0xc91054518 refcount:1 encoding:embstr ...(从库也还在!)
验证了:主库因为禁用了后台扫描,这个早已过期的key还占用着内存——这一点前面文章验证过。真正新的发现是:从库上这个key也还物理存在,哪怕从库有自己独立的系统时钟,理论上完全可以自己判断"这个key的TTL已经过了"。
从库是怎么在"不删除"和"不返回过期数据"之间做到两全的。
结果从库: GET repltest → 返回空(客户端读不到) 从库: DEBUG OBJECT repltest → :数据依然物理存在(跟上一步矛盾?不,这才是关键)
关键点在这里:从库不会自主删除这个已经"看起来过期"的key——它只是在处理 GET 这类读命令时,临时判断一下"这个key的TTL是不是已经过了",过了就假装它不存在(返回空),但底层数据原封不动地留着,不会真的执行删除。
触发主库的被动过期,观察DEL命令是怎么传播到从库的。
结果主库: GET repltest → 触发主库自己的被动过期,返回空 sleep 0.3秒(等复制流同步过去) 从库: DEBUG OBJECT repltest → :ERR no such key(被物理删除了!)
DEL 命令(走正常的复制流),从库收到这条DEL才会物理删除数据。从库自己永远不会因为"我的系统时钟也觉得这个key该过期了"就主动删除任何数据——这是为了保证主从数据的一致性:如果从库自己独立判断过期,一旦主从时钟有轻微误差,或者主库因为某种原因(比如本文验证过的禁用后台扫描)迟迟没有真正过期某个key,从库却自己先删了,主从数据就会出现不一致。用"只听主库指挥"这个规则,彻底避免了这类问题。
每一步都是本文上面用一主一从两个Redis实例跑出来的原始结果。
DEBUG SET-ACTIVE-EXPIRE / DEBUG OBJECT:绕开正常读取路径检查物理存储