数据库和缓存Redis的一致性问题无论在面试还是平时使用的时候都是一个很常见且关键的问题这里我写一篇文章记录一下

Cache-Aside

旁路缓存模式,应用最为广泛的一种缓存策略。

读请求

首先查询redis查看是否存在,如果存在的话直接返回数据,如果不存在查询数据库,然后写入缓存

写请求

直接更新数据库然后删除缓存

为什么先更新数据库,而不是先删除缓存

因为在并发的情况下如果线程A先去删除缓存然后线程B去查询了数据K,此时线程B查询了数据K,并且将数据写入了缓存,然后线程A才去更新了数据库,然后此时我们的缓存就会是一个旧数据所以不能先删除缓存

在这种情况下怎么解决缓存一致性的问题呢

由于先删除缓存后更新数据会有缓存脏数据,所以业内又提出了延时双删,也就是在删除数据之后,更新数据库,防止别的线程读取了数据产生了脏缓存延时之后再进行一次删除,这个延时的时间一般稍微大于业务的读时间耗时,但是无论这个值如何预估,都很难和读请求的完成时间点准确衔接,这也是延时双删被诟病的主要原因。

先更新数据之后再删除缓存就没有问题了么

肯定不是的,在读写并发的时候也是会出现数据不一致的问题的,下面列举一种场景

  1. 线程A读取数据K
  2. 线程B更新数据K
  3. (在线程B)更新了数据K的时候,现在并没有缓存所以我们不需要去删除掉缓存
  4. 线程A读取的旧数据K就将旧数据写入到Redis缓存中了

此时就会造成数据不一致,单无论是缓存双删还是先更新数据后删除缓存都会有可能删除失败

删除数据失败的补偿机制

在上边说到的所有机制里边都会发生缓存不一致的问题,所以针对可能出现删除失败问题,主要有一下几种补偿机制

消息队列进行重试

原因是因为如果我们使用另外一个线程的话,如果在执行失败的线程中一直重试,还没等执行成功,此时如果项目重启了,那这次重试请求也就丢失了,那这条数据就一直不一致了。所以,这里我们必须把重试或第二步操作放到另一个服务中,这个服务用消息队列最为合适。

  • 消息队列保证可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
  • 消息队列保证消息成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的场景)

但是无论是使用消息队列或者去异步的进行重试都会去触及业务层所以我们在为了不干扰业务的时候去进行同步可以监听binlog

使用maxwell监听binlog

使用maxwell监听binlog,在数据发生变化的时候生成JSON格式的消息,作为生产者发送给mq然后通过mq去根据这条数据删除对应的缓存

订阅变更日志,目前也有了比较成熟的开源中间件,例如阿里的 canal,使用这种方案的优点在于:

  • 无需考虑写消息队列失败情况:只要写 MySQL 成功,binlog 肯定会有
  • 自动投递到下游队列:canal 自动把数据库变更日志「投递」给下游的消息队列

Write/Read-Through

核心思想:应用需要操作数据时只与缓存组件进行交互;缓存数据不会过期

Read-Through应用查询缓存是否存在,存在则直接返回,不存在则由缓存组件去数据库加载数据

Write-Through,先查询数据是否存在,存在则先更新缓存再去由缓存组件更新数据库,如果不存在的话有两种办法1. 直接更新数据库 2. 更新缓存然后由缓存组件去同步数据库

存在的问题:

  • 因为应用操作数据时只与缓存组件交互,相对于Cache-Aside而言数据不一致的概率要低一些。
  • 因为此模式下缓存没有过期时间,所以缓存的使用量会非常大。

Write-Back