如何应对数据库缓存双写一致性问题

「这是我参与2022首次更文挑战的第23天,活动详情查看:2022首次更文挑战」。

我们在日常的工作中常常会使用缓存去优化我们程序的性能,但是在使用了缓存之后,往往会引发一个问题,那就是数据库缓存双写一致性的问题,当我们更新数据时应当如何去做,才能更好的保证数据库缓存数据的一致。

1 前言

日常开发中,我们接口响应缓慢,往往是因为数据库读写产生的,这时为了优化这些接口,往往我们会将数据库的数据写入缓存中,让接口直接从缓存中获取数据,这样能极大的提高接口的访问速度。但是随之而来的问题就是,在我们更新数据库的数据时,就需要去更新缓存中的数据,或者是删除缓存中的数据,让其再次访问时通过读数据库,再将读到的数据刷入缓存中。但是如果在这期间出现并发,就很容易导致数据库缓存中数据不一致,这也是本篇文章的主题如何应对数据库缓存双写一致性问题。

这里我们说的一致性是指最终一致性,并不是强一致性。

2 推荐方案 Cache Aside Pattern

命中:程序从缓存读取数据,读到数据即命中

失效:程序从缓存读取数据,未读到数据,此时缓存失效,需要先去数据库读取,再刷入缓存

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

3 更新方案

  • 更新缓存

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

    • 先删除缓存,再更新数据库
    • 先更新数据库,再删除缓存(常用)

4 更新方案选择原因

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

如下图,当A线程更新完缓存的数据A,这时A线程出现延迟,B线程将A线程缓存的更新覆盖,并且将数据库中的数据A也更新,A线程恢复去更新数据库,这时又将B线程对数据库的修改覆盖,这样就会出现严重的双写不一致,导致后续每次读取到的缓存中的数据都是有问题的,并且数据库的数据对我们来说更为重要,我们一般持久化都是依赖数据库的,如果先更新缓存的话,后续程序宕机,数据库中的数据就得不到更新,我们一般是不会依赖缓存做持久化保存的,所以这种方案是一定不能选择的。

image.png

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

如下图,当A线程更新完数据库的数据A,这时A线程出现延迟,B线程将A线程数据库的更新覆盖,并且将缓存中的数据A也更新,A线程恢复去更新缓存,这时又将B线程对缓存的修改覆盖,这样就会出现严重的双写不一致,导致后续每次取到缓存中的数据都是有问题的。

image.png

与上个方案一样,凡是更新缓存数据的都会出现这种严重的双写不一致,所以一般我们不采取更新缓存的方案,而是从删除缓存中的方案做选择。

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

如下图,当A线程删除缓存数据A,这时A线程出现延迟,B线程将读取A,发现缓存无数据,将数据库A的旧值查出来,并且将其更新到缓存中,当A线程恢复时,又将A的新值写入数据库,这样也会出现严重的双写不一致,导致后续每次取到缓存中的数据都是有问题的。

image.png

所以,先删除缓存的方案也不建议选择。

4.4 先更新数据库,再删除缓存(常用)

先更新数据库再删除缓存这种方案是我们所选择的,当然这种也有几率出现缓存不一致的现象,当缓存失效时,就会出现和先删除缓存,在更新数据库一样的问题。如下图:

image.png

但是一般情况下,是不会出现上述情况,出现上述情况的机率是特别低的。出现上述情况也可以采取延迟双删,先删除一次,让线程休眠一会,再删除一次,就会将不小心写入的错误数据清掉。

所以说这种方案只会出现下一种情况,如果想要避免这种情况只能通过加锁来解决,避免读到脏数据。

image.png

5 优化方案

基于上述方案我们还可以做哪些优化

  • 读数据加锁(分布式锁)防止高并发打垮数据库
  • 延迟双删,防止缓存失效时(读写分离架构下,读从库延迟问题),存入旧数据,第二次删除可以异步执行等待删除
  • 如果需要做重试机制可以依赖于消息队列的可靠消费
  • 可以通过订阅Binlog日志来优化删除逻辑

禁忌:过度设计,一般简单的延迟双删就可以实现需求,无需增加系统复杂度 image.png

猜你喜欢

转载自juejin.im/post/7066344853277245470