Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?

微信公众号: 大黄奔跑
关注我,可了解更多有趣的面试相关问题。

写在之前

Hello,大家好,我是只会写HelloWorld的程序员大黄。

本文的主题标题已经说明,今天想和大家讨论一下,开发或者面试过程中关于缓存与数据库一致性问题该如何回答呢,这个是我字节三面的时候被问到的问题。


如果对数据库和缓存具有强一致性要求时,不要利用缓存了,因此根据CAP理论,只要涉及到双写就一定存在一致性问题。我们今天讨论的前提是对于缓存与数据库没有强一致要求。

如果可以容忍暂时的不一致,通常的做法给缓存设置一个过期时间,所有的写操作都以数据库为准,缓存过期后从数据库中取值,保存了数据最终一致性,尽可能得降低数据库和缓存之间的不一致。

如果不依赖过期时间,比如如果要修改某条记录,该如何保证数据库和缓存的一致性呢?

下面说一下常见的三种思路

  1. 先更新数据库、再更新缓存
  2. 先删除缓存、再更新数据库
  3. 先更新数据库、再删除缓存

先更新数据库、再更新缓存

结论:这种策略会存在线程安全及资源损耗问题,不适合生产环境。

比如同时有两个请求A和请求B,可能存在如下情况:
Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第1张图片

(1) 请求A更新数据库(比如age=12)
(2) 请求B更新数据库(比如age=22)
(3) 请求B更新缓存
(4) 请求A更新缓存

这样会产生什么问题?
缓存中存储的数据是错误的,严重的脏数据(数据库的数据已经变成了age=22,但是缓存中age还是12)

为什么会产生这种问题呢?
比如请求A比请求B先更新数据库,但是由于其他的原因导致(比如网络、当时服务器性能等),请求B执行更快,先更新了缓存,之后请求A再更新缓存,这就导致了脏数据(数据库和缓存中数据不一致)。

结论中说到会产生资源损耗问题,为什么呢?
(1)如果当前业务属于写多读少,每次对数据库修改之后,都需要再次更新缓存,产生了没有必要的开销。
(2)如果数据库和缓存中存储的数据不是简单的copy,而是经过复杂的运算,频繁的更新缓存,产生的资源损耗同样不容忽视。

先删除缓存、再更新数据库

结论:该方案同样会导致线程安全问题。

同样针对两个请求,比如请求A(作用将age=10改为age=12)和请求B(查询age,原来age=10),可能存在如下情况:
Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第2张图片

具体的情况:

(1) 请求A删除缓存(缓存中age=10被删除掉)
(2) 请求B查询信息(发现缓存不存在)
(3) 请求B缓存穿透,查询数据库(得到了age=10)
(4) 回填到缓存中 (将age=10回填到缓存中)
(4) 请求A更新数据库(将数据库中age=10改为age=12)

发现没有,数据库存储的age=12,但是缓存中存储的age=10,数据库和缓存直接不一致,缓存中存储的仍然是旧值,数据库中存储的是脏数据。

上面的数据库暂时还不考虑主从备份的情况下,如果是主库写、从库读,则脏数据可能性更高。
比如下面是两个请求:

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第3张图片

(1) 请求A删除缓存(缓存中age=10被删除掉)
(2) 更新数据库,写入主库
(3) 请求B查询信息(发现缓存不存在)
(4) 请求B缓存穿透,查询数据库(得到了age=10)
(5) 回填到缓存中 (将age=10回填到缓存中)
(6) 将主库中的数据同步到从库中(age=12)

同样会导致数据库和缓存之间的不一致。

先更新数据库、再删除缓存

结论:与前面两种方法不同,该方法先更新数据库,然后直接删除缓存,这种方法可以吗?我觉得同样会存在线程安全问题,只是概率会比较小而已。

比如如下场景,假设请求A(查询数据)和请求B(更新数据,将age=10更新为age=12)

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第4张图片

上面图中可以看出,请求A为读取到了旧数据并且刚好将旧数据回填到缓存中。

这种情况的概率实际上比前两种低的,请求A来的时候,缓存刚好失效,而且还在读请求(请求A)比写请求慢(请求B),一般情况下,数据库的读取操作比写操作更快的,通常请求A的2、3步比请求B的4、5步更加快,所以这种情况下,发生数据库和缓存不一致的概率更加低。

还有什么更加优化的方法吗?

如果想要数据库和缓存之间不一致概率和时间更低,可以采用如下思路:

  1. 给缓存设置过期时间。缓存过期后直接取数据库的值,进一步降低不一致时间。
  2. 采用缓存延时双删策略。在更新数据库,删除缓存之后,过一段时间再删除缓存。
  3. 保障的重试策略。

缓存延时双删该如何做呢?

还是以请求A(更新数据)、请求B(查询数据)为例

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第5张图片

当然这种也不能完全杜绝缓存和数据库之间的不一致问题,因为无法保证请求B的第二次删除一定在请求A的回填之后完成。当然这种概率多大呢?

如果还是不可以容忍这么低的概率可以采用重试策略。
具体思路有两种:

通过消息队列自发自收的方法进行双重删除

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第6张图片

具体的业务流程图

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第7张图片

该方法的核心将需要再次删除的key直接扔进消息队列中,消息队列本身为异步的,可以完美的做到延时功能。
但是该方法有一个弊端,需要写发送消息和接收消息的方法,并且需要将这套逻辑耦合到原业务代码中,算是对原业务造成了侵入。

通过监听数据库的binlog来进行双删

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第8张图片

总结

本文主要想和大家探讨一个老生常谈的问题,缓存与数据库一致性。讨论本文的前提是业务场景不要求缓存与数据之间的强一致性。比如订单支付问题不建议使用缓存,如果有人面试中不分青红皂白的说如何保证缓存与数据库的强一致性,大家可以让他gun了。

最后大黄分享多年面试心得。面试中,面对一个问题,大概按照总分的逻辑回答即可。先直接抛出结论,然后举例论证自己的结论。一定要第一时间抓住面试官的心里,否则容易给人抓不着重点或者不着边际的印象。

番外

另外,关注大黄奔跑公众号,第一时间收获独家整理的面试实战记录及面试知识点总结。

我是大黄,一个只会写HelloWorld的程序员,咱们下期见。

Offer快到碗里来:字节三面-缓存与数据库一致性如何保证呢?_第9张图片

你可能感兴趣的:(java,后端)