一、缓存双写一致性,谈谈你的理解
1、如果redis中有数据
需要和数据库中的值相同
2、如果redis中无数据
数据库中的值要是最新值
二、缓存按照操作来分,细分2种
1、只读缓存
2、读写缓存
- 同步直写策略:写缓存时也同步写数据库,缓存和数据库中的数据⼀致。如:canal
- 对于读写缓存来说,要想保证缓存和数据库中的数据⼀致,就要采⽤同步直写策略。
3、什么时候同步直写?
小数据,某条、某—小撮热点数据,要求立刻变更,可以前台服务降级一下,后台马上同步直写。
4、什么时候异步缓写?
- 正常业务,马上更新了mysql,可以在业务上容许出现1个小时后redis起效。
- 出现异常后,不得不将失败的动作重新修补,不得不借助 kafka 或者 RabbitMQ 等消息中间件,实现解耦后重试重写。
三、数据库和缓存一致性的几种更新策略
1、原则
挂牌报错,凌晨升级。采用单线程,这样重量级的数据操作最好不要多线程。比如:停服更新。
目的:总之,我们要达到最终一致性!给缓存设置过期时间,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记以mysql的数据库写入库为准。
上述方案和后续落地案例是调研后的主流+成熟的做法,但是考虑到各个公司业务系统的差距,不是100%绝对正确,不保证绝对适配全部情况,请同学们自行酌情选择打法,合适自己的最好。
2、(错误方式)先更新数据库,再更新缓存
可能的异常:
- 先更新 mysql 的某商品的库存,当前商品的库存是100,更新为99个。
- 更新mysql修改为99成功,然后更新redis。
- 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。
- 上述发生,会让数据库里面和缓存redis里面数据不一致,读到脏数据。
3、(错误方式)先更新缓存,再更新数据库
违背了所有数据以数据库为准的原则。
4、(可用)先删除缓存,再更新数据库
(1)可用,但仍有异常
-
A线程先成功删除了redis里面的数据,然后去更新 mysql,此时 mysql 正在更新中,还没有结束(比如网络延时)。B突然出现要来读取缓存数据。
-
此时redis里面的数据是空的,B线程来读取,先去读 redis 里数据(已经被A线程delete掉了),此处出来2个问题:(低并发,写回旧值;高并发,缓存击穿)
2.1 B从mysql获得了旧值
B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis
获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。 -
A线程更新完 mysql,发现 redis 里面的缓存是脏数据。于是,数据是新数据,缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
-
总结流程:
(1)请求A进行写操作,删除缓存后,工作正在进行中......A还么有彻底更新完
(2)请求B开工,查询redis发现缓存不存在
(3)请求B继续,去数据库查询得到了myslq中的旧值
(4)请求B将旧值写入redis缓存
(5)请求A将新值写入mysql数据库
时间 | 线程A | 线程B | 出现的问题 |
t1 | 请求A进行写操作,删除缓存后,工作正在进行中...... | ||
t2 |
1、缓存中读取不到,立刻读mysql,由于A还没有对mysql更新完,读到的是旧值。 |
1、A还未更新完mysql,导致B读到了旧值。 |
|
t2 | 更新mysql数据库的值,over |
redis是被B写回的旧值,mysql是被A更新的新值。 |
(2)危害
如果数据库更新失败,导致B线程请求再次访问缓存时,发现redis里面没数据,缓存缺失,再去读取mysql时,从数据库中读取到旧值。
(3)解决:延迟双删
加上 sleep 的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存。然后,线程A再进行删除。所以,线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这样一来,其它线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做“延迟双删”。
(4)“延迟双删”的常见问题
- 这个删除该休眠多久呢?
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
目的就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 -
当前演示的效果是mysql单机,如果mysql主从读写分离架构如何?
还是使用双删延时策略。
只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。 -
这种同步淘汰策略,吞吐量降低怎么办?
异步线程。
5、(可用)先更新数据库,再删除缓存
(1)可用,但仍有异常
时间 | 线程A | 线程B | 出现的问题 |
t1 | 更新数据库中的值 | ||
t2 | 缓存中立刻命中,此时B读取的是缓存旧值。 | A还没有来得及更新缓存的值,导致B缓存命中读到旧值。 | |
t3 | 更新缓存的数据,over |
(2)危害
假如缓存更新失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。
(3)解决
- 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(例如使用Kafka/RabbitMQ等)。
- 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
- 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
- 如果重试超过的一定次数后还是没有成功,我们就需要向业务层发送报错信息了,通知运维人员。
四、总结
完美方案,两害相衡趋其轻的原则。优先使用先更新数据库,再删除缓存的方案。理由如下:
- 先删除缓存值再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力,严重导致打满mysql。
- 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间就不好设置。
如果使用先更新数据库,再删除缓存的方案:
如果业务层要求必须读取一致性的数据,那么我们就需要在更新数据库时,先在Redis缓存客户端暂存并发读请求,等数据库更新完、缓存值删除后,再读取数据,从而保证数据一致性。