缓存和数据库为什么会不一致
为了扛高并发,我们把热点数据放进缓存、真实数据存数据库。但读写是两条路径:读先查缓存、没有再查库回填;写先改库、再删或改缓存。这两步不是原子的,在并发、重试、宕机下就会出现缓存还是旧值、数据库已是新值的不一致。彻底强一致成本极高,工程上追求的是可接受时间内的最终一致。
- 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);
更新数据库还是先删缓存
业界共识是先更新数据库、再删缓存。反过来先删缓存再更新库,会有一个危险窗口:删完缓存、还没来得及改库,另一个读请求把旧值又回填进缓存,导致长期不一致。先改库再删缓存,即使删失败,缓存里留的也是旧值,下次靠过期时间兜底。
延迟双删:补竞态窗口
极端并发下,写库后删缓存的瞬间,可能有一个读请求正拿着旧值准备回填。延迟双删就是:删完缓存后,等一个读写往返的时间,再删一次缓存,把这个窗口内被回填的旧值清掉。延迟时间要略大于一次读的耗时,太短无效、太长浪费。
订阅 binlog:彻底解耦的方案
对一致性要求更高的场景,业务代码里手动删缓存很容易漏。可以用 Canal 订阅数据库的 binlog,把每一次变更事件投到消息队列,由专门的同步消费者去更新或删除缓存。这样业务不用关心缓存,一致性由数据变更驱动,可靠性高得多。
热点 key 与并发写
热点 key 失效瞬间,大量请求同时打回数据库(缓存击穿)。可以给热点 key 不过期、由后台异步刷新,或用互斥锁只允许一个请求去查库回填。并发写频繁的字段,干脆不放进缓存,直接读库,避免反复不一致。
一致性级别怎么选
不是所有数据都要强一致。商品详情页晚几百毫秒更新完全可接受,用 Cache Aside 加过期即可;账户余额、库存扣减这种错一点就出事的,要么不走缓存、要么用 binlog 同步加短过期。按业务对脏数据的容忍度分层设计,别一刀切。
落地清单
- 读用 Cache Aside,写先更库再删缓存。
- 关键路径用延迟双删补竞态窗口。
- 高一致性数据用 binlog 订阅同步缓存。
- 热点 key 防击穿,别让失效瞬间打垮库。
- 按业务容忍度分层,别对所有数据强一致。