# 如何保证缓存和数据库的一致性?

在这里插入图片描述

# 核心策略

保证缓存(如 Redis)和数据库(如 MySQL)的数据一致性是一个非常经典、但也挺让人头疼的问题。因为两者的更新不是同一个原子操作,只要有时间差,或者高并发时执行顺序错乱,就会出现一致性问题

先说最基本的策略,一定要给缓存设置一个过期时间,避免异常情况下数据库和缓存长时间不一致

方案 问题 结论
先更新数据库,再更新缓存 会出现脏数据 不推荐
先更新缓存,再更新数据库 会出现脏数据 不推荐
先删除缓存再更新数据库 会有并发读写问题,虽然可以用延迟双删解决,但还是有风险 可用,但控制不好再次删除的时间,依然有风险
先更新数据库,再删除缓存 出现问题的概率极低 推荐
场景 方案 一致性强度
中小型项目 / 允许微小延迟 先更新DB + 再删缓存 最终一致
高并发 / 业务解耦 先更新DB + Canal监听Binlog + 异步删缓存 最终一致
强一致性 读写请求使用 分布式锁(Redisson) 或 读写锁 强一致

# 先更新数据库,再更新缓存

问题:如果两个线程同时更新同一条数据,可能会因为网络延迟导致并发乱序

现象:线程A先更新DB,线程B后更新DB;但缓存中,线程B的缓存先写进去,线程A的缓存后写进去。结果导致数据库是B的值,缓存是A的值,出现脏数据

结论:不推荐

在这里插入图片描述

# 先更新缓存,再更新数据库

在这里插入图片描述

如图所示,不同颜色的线代表不同的线程。无论是先更新数据库还是先更新缓存都会造成不一致的情况

除了并发的情况,我们还需要考虑异常的情况。比如更新数据库成功了,更新缓存失败了,或者更新缓存成功了,更新数据库失败了

基于内存缓存利用率的角度,我们一般不会采用同步更新的操作,因为有可能更新完的缓存并不一定会马上读取,导致缓存中缓存了大量无用的数据,降低缓存命中率。所以我们可以考虑删除缓存

# 先删除缓存,再更新数据库

线程A去删除缓存,然后准备写DB。此时线程B来读数据,发现缓存中没有,就去读了旧的DB数据,并把旧数据回写进了缓存。接着线程A才把新数据写入DB。结果缓存里永远是旧数据,还是会有并发读写问题

在这里插入图片描述延时双删解决,主要思路为删除数据库中的值后等待一段时间再删除一次缓存。这个等待的时间需要保证删除的时间点在写入旧缓存的时间点后面,所以这个等待的时间是难以估计的,极端情况下,仍然会出现缓存不一致的情况 在这里插入图片描述

# 先更新数据库,再删除缓存

在这里插入图片描述 从上图看,还是会存在数据不一致的情况,但是在实际中这个问题出现的概率并不高,因为要满足如下3个条件

  1. 缓存刚好失效
  2. 读写请求并发
  3. 更新数据库+删除缓存的时间,要比读数据库+写缓存时间短

条件3发生的概率是很低的,因为写数据库要加锁,耗费的时间都比较长。所以我们一般情况下会采用先更新数据库+再删除缓存的方案

# 解决删除缓存失败的方案

为了确保“先更新数据库,再删除缓存”在异常情况下依然可靠,我们需要引入重试与异步机制

# 消息队列重试机制

当应用层删除缓存失败时,将失败的那个 Key 丢进消息队列

流程如下

  1. 更新数据库
  2. 删除缓存失败
  3. 将失败信息发给 MQ
  4. 消费服务异步读取 MQ,尝试再次删除缓存,直到成功

# 订阅数据库Binlog

在这里插入图片描述

  1. 业务代码只管更新数据库。
  2. 数据库(如 MySQL)生成 Binlog 日志。
  3. 利用中间件(如 Canal)伪装成数据库从节点,实时异步订阅这个 Binlog。
  4. Canal 提取出变更的数据,投递给消息队列,最终由一个专用的消费服务去删除对应的缓存

业务代码完全不需要关心缓存更新,彻底解耦,且自带重试机制,一致性保障最高