TouchAll技术博客

TouchAll技术博客

[系统设计 - 缓存设计专题] 3. 缓存一致性

2026-07-11

缓存一致性

缓存一致性矛盾根源是: 主存储与副本缓存之间的同步无法原子化导致的,会出现缓存与数据库数据不一致。

如何设计缓存一致性方案

flowchart TD A[开始] --> B{读多写多?} B -- 读多 --> C{并发高?} C -- 低 --> D0[旁路缓存] C -- 一般 --> D1[旁路缓存+延时队列双删] C -- 高 --> E[读穿透+直接同步删除缓存] B -- 写多 --> F{是否强一致?} F -- 是 --> G[读写穿透] F -- 否 --> H{可靠性?} H -- 高 --> J[写穿透] H -- 低 --> K[写回]

使用缓存的场景

  1. 读多写少(绝大多数业务:配置、商品、用户信息、不怎么变化的数据)
  2. 写多读少/强数据可靠(订单、计费、设备实时状态、要求强一致)
    1. 最终一致(业务可容忍短暂不一致,绝大多数场景)
    2. 强一致(读写完成后缓存 DB 立刻同步,无脏读窗口)

处理缓存一致性的通用标准方案

Cache Aside(最常用,最终一致)

有两种路径: 1.先更新 DB,后删除缓存;2.先删除缓存,后更新 DB

路径一

更新路径

flowchart TD a[更新主存储] b[删除缓存] a --> b

命中缓存路径

flowchart TD a[请求数据] c[返回缓存] e[读取主存储] f[更新缓存] a-- 命中缓存key -->c a-- 未命中缓存key -->e e-->f f-->c

优点:

  1. 并发冲突概率低

缺点:

  1. 幻读,在更新主存储与删除缓存之间的时候,可能触发命中缓存路线,读到了旧数据。但选择这条路线就表示可以接受一定时间内的不一致。
  2. 脏读,在删除缓存后
  3. 不适合高并发多场景,容易缓存击穿,删除缓存后可能一瞬间主存储被击穿。

路径二

  1. 删除缓存
  2. 更新主存储

缺点:

  1. 永久脏数据,如果删除缓存后,还没更新主存储,此时请求促使路线更新了缓存,则存在永久脏数据。

兜底措施

  1. 延时双删,延时(延迟窗口要大于查询线程「读缓存→查 DB→写缓存」 的完整耗时: 500ms-1000ms)再删除一次,确保不会产生永久的脏数据。
延时实现
  1. 业务线程延迟: 如果发生宕机,容易丢失操作。
  2. 异步延时队列(推荐):更新逻辑完成后发送消息到延迟队列中,可用性可靠性由队列保证。

FAQ

为什么不能只延时删除?

  • 只延时删除在并发高情况下,导致该延时窗口里有大量脏读请求。

什么场景可以不使用延时双删?

  • 读写串行化(有锁)
  • 单线程操作数据
  • 并发极低

读写穿透(强一致性)

这是一种数据代理方案,即使用 Cache 代理统一返回数据,能够保证数据强一致性。读写穿透是两条链路,一起配合使用才能保证强一致性。
必须保证读写路径一致,不被绕开,否则无法保证强一致。

Write Through

写操作直接同时更新缓存和主存储,缓存和数据库同写成功才返回。需要做临时事务隔离才行

更新路径

sequenceDiagram participant App as 业务应用 participant Cache as 缓存 participant DB as 主存储 App -->> DB: 更新数据 App -->> Cache: 更新缓存(开启缓存事务提交) alt 更新DB失败 DB -->> DB: 回滚数据 DB -->> App: 返回写入失败 else 更新DB成功 DB -->> App: 返回写入成功 end

优点:

  1. 保证了缓存与主存储的强一致性

缺点:

  1. WriteThrough 的事务隔离只作用于单个 key,多表、多个缓存 key 无法原子提交,复杂关联更新(订单 + 库存)必须加锁保证整体串行。
  2. 要增加临时隔离的事务屏蔽读,增加了复杂度。
  3. 如果要保证覆盖顺序,需要加锁。

Read Through

sequenceDiagram participant App as 业务应用 participant Cache as ReadThrough缓存代理 participant DB as 主存储 App -->> Cache: query(key) Cache -->> Cache: 查询缓存key alt 缓存命中 Cache -->> App: 返回缓存数据 else 缓存未命中 Cache -->> DB: 查询原始数据 DB -->> Cache: 返回原始数据 Cache -->> Cache: 回填缓存 key=data Cache -->> App: 返回缓存数据 end

优点:

  1. key未命中时,缓存组件自动回填获取最新数据,避免脏读和幻读。

缺点:

  1. 高并发时,缓存组件会阻塞所有请求。
  2. 若不搭配写穿透,使用 Cache-Aside或双写 方案在高并发时,仍会存在短暂窗口的脏数据。

高并发优化方案(解决并发的缓存击穿)

  1. 单 key 并发回填锁(最通用,互斥锁)
  2. 缓存占位预热
  3. 批量合并回填缓存:代理做请求队列聚合, 回填后notify all。

写回/异步刷盘

写只更新缓存,异步批量刷数据库

优点:

  1. 写入快

缺点:

  1. 缓存宕机数据丢失。
  2. 不可靠,金融类有风控的业务禁用。