缓存双写一致性之更新策略
- 缓存双写一致性的理解
- 缓存按照操作来分,有细分两种
- 只读缓存
- 读写缓存
- 数据库和缓存一致性的几种更新策略
- 挂牌报错,凌晨升级
- 目的
- 三种更新策略(写操作要以数据库为准)
- 1. 先更新数据库,再更新缓存
- 案例
- 结论
- 2. 先删除缓存,再更新数据库
- 案例
- 异常情况
- 解决方案(延时双删策略)
- 双删方案面试题
- 1. 这个删除该休眠多久呢
- 2. 当前演示的效果是mysql单机,如果mysql主从读写分离结构如何?
- 3. 这种同步淘汰策略,吞吐量降低怎么办?
- 3. 先更新数据库,再删除缓存
- 业务指导思想
- 解决方案(重试机制+引入MQ)
- 重试机制
- 总结
缓存双写一致性的理解
- 如果redis中有数据,需要和数据库中的值相同
- 如果redis中没有数据,数据库中的值要是最新值
缓存按照操作来分,有细分两种
只读缓存
读写缓存
- 同步直写策略: 写缓存时同步写数据库,数据库和缓存中的数据一致
- 对于读写缓存,要想保证数据库和缓存中的数据一致,就要采用同步直写策略
数据库和缓存一致性的几种更新策略
挂牌报错,凌晨升级
单线程,这种重量级的操作最好不要多线程
目的
总之,我们要达到最终一致性
给缓存设置过期时间,是保证最终一致性的解决方案。
我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记以mysql的数据库写入库为准。
三种更新策略(写操作要以数据库为准)
1. 先更新数据库,再更新缓存
案例
- 先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。
- 先更新mysql修改为99成功,然后更新redis
- 此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面是100
- 上述情况发生,会让数据库里面和缓存redis里面数据不一致,读到脏数据。
结论
可以用,先更新数据库,再更新缓存,但是如果数据库更新成功,redis异常更新失败,会导致从redis中读取到了老的脏数据。
2. 先删除缓存,再更新数据库
案例
- A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时),B突然出现要来读取缓存数据。
- 此时redis里面的数据是空的,B线程来读取,先去读redis里面数据(已经被A线程delete掉了),此处出来2个问题:
- B从mysql中获得了旧值
- B线程发现redis中乜有(缓存缺失)马上去mysql里面读取,从数据库里面读出来的是旧值。
- B会把获得的旧值写回redis
- 获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。
- A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了。
两个并发操作,一个是更新操作,另一个是查询操作,A更新操作删除缓存后,B查询操作没有命中缓存,B线程把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据时脏的,而且还一直这样脏下去了
异常情况
低并发: 旧值被写回redis中
高并发: 缓存击穿
解决方案(延时双删策略)
线程A加上sleep的这段时间,就是为了让线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后,线程A再进行删除。所以,线程Asleep的时间,就需要大于线程B读取数据再写入缓存的时间。这样一来,其他线程读取数据时,会发现缓存缺失,所以会从数据库中读取最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以我们也把它叫做"延迟双删"。
双删方案面试题
1. 这个删除该休眠多久呢
线程Asleep的时间,就需要大于线程B读取数据再写入缓存的时间。
这个时间怎么确定呢?
在业务程序运行的时候,统计下线程读数据和写缓存的操作时间,自行评估自己的项目的读数据业务逻辑的耗时,以此为基础进行估算。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上加百毫秒即可。
这么做的目的,就是确保请求结束,写请求可以删除读请求造成的缓存脏数据
2. 当前演示的效果是mysql单机,如果mysql主从读写分离结构如何?
- 请求A进行写操作,删除缓存
- 请求A将数据写入数据库了
- 请求B查缓存发现,缓存没有值
- 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
- 请求B将旧值写入缓存
- 数据库完成主从同步,从库变为新值 上述情形,就是数据不一致的原因。还是使用延时双删策略
只是睡眠时间修改为在主从同步的延时时间基础上,再加几百毫秒。
3. 这种同步淘汰策略,吞吐量降低怎么办?
第二次删除使用异步线程,一异步删除。这样,写的请求就不用沉睡一段时间后,再返回。这么做,加大吞吐量。
3. 先更新数据库,再删除缓存
假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读到的是缓存旧值。
业务指导思想
- 老外论文
- 知名社交网站facebook也在论文《Scaling Memcache at Facebook》书中提出
- 我们上面的canal也是类似的思想
订阅binlog程序在mysql中有现成的中间件叫做canal,可以完成订阅binlog日志的功能。
解决方案(重试机制+引入MQ)
流程:
- 更新数据库数据
- 数据库会将操作信息写入binlog日志当中
- 订阅程序提取出所需要的数据以及key
- 另起一段非业务代码,获得该信息
- 尝试删除缓存操作,发现删除失败
- 将这些信息发送至消息队列
- 重新从消息队列中获得改数据,重试操作。
重试机制
- 可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中(六使用Kafka/RabbitMQ等)。
- 当程序没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
- 如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,一面重复操作,此时,我们也可以保证数据库和缓存的数据一致了,否则还需要再次进行重试
- 如果重试超过的一定次数后还是没有成功,我们就需要项业务层发送报错信息了,通知运维人员。
总结