缓存和数据库为什么会不一致

为了扛高并发,我们把热点数据放进缓存、真实数据存数据库。但读写是两条路径:读先查缓存、没有再查库回填;写先改库、再删或改缓存。这两步不是原子的,在并发、重试、宕机下就会出现缓存还是旧值、数据库已是新值的不一致。彻底强一致成本极高,工程上追求的是可接受时间内的最终一致。

本文核心知识点
  • Cache Aside由业务代码显式管理缓存的读写。
  • 延迟双删在写库后隔一会再删一次缓存。
  • binlog 订阅用 Canal 等把库变更同步到缓存。
  • 最终一致允许短暂不一致,业务可接受。
缓存一致性配图:蓝色渐变背景左侧数据库圆柱与右侧缓存面板之间两组双向发光箭头,中间发光同步齿轮方块,扁平科技插画
数据库与缓存之间由同步齿轮维持双向一致,正是缓存一致性的意象。

Cache Aside:最常用的读写模式

读:先查缓存,命中就返回;没命中再查数据库,把结果写回缓存再返回。写:先更新数据库,再删除缓存。为什么是删而不是改?因为改缓存要处理并发写的覆盖顺序,而删掉后下次读会自动回填新值,更简单也更安全。

// 读
val = cache.get(key);
if (val == null) { val = db.query(key); cache.set(key, val); }
// 写
db.update(key, newVal);
cache.del(key);

更新数据库还是先删缓存

业界共识是先更新数据库、再删缓存。反过来先删缓存再更新库,会有一个危险窗口:删完缓存、还没来得及改库,另一个读请求把旧值又回填进缓存,导致长期不一致。先改库再删缓存,即使删失败,缓存里留的也是旧值,下次靠过期时间兜底。

延迟双删:补竞态窗口

极端并发下,写库后删缓存的瞬间,可能有一个读请求正拿着旧值准备回填。延迟双删就是:删完缓存后,等一个读写往返的时间,再删一次缓存,把这个窗口内被回填的旧值清掉。延迟时间要略大于一次读的耗时,太短无效、太长浪费。