双写一致性

面试题

1.缓存双写基本逻辑图.jpg

你只要用缓存,就可能涉及到 redis 缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?

双写一致性,你先动缓存 redis 还是数据库 MySQL 哪一个?why?

延时删除 你做过吗?会有哪些问题?

有这么一种情况,微服务查询 redis 无 MySQL 有,为保证数据双写一致性回写 redis 你需要注意什么?

双检加锁 策略你了解过吗?如何尽量避免缓存击穿?

redis 和 MySQL 双写 100% 会出纰漏,做不到强一致性,你如何保证 最终一致性

缓存双写一致性的理解

如果 redis 中有数据

需要和数据库中的值相同

如果 redis 中无数据

数据库中的值要是最新值,且准备回写 redis

缓存按照操作来分,细分 2 种

  • 只读缓存

  • 读写缓存

    • 同步直写策略

      写数据库之后也同步写 redis 缓存,缓存和数据库中的数据一致;

      对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略。

    • 异步缓写策略

      正常业务中,MySQL 数据变了,但是可以在业务上容许出现一定时间后才作用于 redis,比如仓库、物流系统;

      异常情况出现了, 不得不将失败的动作重新修补,有可能需要借助 kafka 或者 RabbitMQ 等消息中间件,实现重试重写。

采用双检加锁策略

多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。

后面的线程进来发现已经有缓存了,就直接走缓存。

2.双检策略.jpg

数据库和缓存一致性的几种更新策略

目的:总之,我们要达到最终一致性

给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。

我们可以对存入缓存的数据设置过期时间,所有的 写操作以数据库为准 ,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性, 切记,要以 mysql 的数据库写入库为准

上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距, 不是 100% 绝对正确,不保证绝对适配全部情况 ,需要自己酌情选择打法,合适自己的最好。

可以停机的情况

挂牌报错,凌晨升级,温馨提示,服务降级

单线程,这样重量级的数据操作最好不要多线程

我们讨论 4 种更新策略

1.先更新数据库,在更新缓存

异常问题 1

  1. 先更新 mysql 的某商品的库存,当前商品的库存是 100,更新为 99 个。
  2. 先更新 mysql 修改为 99 成功,然后更新 redis。
  3. 此时假设异常出现,更新 redis 失败了,这导致 mysql 里面的库存是 99 而 redis 里面的还是 100。
  4. 上述发生,会让数据库里面和缓存 redis 里面数据不一致,读到 redis 脏数据。

异常问题 2

【先更新数据库,再更新缓存】,A、B 两个线程发起调用

【正常逻辑】

  1. A update mysql 100
  2. A update redis 100
  3. B update mysql 80
  4. B update redis 80

【异常逻辑】

多线程环境下,A、B 两个线程有快有慢,有前有后有并行

  1. A update mysql 100

  2. B update mysql 80

  3. B update redis 80

  4. A update redis 100

最终结果,mysql 和 redis 数据不一致,o (T_T) o,

mysql 80,redis 100

2.先更新缓存,再更新数据库

不推荐,业务上一般把 MySQL 作为 底单数据库,保证最后解释

[先更新缓存,再更新数据库],A、B 两个线程发起调用

[正常逻辑]

  1. A update redis 100
  2. A update mysql 100
  3. B update redis 80
  4. B update mysql 80

[异常逻辑]

多线程环境下,A、B 两个线程有快有慢有并行

  1. A update redis 100
  2. B update redis 80
  3. B update mysq 80
  4. A update mysq 100

mysql 100,redis 80

3.先删除缓存,在更新数据库

异常问题:

