随着业务量的增长,请求量越来越大,数据库的并发压力也会越来越大。一般会通过引入缓存来缓解数据库的并发压力。而这也不可避免的会带来缓存和数据库一致性问题。

如何操作缓存

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插入缓存老数据的情况,导致缓存和数据库中的数据不一致。
先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况

在这种方案下,可以使用延时双删来解决。

  1. 删除缓存
  2. 更新数据库
  3. 延迟一段时间再次删除缓存

延迟双删主要是为了确保线程A在延迟期间内,线程B能够在这这一段时间完成【从数据库读取数据,再把缓存写入缓存】,然后线程A再次删除缓存。(延迟操作可以放入延迟队列
但是具体延迟时间很难评估,所以也不建议使用该方案。

方案四 【先更新数据库再删除缓存(Cache-Aside)】

该方案解决了第三种方案的读写并发问题。但也不是绝对的一致性。
看下面的场景
线程A: 更新数据库(第3s)——> 删除缓存(第5s)
线程B: 查询数据库(old) ——> 插入缓存(第10s)

其实概率「很低」,这是因为它必须满足3个条件:

  1. 缓存刚好已失效
  2. 读请求 + 写请求并发
  3. 线程A更新数据库 + 删除缓存的时间,要比线程B读数据库 + 写缓存时间短

因为写数据库一般会先加锁,所以写数据库通常是要比读数据库的时间更长。这个方案,基本是可以保证数据一致性的。

如何保证缓存删除成功

由于网络抖动等原因,操作缓存时是可能失败的,就会出现数据一致的问题。
解决方案:

  1. 重试
    引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消息队列进行失败重试。
  2. 订阅变更日志
    更新数据库成功后会产生一条变更日志,我们就可以通过订阅变更日志来执行缓存删除及失败重试。

总结

推荐采用 [先更新数据库,再删除缓存] 的方案,并配合 [消息队列][订阅变更日志] 的方式来解决一致性问题,当然如果仍存在数据不一致的情况,还可以再加上分布式锁来处理。