Appearance
十八、缓存双写一致性之更新策略探讨
1、缓存双写一致性
1、如果redis中有数据
需要和数据库中的值相同
2、如果redis中无数据
数据库中的值要是最新值
2、缓存按照操作来分,有细分2种
1、只读缓存
2、读写缓存
- 同步直写策略:写缓存时也同步写数据库,缓存和数据库中的数据⼀致;
- 对于读写缓存来说,要想保证缓存和数据库中的数据⼀致,就要采⽤同步直写策略
3、数据库和缓存一致性的几种更新策略
单线程,这样重量级的数据操作最好不要多线程
总之,我们要达到最终一致性!
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记以mysql的数据库写入库为准。
1、3种更新策略
1、先更新数据库,再更新缓存
1 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
2 先更新mysql修改为99成功,然后更新redis。3 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。4 上述发生,会让数据库里面和缓存redis里面数据不一致,读到脏数据
2、先删除缓存,再更新数据库
问题
这里写20秒,是自己故意乱写的,表示更新数据库可能失败,实际中不可能...O(∩_∩)O哈哈~
1 A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时)
B突然出现要来读取缓存数据。
2 此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
2.1 B从mysql获得了旧值
B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis
获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。
3 A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,o(╥﹏╥)o
两个并发操作,一个是更新操作,另一个是查询操作,A更新操作删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。
于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
4 总结流程:
(1)请求A进行写操作,删除缓存后,工作正在进行中......A还么有彻底更新完
(2)请求B开工,查询redis发现缓存不存在
(3)请求B继续,去数据库查询得到了myslq中的旧值
(4)请求B将旧值写入redis缓存
(5)请求A将新值写入mysql数据库
上述情况就会导致不一致的情形出现。
解决方案
多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。
其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。
后面的线程进来发现已经有缓存了,就直接走缓存。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4PbAiBtE-1638778371955)(images/Redis6/image-20210628150653527.png)]
采用延时双删策略
双删方案面试题
这个删除该休眠多久呢
线程Asleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这个时间怎么确定呢?
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,
以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当前演示的效果是mysql单机,如果mysql主从读写分离架构如何?
(1)请求A进行写操作,删除缓存
(2)请求A将数据写入数据库了,
(3)请求B查询缓存发现,缓存没有值
(4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
(5)请求B将旧值写入缓存
(6)数据库完成主从同步,从库变为新值 上述情形,就是数据不一致的原因。还是使用双删延时策略。
只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms
这种同步淘汰策略,吞吐量降低怎么办?
3、先更新数据库,再删除缓存
业务指导思想
- 老外论文 https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside
- 知名社交网站facebook也在论文《Scaling Memcache at Facebook》中提出 https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf
- 我们上面的canal也是类似的思想 上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。
解决方案
1 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
2 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
3 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
4 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。
4、方案2和方案3用那个?利弊如何
在大多数业务场景下,我们会把Redis作为只读缓存使用。假如定位是只读缓存来说,
理论上我们既可以先删除缓存值再更新数据库,也可以先更新数据库再删除缓存,但是没有完美方案,两害相衡趋其轻的原则
个人建议是,优先使用先更新数据库,再删除缓存的方案。理由如下:
1 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,严重导致打满mysql。
2 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
多补充一句:如果使用先更新数据库,再删除缓存的方案
如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。