1、情况描述
默认不存在缓存雪崩和缓存击穿情况。首先Java先查询redis,若redis中存在数据则直接返回数据。若redis中不存在数据,需要查询mysql,将mysql中的数据返回,同时需要将mysql中的数据回写进入redis中。
2、缓存双写一致性
2.1 情况讨论
(1)如果redis中有数据:需要和数据库中的值相同
(2)如果redis中无数据:数据库中的值要是最新值,且准备回写redis
缓存按照格式来分:
- 只读缓存
- 读写缓存:(1) 同步读写策略:<1>写数据库后也同步写redis缓存,缓存和数据库中的数据一致;<2>对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略。(2)异步缓写策略:<1>正常业务运行中,mysql数据变动了,但是可以再业务上容许出现一定时间后才作用于redis,比如仓库、物流系统。<3>异常情况出现了,不得不将失败的动作重新休比,有可能需要借助kafka或者RabbitMQ等消息中间件,实现重试重写。
2.2 双检加锁
问题:微服务查询redis中没有数据,而mysql数据有,为保证数据双写一致性回写redis时需要注意什么。(双检加锁是什么?如何尽量避免缓存击穿?)
解决措施:多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个互斥锁来锁住它,其他线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
2.3 数据库和缓存一致性的几种更新策略。
目的:达到最终一致性
给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记,要以mysql的数据库写入库为准。
可以停机的情况:(1) 挂牌报错,凌晨升级,温馨提示,服务降级。(2) 单线程,这样重量级的数据操作最好不要多线程。
4种不同的更新策略:
先更新数据库,再更新缓存:(1) redis回写失败(更新redis失败),这导致数据库里面和缓存redis里面的数据不一致,读到redis脏数据。(2) 多线程环境下,A、B两个线程同时对mysql进行修改,可能A先修改完数据库,B接着修改数据库,然而回写redis时是B先回写,A后回写,于是导致mysql和redis数据不一致。
先更新缓存,再更新数据库:(1) 业务上一般把mysql作为底单数据库,保证最终解释。(2) 多线程环境下,A、B两个线程同时对redis进行修改,A更新redis,B紧接着更新mysql,B先更新mysql,A接着更新mysql,导致mysql和redis数据不一致。
先删除缓存,再更新数据库:(1) A线程先成功删除了redis中的数据,然后去更新mysql,此时mysql正在更新,B线程来读取缓存的数据,B读取mysql的数据,并把旧值写回mysql。A更新完mysql,发现redis里的缓存是脏数据,此时出现数据不一致的情况。
延迟双删: 在第一次删除缓存值后,延迟一段时间再次进行删除。- 问题1:删除该休眠多少。线程A sleep的时间,就需要大于线程B读取数据再写入缓存的时间。确定时间的方法。<1> 在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的数据业务逻辑的耗时,以此为基础来进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。(这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据)。<2> 新启动一个后台监控程序,如WatchDog监控程序,会加时。
- 问题2:这种同步淘汰策略,吞吐量降低怎么办?解决方式: 启用一个CompletableFuture将第二次删除作为异步的。自行起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回,可以增加吞吐量。
先更新数据库,再删除缓存:线程A更新MySQL还未来得及删除Redis中数据,结果线程B就读走了Redis中的脏数据。
总结
在大多数情况下,优先使用先更新数据库,再删除缓存的方案(先更库——>后删存)。理由如下:
- 先删除缓存值,再更新数据库,有可能导致请求因缓存缺失而访问数据库,给数据库带来压力导致打满mysql。
- 如果业务应用中读取数据库和写缓存的时间不好估算,那么,延迟双删中的等待时间不好设置。