只要使用Redis做缓存,就必然存在缓存和DB数据一致性问题。若数据不一致,则业务应用从缓存读取的数据就不是最新数据,可能导致严重错误。比如将商品的库存缓存在Redis,若库存数量不对,则下单时就可能出错,这是不能接受的。
1 什么是缓存和DB的数据一致性
一致性
包含如下情况:
- 缓存有数据
缓存的数据值需和DB相同 - 缓存无数据
DB必须是最新值
不符合这两种情况的,都属于缓存和DB数据不一致。
2 缓存的读写模式
根据是否接收写请求,可将缓存分成读写缓存和只读缓存。
2.1 读写缓存
若要对数据进行增删改,需要在Cache进行。
同时根据采取的写回策略,决定是否同步写回DB:
2.1.1 同步直写
写缓存时,也同步写数据库,缓存和数据库中的数据一致。
2.1.2 异步写回
写缓存时不同步写DB,等到数据从缓存中淘汰时,再写回DB。使用这种策略时,若数据还没有写回DB,缓存就发生故障,则此时,DB就没有最新数据了。
所以,对于读写缓存,要想保证缓存和DB数据一致,就要采用同步直写。若采用这种策略,就需同时更新缓存和DB。所以,要在业务代码中使用事务,保证缓存和DB更新的原子性,即两者:
要么一起更新
要么都不更新,返回错误信息,进行重试
否则,我们无法实现同步直写。
有些场景下,我们对数据一致性要求不高,比如缓存的是电商商品的非关键属性或短视频的创建或修改时间等,则可以使用异步写回。
2.2 只读缓存
- 新增数据
直接写DB - 删改数据
删改DB,删除只读缓存中的数据
这样应用后续再访问这些增删改的数据时,由于Cache无数据 =》缓存缺失。
此时,再从DB把数据读入Cache,这样后续再访问数据时,直接读Cache。
下面我们针对只读缓存,看看具体会遇到哪些问题,又该如何解决。
3 新增数据
数据直接写到DB,不操作Cache。此时,Cache本身无新增数据,而DB是最新值,所以,此时缓存和DB数据一致。
4 删改数据
此时应用既要更新DB,也要删除Cache。这俩操作若无法保证原子性,就可能出现数据不一致。
4.1 先删Cache,再更新DB
4.2 先更新DB,再删除Cache
综上,在更新DB和删除Cache时,无论这俩操作谁先执行,只要有一个操作失败了,就会导致客户端读到旧值。
那怎么办?好像怎么都会导致数据不一致?
5 数据不一致的解决方案
5.1 无并发
重试
将:
要删除的Cache值
或要更新的DB值
暂存到MQ。
当应用删除Cache或更新DB:
成功
把这些值从MQ去除,避免重复操作,这时即可保证DB、Cache数据一致性。
失败
重试。从MQ重新读取这些值,然后再次进行删除或更新。若重试超过一定次数,还没成功,就向业务层发送报错信息。
在更新数据库和删除缓存值的过程中,其中一个操作失败了:
先更新DB,再删除缓存
- 若删除缓存失败,再次重试后删除成功
- 其它情况不再赘述。
即使这两个操作第一次执行时都没有失败,当有大量并发请求时,应用还是有可能读到不一致的数据。
按不同的删除和更新顺序,分成两种情况来看
5.2 高并发
5.2.1 先删除Cache,再更新DB
假设现有时刻t1< t2 < t3,线程 T1、T2:
此时,该怎么办呢?
解决方案
T1更新完DB后,让它sleep一段时间,再删除缓存。
为什么要sleep一段时间呢?
为了让T2能够先从DB读数据,再把缺失数据写入缓存,然后,T1再进行删除。
所以,T1 sleep的时间,就需要大于T2读取数据再写入缓存的时间。
这个时间怎么确定?
在业务程序运行时,统计下线程读数据和写缓存的操作时间,以此为基础来进行估算。
这样,当其它线程读数据时,会发现缓存缺失,所以会从DB读最新值。因为这个方案会在第一次删除缓存值后,延迟一段时间再次进行删除,所以称为“延迟双删”。
redis.delKey(X) db.update(X) Thread.sleep(N) redis.delKey(X)