缓存一致性怎么保证?

wen python案例 2

本文目录导读:

缓存一致性怎么保证?

  1. 业务系统层面:Redis与数据库(MySQL)的一致性
  2. 计算机系统层面:CPU多核缓存一致性
  3. 总结:如何选择合适的策略?
  4. 最终的“保命”经验:

缓存一致性是分布式系统、数据库以及多核CPU架构中的一个核心难题,要理解如何保证它,首先需要明确矛盾点:数据在缓存(速度快)和主存(速度慢)之间有两份拷贝,当一方修改了数据,另一方就变得“不一致”了。

针对不同的应用场景,保证缓存一致性的策略完全不同,下面从业务系统(如Redis与MySQL)计算机系统(如CPU多核缓存) 两个层面来详细说明。


业务系统层面:Redis与数据库(MySQL)的一致性

这是日常开发中最常遇到的问题,目标是让Redis中的缓存数据和MySQL中的数据最终一致(甚至强一致)。

核心原则:先更新数据库,再删除(或更新)缓存

这几乎是业界公认的最佳实践“套路”,但需要配合正确的策略。

最常用策略:Cache-Aside Pattern + 延迟双删

  • 读操作

    1. 先读缓存,命中则直接返回。
    2. 缓存未命中,则读数据库。
    3. 将读取的数据写入缓存(设置过期时间)。
    4. 返回数据。
  • 写操作

    1. 更新数据库(第一步,关键)。
    2. 删除缓存(第二步,而非更新缓存)。

    为什么是“删除”而不是“更新”?

    • 避免并发写:如果同时有两个写请求,先更新缓存A,再更新缓存B,但数据库更新顺序相反,会导致缓存数据是脏的。
    • 懒加载:缓存只会在下次被读取时才加载,减少了不必要的写缓存开销,如果数据很少被读取,删掉它非常划算。
  • 问题与解决:并发读写的“脏读”问题

    • 场景:线程A更新数据库,随后准备删除缓存,在线程A删除缓存之前,线程B读取缓存(发现还有旧数据)并返回,这导致了短暂的不一致。
    • 解决方案:延迟双删
      1. 先删除缓存(预删除)。
      2. 更新数据库。
      3. 休眠一小段时间(如几百毫秒)
      4. 再次删除缓存。
    • 原理:首次删除确保后续读请求会去读库,第二次删除是为了“消灭”在更新数据库期间,恰好有读请求写入缓存的旧数据,休眠时间需要大于一次读请求 + 写缓存的时间。

强一致性方案:读写锁(分布式锁)

如果业务不能接受任何不一致(例如交易、库存),可以使用读写锁

  • 写锁:在更新数据库和删除缓存之前,先获取写锁,其他所有读写请求都要等待。
  • 读锁:读数据时,获取读锁,多个读锁可以共存,但写锁需要排他。
  • 代价:会极大地降低系统的并发性能,将“缓存”降级为“同步队列”,通常只在极端场景(如并发写冲突极高)下使用。

终极方案:缓存和数据库同步写入(2PC / TCC)

  • 2PC(两阶段提交):数据库和缓存都作为一个参与者,要么都写成功,要么都回滚,但实现复杂,性能损失大,且Redis原生不支持2PC。
  • TCC(Try-Confirm-Cancel):尝试(预留资源)、确认(写入)、取消(回滚),适用于复杂的跨服务事务。

兜底方案:设置过期时间

  • 所有缓存数据都应该设置合理的TTL(过期时间),即使因为并发问题出现了短暂的不一致,一旦缓存过期,下一次读请求就会从数据库拉取最新数据,自动修复不一致。

计算机系统层面:CPU多核缓存一致性

当多个CPU核各自拥有自己的L1/L2缓存时,需要保证它们看到同一份内存数据的“视图”是一致的。

核心技术:缓存一致性协议(MESI及其变种)

MESI是经典的缓存一致性协议,它定义了缓存行(Cache Line)的四种状态:

  • M(Modified,修改):该缓存行数据是脏的,尚未写回主存,且仅此CPU拥有。
  • E(Exclusive,独占):该缓存行数据与主存一致,且仅此CPU拥有。
  • S(Shared,共享):该缓存行数据与主存一致,且多个CPU拥有该副本。
  • I(Invalid,失效):该缓存行无效,不能使用。

工作原理(通过总线嗅探实现):

  1. CPU A 读取数据 x:如果x不在缓存中,从主存加载,状态为 E(独占),如果其他CPU也有,则变为 S(共享)。
  2. CPU B 读取同一数据 x:CPU A 嗅探到总线上有读请求,将自身状态从 ES 变为 S,CPU B 加载后状态也是 S
  3. CPU A 修改数据 x
    • CPU A 必须先发送一个“读独占(Read For Ownership,RFO)”信号到总线。
    • 其他CPU(如CPU B)嗅探到这个信号,将自身状态改为 I(失效)。
    • 之后CPU A 修改数据,状态变为 M(修改)。
  4. CPU B 再次读取数据 x
    • CPU B 发现缓存状态为 I(失效),于是发起读请求。
    • CPU A 嗅探到读请求,将自身状态从 M 写回主存,并变为 SE(取决于是否保留副本)。
    • CPU B 从主存(或直接从CPU A的缓存)读取最新的数据。

现代CPU的优化: MESI协议性能有瓶颈(总线压力大,写操作需要等待其他核心失效),现代CPU引入了写缓冲(Store Buffer)失效队列(Invalidation Queue) 等机制,这导致了内存可见性问题(一个线程的写操作对其他线程不可见),需要通过内存屏障(如Java中的volatile关键字的实现)来强制执行顺序和可见性。


如何选择合适的策略?

场景 策略 特点 适用场景
业务系统(高并发) Cache-Aside + 延迟双删 最终一致性,性能最高 绝大多数业务系统,如商品详情页、用户信息
业务系统(强一致) 读写锁 / 分布式锁 强一致性,性能中等 库存扣减、账户余额、有严格限制的配置
业务系统(要求极高) 2PC / TCC + 缓存旁路 强一致性,性能最低 支付结算、核心交易链路
CPU多核缓存 MESI / MOESI / 各种变体 硬件级别保证一致性 所有现代多核CPU的透明执行

最终的“保命”经验:

  1. 不管用什么方案,缓存一定要设置过期时间。 这是最终一致性最坚固的防线。
  2. 写操作永远先更新数据库,再操作缓存。 (不要反过来,否则并发问题极难处理)。
  3. 能删缓存就别更新缓存。 删除是原子操作(清除一个键),而更新需要复杂的并发控制。
  4. 如果流量极低且允许不一致,可以完全不使用缓存。 不是所有系统都需要缓存一致性。
  5. 实际生产中最稳妥的方案: 更新数据库 -> 删除缓存 -> (可选)短暂延时后再次删除缓存。 配合一个兜底的异步补偿任务(如监听数据库Binlog,发现有变更后刷新缓存),可以在99.99%的情况下达到“准强一致”的效果。

抱歉,评论功能暂时关闭!