03——缓存双写一致性
一、缓存双写一致性
- 如果redis中有数据,需要和数据库中的值相同
- 如果redis中无数据,数据库中的值要是最新值,且准备回写redis
缓存按照操作来分,可以分为两种:
-
只读缓存
-
读写缓存
-
同步直写操作(及时生效)
写数据库后,也同步写redis缓存,缓存和数据库中的数据一致
对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略
-
异步缓写策略
正常业务中,mysql数据变动了,但是可以在业务上允许出现一定时间后才作用与redis(仓库、物流)
异常情况出现了,不得不将失败的动作重新修补,有可能需要借助kafka或者其他MQ等消息中间件,实现重试重写
-
双检加锁策略:当多个线程同时去查询数据库的某一条数据时,可以在第一个查询数据的请求上使用一个互斥锁。其他线程获取锁失败,就会阻塞。第一个线程查询完毕,并将数据回写redis后,其他线程直接从redis中获取数据。以此来减轻数据库的压力。
二、数据库和缓存一致性的几种更新策略
目标:数据最终一致性
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。
上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是100%绝对正确,不保证绝对适配全部情况,请同学们自行酌情选择打法,合适自己的最好。
可以停机的情况:
挂牌报错,凌晨升级,温馨提示,服务降级
单线程,这样重量级的数据操作最好不要多线程
四种更新策略:
-
先更新数据库,再更新缓存
案例一:
更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
1、先更新mysql修改为99成功,然后更新redis。
2、此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100。上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据
案例二:
-
先更新缓存,再更新数据库
业务上一般把mysql作为底单数据库,保证最后解释
案例一:
-
先删除缓存,再更新数据库
案例:
两个并发操作,一个是更新操作,另一个是查询操作,
A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eSCPx6nt-1692427619711)(https://you-blog.oss-accelerate.aliyuncs.com/2023/202303062316864.png)]
解决方案: 延时双删策略
加上sleep的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。所以,线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。
延迟双删问题:
-
这个删除该休眠多久呢
线程A sleep的时间,需要大于线程B读取数据再写入缓存的时间
确认时间的方法:
-
第一种方法:
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
-
第二种方法:
新启动一个后台监控程序,比如后面要讲解的VatchDog监控程序,会加时
-
-
这种同步淘汰策略,吞吐量降低怎么办
使用异步线程,避免阻塞
-
看门狗WatchDog分析
-
-
先更新数据库,再删除缓存
-
异常问题:
-
业务指导思想
-
微软云
https://learn.microsoft.com/en-us/azure/architecture/patterns/cache-aside
-
阿里canal
-
-
解决方案
流程如下图所示:
-
更新数据库数据
-
数据库会将操作信息写入binlog日志当中
-
订阅程序提取出所需要的数据以及key
-
另起一段非业务代码,获得该信息
-
尝试删除缓存操作,发现删除失败
-
将这些信息发送至消息队列
-
重新从消息队列中获得该数据,重试操作。
-
可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)
-
当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
-
如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
-
如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。
-
-
类似经典的分布式事务问题
只能保证最终一致性
-
三、总结
-
如何选择方案?利弊如何
在大多数业务场景下,优先使用先更新数据库,再删除缓存的方案(先更库→后删缓存)。
理由如下:
-
先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满nysql。.
-
如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
如果使用先更新数据库,再删除缓存的方案: 如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Rdis缓存客户端暂停并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性,这是理论可以达到的效果,但实际,不推荐,因为真实生产环境中,分布式下很难做到实时一致性,一般都是最终一致性。
-
-
总结