缓存和数据库一致性问题
随着业务量的增长,请求量越来越大,数据库的并发压力也会越来越大。一般会通过引入缓存来缓解数据库的并发压力。而这也不可避免的会带来缓存和数据库一致性问题。
如何操作缓存
1. 缓存查询
先查询缓存,如果查询失败,那么去查询DB,之后重建缓存(加上过期时间)。
2. 缓存更新
缓存的更新操作大致有下面四种方案
方案一 【先更新数据库再更新缓存】
线程A:更新数据库(第1s)——> 更新缓存(第10s)
线程B:更新数据库(第3s)——> 更新缓存(第5s)
并发场景下,每个线程的操作先后顺序不同,这样就导致请求B的缓存值被请求A给覆盖了,数据库中是线程B的新值,缓存中是线程A的旧值。
方案二 【先更新缓存再更新数据库】
线程A:更新缓存(第1s)——> 更新数据库(第10s)
线程B: 更新缓存(第3s)——> 更新数据库(第5s)
和前面一种情况相反,缓存中是线程B的新值,而数据库中是线程A的旧值。
前两种方式之所以会在并发场景下出现异常,是因为更新缓存和更新数据库是两个操作,没有办法控制并发场景下两个操作之间先后顺序,也就是先开始操作的线程先完成自己的工作。 所以需要考虑另一种删除缓存的方案。
方案三 【先删除缓存再更新数据库】
线程A: 删除缓存(第3s) ——> 更新数据库(第10s)
线程B: 查询数据库(old) ——> 插入缓存(第5s)
删除缓存的方式,解决了前面两种并发场景数据不一致的问题。但还存在上面这种场景,线程B插入缓存老数据的情况,导致缓存和数据库中的数据不一致。
先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况
在这种方案下,可以使用延时双删来解决。
- 删除缓存
- 更新数据库
- 延迟一段时间再次删除缓存
延迟双删主要是为了确保线程A在延迟期间内,线程B能够在这这一段时间完成【从数据库读取数据,再把缓存写入缓存】,然后线程A再次删除缓存。(延迟操作可以放入延迟队列)
但是具体延迟时间很难评估,所以也不建议使用该方案。
方案四 【先更新数据库再删除缓存(Cache-Aside)】
该方案解决了第三种方案的读写并发问题。但也不是绝对的一致性。
看下面的场景
线程A: 更新数据库(第3s)——> 删除缓存(第5s)
线程B: 查询数据库(old) ——> 插入缓存(第10s)
其实概率「很低」,这是因为它必须满足3个条件:
- 缓存刚好已失效
- 读请求 + 写请求并发
- 线程A更新数据库 + 删除缓存的时间,要比线程B读数据库 + 写缓存时间短
因为写数据库一般会先加锁,所以写数据库通常是要比读数据库的时间更长。这个方案,基本是可以保证数据一致性的。
如何保证缓存删除成功
由于网络抖动等原因,操作缓存时是可能失败的,就会出现数据一致的问题。
解决方案:
- 重试
引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消息队列进行失败重试。 - 订阅变更日志
更新数据库成功后会产生一条变更日志,我们就可以通过订阅变更日志来执行缓存删除及失败重试。
总结
推荐采用 [先更新数据库,再删除缓存] 的方案,并配合 [消息队列] 或 [订阅变更日志] 的方式来解决一致性问题,当然如果仍存在数据不一致的情况,还可以再加上分布式锁来处理。