分布式系统——并发条件下如何保证缓存与DB数据一致性
Posted loveletters
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分布式系统——并发条件下如何保证缓存与DB数据一致性相关的知识,希望对你有一定的参考价值。
什么是数据一致性
我们常说的数据一致性指的是在程序运行过程中本地缓存、分布式缓存、数据库三者之间的数据一致性
常见的本地缓存有 hashmap、currenthashmap、guava cache、caffeine
分布式缓存常见的有 redis、memcache
常见数据不一致常见有:
-
本地缓存与mysql不一致
-
redis缓存与mysql数据不一致
引入本地缓存的目的是增加服务的吞吐量、但是同时也会让架构变得复杂。
本地缓存与DB一致性解决方案
MQ方案
-
新增或更新了一条数据
-
通过gateway负载均衡策略转发到某一台应用A-1
-
应用A-1收到请求后,更新DB,同时更新了自己的本地缓存
-
应用A-1发送更新MQ的广播消息
-
应用A-2跟应用A-3收到消息,查询DB,更新本地缓存。
比较本地JVM变量的版本号与redis版本号
- 在应用本地定义一个成员变量LongAddr的版本号
- 比较redis中的版本号与本地变量中的是否相等
- 如果不相等则将DB中的数据读取到本地,同时将redis的版本号赋值到本地的版本号。
redis缓存与DB一致性解决方案
基于binlog方案
- 更新DB数据
- 通过CDC中间件监听mysql的binlog,同时同步数据到MQ
- 启动一个数据处理应用,消费MQ数据并进行数据加工
- 将加工后的数据写入redis
- 查询redis数据返回
延迟双删
先进行缓存清除,再执行更新操作,最后(延迟N秒)再执行缓存删除,上述中延迟的时间一定要大于一次读写操作的时间,原因是如果延迟时间小于写入redis的时间,会导致请求1删除了缓存,但是请求2缓存还未写入。
基于定时任务方案
- 更新DB数据,同时写入数据到redis
- 启动一个定时任务将DB数据同步到redis
- 前端发起接口查询请求,先从redis中查询数据
- reidis中如果没有数据,加一个分布式锁,再从DB中查询数据
- 查询redis返回数据
大厂面试01期高并发场景下,如何保证缓存与数据库一致性?
面试题:高并发场景下,如何保证缓存与数据库一致性?
问题分析
我们日常开发中,对于缓存用的最多的场景就像下图一样,可能仅仅是对数据进行缓存,减轻数据库压力,缩短接口响应时间。
这种方案在不需要考虑高并发得去写缓存,高并发得读写缓存时,是不会有问题,但是如果是在高并发场景下,要保证缓存和数据库的一致性,至少需要解决以下问题:
高并发写时的数据不一致问题
高并发读写时,请求执行各步骤的顺序是不可控的。假设此时有一个请求A,B都在在执行写流程,请求A是需要将某个数据改成1,请求B是需要将某个数据改为2,执行操作如下时就会导致数据不一致的问题:
1.请求A执行操作1.1删除缓存。
2.请求A执行操作1.2更新数据库,将值改为1。
3.请求B执行操作1.1删除缓存。
4.请求B执行操作1.2更新数据库,将值改为2
5.假设说请求B所在服务器网络延迟比较低,请求B先更新缓存,此时缓存中的key对应的value是2。
6.请求A更新缓存,将缓存中B更新的数据进行覆盖,将key对应的值改为1。
此时数据库中是B修改后的数据,值为2,而缓存中的数据是1,这样在缓存过期钱,用户读到的都是脏数据,与数据库不一致。
高并发读写时的数据不一致的问题
高并发读写时,请求执行各步骤的顺序是不可控的。假设此时有一个请求A在执行写流程,将原值由1改成2,请求B执行读流程,执行操作如下时就会导致数据不一致的问题:
1.写请求A执行1.1操作删除缓存key,value是原值1。
2.读请求B执行2.1操作发现缓存中没有数据,就去执行2.2操作读数据库,读到旧数据,值为1。
3.写请求A执行1.2操作更新数据库,将数据由1改为2。
4.写请求A执行1.3操作更新缓存,此时缓存中的数据key对应的value是2。
5.读请求B执行2.3操作更新缓存,将之前读到的旧数据1设置到缓存中,此时缓存中的数据key对应的value是1。
所以如果说读请求B所在服务器网络延迟比较高,去执行2.3操作比写请求A晚,就会导致写请求A更新完缓存后,读请求B使用之前读到的旧数据去更新缓存,此时缓存中数据就与数据库中的不一致。
解决方案
保证数据一致性,网上有很多种方案,例如:
1.先删除缓存,再更新数据库。
2.先更新数据库,再删除缓存。
3.先删除缓存,再更新数据库,然后异步延迟一段时间再去删一次缓存。
但是这些方案都是存在各种各样的问题,这里篇幅有限,只给出目前相对正确的三套方案,目前的这些方案也有自己的局限性。
方案1.写请求串行化
写请求
1.写请求更新之前先获取分布式锁,获得之后才能去数据库更新这个数据,获取不到就进行等待,超时后就返回更新失败。
2.更新完之后去刷新缓存,如果刷新失败,放到内存队列中进行重试(重试时取数据库最新数据更新缓存)。
读请求
读请求发现缓存中没有数据时,直接去读取数据库,读完更新缓存。
总结
这种技术方案通过对写请求的实现串行化来保证数据一致性,但是会导致吞吐量变低。比较适合银行相关的业务,因为对于银行项目来说,保证数据一致性比可用性更加重要,就像是去存款机存钱,取钱时,为了保证账户安全,都是会让用户执行操作后,等待一段时间才能获得反馈,这段时间其实取款机是不可用的。
方案2.先更新数据库,异步删除缓存,删除失败后重试
1.先更新数据库
2.异步删除缓存(如果数据库是读写分离的,那么删除缓存时需要延迟删除,否则可能会在删除缓存时,从库还没有收到更新后的数据,其他读请求就去从库读到旧数据然后设置到缓存中。)
3.删除缓存失败时,将删除的key放到内存队列或者是消息队列中进行异步重试
发散思考
在更新完数据库后,我们为什么不直接更新,而是采用删除缓存呢?
这是因为直接更新缓存的话,在高并发场景下,有多个更新请求时,难以保证后更新数据库的请求会后更新缓存,也就是上面的高并发写问题。如果采用删除缓存,可以让下次读时读取数据库,更新缓存,保证一致性。
方案3.业务项目更新数据库,其他项目订阅binlog更新
1.业务项目直接更新数据库。
2.cannal项目会读取数据库的binlog,然后解析后发消息到kafka。
3.然后缓存更新项目订阅topic,从kafka接收到更新数据库操作的消息后,更新缓存,更新缓存失败时,新建异步线程去重试或者将操作发到消息队列,后续再进行处理。
总结:
但是这种方案在更新数据库后,缓存中还是旧值,必须等缓存更新项目消费消息后,更新缓存,缓存中才是最新值。所以更新操作完成与更新生效之间会有一定的延迟。
最后
大家有了解其他的技术方案,欢迎大家一起讨论!本文已收录到1.1K Star数开源学习指南——《大厂面试指北》,如果想要了解更多,可以访问预览地址:http://notfound9.github.io/interviewGuide/
参考链接:
https://www.cnblogs.com/-wenli/p/11474164.html
https://www.cnblogs.com/rjzheng/p/9041659.html
以上是关于分布式系统——并发条件下如何保证缓存与DB数据一致性的主要内容,如果未能解决你的问题,请参考以下文章