写点什么

关于 Redis 主从节点数据过期一致性的思考,它真的足够一致了吗?

用户头像
极客good
关注
发布于: 刚刚

如果抽样中过期键的比例较高,则继续执行抽查删除,这样可以实现过期键越多,删的速度越快的效果。


当然,在过期键比例非常高时,也不能无止境的陷在抽样删除的循环里,Redis 还设置了一次执行的最大时长限制,避免一直阻塞主线程,影响服务


以下是定期抽样删除功能的终止条件部分代码:(仅更改了注释,下文同)


// src/expire.c/activeExpireCycle()do {//执行抽样和删除(略)


// 过期键太多时不能永远阻塞下去,需要在指定时间内结束 if ((iteration & 0xf) == 0) { /* check once every 16 iterations. /elapsed = ustime()-start; //当前时间 - 开始时间 = 已用时长 if (elapsed > timelimit) { //达到最大时长限制,break 退出 do...while 循环 timelimit_exit = 1;server.stat_expired_time_cap_reached_count++;break;}}//如果抽样中过期键的比例低于 config_cycle_acceptable_stale ,结束循环//若过期键比例较高,则继续删} while (sampled == 0 ||(expired100/sampled) > config_cycle_acceptable_stale);


惰性删除


仅凭定期删除会存在删除不及时的问题,但客户端访问可不会等过期键删除完。


所以当访问任意 key 时,都会先判断它是否处于已经过期但还没来得及删除的状态,如果已经过期则立刻删除,并向上层返回该 key 不存在。


主从之间的过期


说完了单机情况下键过期的实现方式,再回到最开始的问题:从节点可能读到过期数据。


书里写的原因是:


从节点不会主动删除访问到的过期数据,而是要等主节点数据过期后生成 DEL 命令发过来。由于定期删除机制不够及时,在到达过期时间点与实际收到 DEL 命令这段时间内,读取从节点将会获取到本应过期的数据,而不执行惰性删除。


这怎么看都是一个 bug 吧? 网上搜了一圈,都说在 Redis 3.2 版本修复了该问题,但没有找到具体用什么方式修的。


在官方文档的 Replication 一节中,有部分关于主从之间过期键处理的说明。


大意如下:


实现复制功能不能依靠主从节点的墙上时钟,Redis 使用了以下三个方式来处理:


副本不会主动删除过期键,而是等主节点过期时生成 DEL 命令同步至从节点进行删除。


但主节点过期删除不及时,可能会使从


【一线大厂Java面试题解析+核心总结学习笔记+最新架构讲解视频+实战项目源码讲义】
浏览器打开:qq.cn.hn/FTf 免费领取
复制代码


节点上存在逻辑上已经过期的键。为了处理该问题,副本采用它自己的 逻辑时钟 来判断读取时键是否应当过期,过期则返回不存在(即使数据仍然在内存中,等着主节点的 DEL 命令)


In order to deal with that the replica uses its logical clock in order to report that a key does not exist only for read operations that don't violate the consistency of the data set (as new commands from the master will arrive).


在 Lua 脚本运行时,服务器中的时间是 冻结 的,防止键在脚本运行的过程中过期。这是为了保持副本上执行的脚本能具有相同的效果。(注:不同机器,性能不一样,脚本执行时长也不同)


文档中只给出了一个模糊的 “根据 逻辑时钟 判断” 的描述,没有具体说明实现方式。我只能先来猜一下实现方式,然后去代码里试试能不能找到。


我猜的实现方式


先补充一下背景知识:分布式系统中,网络和本地系统时钟都是 不可靠 的。


既然文档中描述了逻辑时钟,那么我猜测的方式如下:


开始执行 slaveof 的时候,主从节点校对时钟,每个从节点维护一个与主节点的时间差,实现逻辑时钟。


主节点上执行的与过期相关的命令 set k v ex time、expire、expireat、pexpire、pexpireat 等在传播到从节点时通通根据主节点的时钟转成 pexpireat,使用基于主节点的绝对时间。


从节点接到命令时,对这些过期命令加减时间差,实现主从一致的过期。(也就是说一切时间以主节点为准)


实际的实现方式


然而事实证明我完全想多了,Redis 代码中关于过期判断的部分完全没有计算时间差的影子。


这里是其中一处过期判断的代码:


// src/db.cint keyIsExpired(redisDb *db, robj *key) {mstime_t when = getExpire(db,key); //从过期字典获取过期时间,没有返回-1mstime_t now;


if (when < 0) return 0;// when=-1 永不过期 if (server.loading) return 0;


if (server.lua_caller) { //lua 脚本中的命令总是使用脚本开始时间 now = server.lua_time_start;} else if (server.fixed_time_expire > 0) { //如 RPOPLPUSH 这种操作多个 key 的命令也使用命令开始时间,fixed_time_expire 在调用 call()时刷新 now = server.mstime;} else {now = mstime();}return now > when;}


这里没找到解决方式,没办法只能直接去找 3.2 版本那次修复的提交记录,看看他是怎么改的。


于是我从 issues 里找到了 Improve expire consistency on slaves 这个关于提高主从过期时间一致性的问题,并在其中发现了对应的 提交记录。


结果令人大跌眼镜,总共只有寥寥几行,在过期判断后增加了从节点和只读命令的判断条件,然后返回 null。


这下更令人疑惑了,这么说来,主从节点的过期没做区分,都用了本地系统的墙上时钟吗?


只能再换个方向去找了,Redis 还有什么地方可能让主从之间的命令产生区别?没错,在主节点到从节点的命令传播那里说不定能有新发现!


// src/server.c// 命令传播 void propagate(struct redisCommand *cmd, int dbid, robj **argv, int argc,int flags){if (server.aof_state != AOF_OFF && flags & PROPAGATE_AOF)feedAppendOnlyFile(cmd,dbid,argv,argc); // 传给 AOFif (flags & PROPAGATE_REPL)replicationFeedSlaves(server.slaves,dbid,argv,argc); //传给从节点}


先看了下传播到 AOF 文件的函数 feedAppendOnlyFile(),在内部它通过 catAppendOnlyExpireAtCommand 将 expire、expireat 等过期相关命令全转成了 PEXPIREAT,看起来有门哈。


再看向从节点传播命令的 replicationFeedSlaves(),很遗憾找了一圈下来没发现有对过期命令的特殊处理。


没做特殊处理也就意味着 expire、expireat 它们在主从节点上都是按原样执行的。但是,由于判断过期使用了各自的本地系统时钟,会不会导致主从节点之间产生不一致的行为?


使用本地时钟导致的问题


下面分析当主从节点所在机器的系统时间不一致时,将导致什么样的问题。

用户头像

极客good

关注

还未添加个人签名 2021.03.18 加入

还未添加个人简介

评论

发布
暂无评论
关于Redis主从节点数据过期一致性的思考,它真的足够一致了吗?