步骤分析,先删除缓存,再更新数据库

  1. A 线程先成功删除了 redis 里面的数据,然后去更新 mysql,此时 mysql 正在更新中,还没有结束。(比如网络延时)

    B 突然出现要来读取缓存数据。

  2. 此时 redis 里面的数据是空的,B 线程来读取,先去读 redis 里数据(已经被 A 线程 delete 掉了),此处出来 2 个问题:

    1. B 从 mysq 获得了旧值 B 线程发现 redis 里没有(缓存缺失)马上去 mysql 里面读取,从数据库里面读取来的是旧值。
    2. B 会把获得的旧值写回 redis 获得旧值数据后返回前台并回写进 redis(刚被 A 线程删除的旧数据有极大可能早被写回了)。
  3. A 线程更新完 mysql,发现 redis 里面的缓存是脏数据,A 线程直接懵逼了,o (T__T) o

    两个并发操作,一个是更新操作,另一个是查询操作,A 删除缓存后,B 查询操作没有命中缓存,B 先把老数据读出来后放到缓存中,然后 A 更新操作更新了数据库。

    于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。

  4. 总结流程:

    1. 请求 A 进行写操作,删除 redis 缓存后,工作正在进行中,更新 mysql…..

      A 还么有彻底更新完 mysql,还没 commit

    2. 请求 B 开工查询,查询 redis 发现缓存不存在(被 A 从 redis 中删除了)

    3. 请求 B 继续,去数据库查询得到了 mysq 中的旧值(A 还没有更新完)

    4. 请求 B 将旧值写回 redis 缓存

    5. 请求 A 将新值写入 mysql 数据库

    上述情况就会导致不一致的情形出现。

先删除缓存,再更新数据库:如果数据库更新失败或超时或返回不及时,导致 B 线程请求访问缓存时发现 redis 里面没数据,缓存缺失,B 再去读取 mysql 时,从数据库中读取到旧值,还写回 redis, 导致 A 白干了,o (π_ _π) o

解决方案:

采用延时双删策略 

3.延时双删.jpg

加上 sleep 的这段时间,就是为了让线程 B 能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程 A 再进行删除。所以,线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做 “延迟双删”。

延迟双删面试题

这个删除该休眠多久呢?线程 A sleep 的时间,就需要大于线程 B 读取数据再写入缓存的时间。

这个时间怎么确定呢?

第一种方法:

在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。 这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

第二种方法:

新启动一个后台监控程序,比如后面要讲解的 WatchDog 监控程序,会加时

这种同步淘汰策略,吞吐量降低怎么办?

4.异步双删.jpg

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

(目前用的比较多)

异常问题:

时间 线程 A 线程 B 出现的问题
t1 更新数据库中的值……
t2 缓存立刻命中,此时 B 读取的是缓存旧值 A 还没来得及删除缓存的值,导致 B 缓存命中读到旧值
t3 更新缓存的数据,over

先更新数据库,在删除缓存,假如缓存删除失败或者来不及删除,导致请求再次访问 redis 时缓存命中, 读取到的是缓存的旧值。

  • 业务指导思想

    微软云: https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside

    后面的阿里巴巴 canal 也是类似的思想

    订阅 binlog 程序在 MySQL 中有现成的中间件叫 canal,可以完成订阅 binlog 日志的功能。

  • 解决方案

    5.订阅binlog.jpg

    1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用 Kafka/RabbitMQ 等)。 2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。

    3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试 4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。

  • 类似经典的分布式事务问题,只有一个权威答案,只能达到最终一致性

    流量充值,先下发短信实际充值可能滞后 5 分钟,可以接受

    电商发货,短信下发但是物流明天见

小总结

方案如何选择?利弊如何

在大多数业务场景下,个人建议是,优先使用先更新数据库, 再删除缓存的方案(先更库→后删存)。理由如下:

1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满 mysql。

2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。

多补充一句:如果 使用先更新数据库,再删除缓存的方案

如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在 Redis 缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性。

策略 高并发多线程条件下 问题 现象 解决方案
先删除 redis 缓存,再更新 mysql 缓存删除成功但数据库更新失败 Java 程序从数据库中读到旧值 再次更新数据库,重试
缓存删除成功但数据库更新中…
有并发请求
并发请求从数据库读到旧值并回写到 redis,导致后续都是从 redis 读取到旧值 再次删除缓存,重试
先更新 mysql,再删除 redis 缓存 数据库更新成功,但缓存删除失败 Java 程序从 redis 中读到旧值 再次删除缓存,重试
数据库更新成功但缓存删除中……
有并发读请求
并发请求从缓存读到旧值 等待 redis 删除完成,这段时间数据不一致,短暂存在。
0%