
假设 IMBoy 部署了两个服务节点,A 和 B。
用户把昵称从"小张"改成"老张"。A 更新了数据库,也清掉了自己的旧缓存。但下一次请求落到 B,页面上显示的还是"小张"。
原因很简单:B 还留着修改前的缓存。
前面两篇文章,就是围绕这件事一步步展开的。第一篇介绍为什么选择 depcache,解决本地缓存的存取、过期等问题;第二篇给它加上节点间广播,让一个节点的变更能通知其他节点。
沿着这个思路,还得再问一句:如果 B 当时没收到通知呢?
这就是这次继续改进 imboy_cache 的原因。
发出通知,不等于大家都更新了
两个节点一直连通时,广播很好理解:A 发消息,B 收到后处理。
但如果 A 更新昵称时,A 和 B 之间刚好断网,B 就可能错过这次通知。网络恢复以后,B 仍然可以提供服务,却未必知道自己少收了一条消息。
节点重启也是类似的问题,不过表现不同:本地内存缓存会丢失。它重新加入集群以后,要考虑的是如何恢复需要的缓存,以及恢复期间怎样处理其他节点的新变化。
所以,前篇提到的"自动同步"和"最终一致",需要补上条件。广播提供了通知通道,但漏掉的消息能不能发现、发现后怎么办,还得另做处理。
有些数据,通知对方删掉就够了
拿用户昵称来说,数据库里已经有最新值。B 没必要非得收到一整份用户资料;它只要知道"这份缓存过时了",下次查询时重新读数据库即可。
两种做法可以这样区分:
text
复制缓存:A 把新值发给 B,B 保存新值。
使缓存失效:A 通知 B 丢掉旧值,B 下次需要时重新查询。
它们都用到了节点间通信,但要解决的问题不同。
复制值时,要考虑收到的究竟是新值还是旧值。只通知失效,也仍要考虑通知有没有漏掉。不能因为消息里只带了一个键,就认为同步问题已经消失了。
现在的 imboy_cache 把本地缓存、复制值、通知失效区分开来。哪些数据允许复制,需要显式登记,不能把任意缓存都直接发到其他节点。
对可以从数据库重新查到的数据,我更愿意先问:只让旧缓存失效,够不够?够用的话,就不必多维护一份跨节点副本。
先发现自己漏收了消息
假设 A 发出的更新依次编号为 21、22、23。
B 已经处理到 21,下一条却收到了 23,它就能发现中间少了一条。只有变更内容、没有编号的广播,做不到这一点。
这次实现给同步消息加上了版本信息,也区分节点重启前后的身份。这样,旧进程留下的迟到消息,就不会只凭节点名称相同,被直接当成当前更新。
不过,编号还解决不了一种情况:B 漏掉的恰好是最后一条通知,后面一直没有新消息。
没有下一条消息提醒它跳号,B 可能一直不知道自己落后了。因此还需要定期向其他节点核对进度,而不能完全依赖下一次业务写入。
这里多做的这些事,目的都一样:让节点有机会发现"我现在这份缓存可能不可靠"。
恢复缓存时,最怕把旧值又放回来
发现缺口以后,可以向其他节点获取一份缓存快照,也就是把对方当前保存的数据分批拿过来。
难点在于,传输期间系统还在运行。
例如,B 正在接收某个用户的资料,A 刚好删除了这份缓存。如果 B 随后把先前收到的旧资料写进去,被删掉的数据就又出现了。
同样的问题也会发生在数据库查询上:一个请求开始查询旧资料,查询还没结束,另一个请求已经修改资料并清掉缓存;前一个请求回来后,又把旧结果填进缓存。
这次实现用版本和缓存代次检查这些写入。简单说,就是写回之前再确认:这份结果现在还算数吗? 已经失效的旧结果,不能仅仅因为回来得晚,就重新生效。
恢复也不能无限等待或无限占内存。快照需要分页,条目和字节数要有限制,恢复过程要有期限。条件不满足时,应进入降级状态,不能把"收到了一部分"当作"已经恢复完整"。
本地缓存仍然交给 depcache
这些改动没有替换 depcache。它仍然负责本地缓存,新增的同步模块负责节点间的版本、通知和恢复。
新机制也没有默认套到所有缓存上。只有配置中明确启用的缓存类别才走这套保护,方便逐步接入和验证。
还有一个容易忽略的细节:滚动升级时,新旧节点可能同时运行。新节点发出的失效通知,旧节点也必须能理解。因此兼容期间仍要保留旧格式的失效消息。
这类改动在正常读写时不显眼,却决定了断网、重启、升级时会发生什么。
这次做到哪里
当前代码已经加入版本检查、缺口发现、定期核对、快照恢复,以及阻止旧结果回填的保护。
但完整恢复仍有未验收的部分,尤其是多个来源同时变化时,快照和后续更新能否完整衔接。已有的逐项保护,不能直接推导成"任意故障后都能恢复正确",也不能据此宣称生产验证已经完成。
如果你也在给本地缓存加广播,可以先画出一个最简单的过程:A 更新,B 断网,A 再更新,B 恢复连接。然后检查 B 靠什么发现缺口、接下来读哪里、旧查询结果还能不能写回来。
把这几步说明白,比先加更多同步接口更有用。
实现参考:IMBoy 仓库的 https://gitee.com/imboy-pub/imboy/blob/main/src/lib/imboy_cache.erl、src/lib/imboy_cache_sync*.erl;设计与验证边界见 https://gitee.com/imboy-pub/imboy/blob/main/docs/architecture/imboy-distributed-cache-v2.md。