MySQL与Redis数据一致性

89 阅读3分钟

一、简介

引入缓存可以提高我们服务的访问效率,因为我们将数据库中的数据提前缓存了,但同样也会引发数据不一致的问题,常见的处理方案有两种:

  • 更新数据库+更新缓存
  • 更新数据库+删除缓存

我们暂时不考虑先后顺序,可以发现,这两个方案整体都不具备原子性,所以在并发情况下是一定会出现数据不一致的问题的。

二、操作都成功

我们先考虑第二步操作都成功的情况。由于方案并不具备原子性,所以要想解决并发情况下数据不一致的问题,只需要引入分布式锁即可,但是使用锁的同时,也会影响性能。下面对方案进行分析:

方案一

  • 更新数据库+更新缓存
  • 更新缓存+更新数据库

线程A和线程B,需要更新同一条数据,我们以先更新数据库,再更新缓存为例,分析如下

  1. 线程A更新数据库(X=1)
  2. 线程B更新数据库(X=2)
  3. 线程B更新缓存(X=2)
  4. 线程A更新缓存(X=1)

很明显,数据不一致了。

通过分析我们发现,方案一无论哪种顺序都会有数据一致性的问题,解决方案就是使用分布式锁,但是使用分布式锁一定好么?未必!

我们知道,使用锁会影响性能,其实还可以从缓存利用率这个角度去考虑,每次数据发生变化,我们都去更新缓存,但是并不一定在更新后就会立刻去访问数据,这也就造成了缓存的浪费!

其实我们可以根据懒加载的思想(lazy loading),将写缓存的操作延迟到访问时。我们可以在数据发生变化的时候,只去更新数据库,并且删除掉缓存,也就是方案二。

方案二

  • 删除缓存,更新数据库
  • 更新数据库,删除缓存(推荐)

对比方案一,我们将更新缓存的操作改为删除缓存,这样就能保证数据一致了么?答案同样是不行的,因为在不使用分布式锁的情况下,由于原子性的问题,数据不一致的可能性一定不为0。但是更推荐先更新数据库,再删除缓存。

因为先更新数据库,再删除缓存出现数据不一致的概率更低,因为写数据库比读数据的时间更短,另外就是我们数据必然是以数据库中的数据为基准的,所以要优先数据库。

三、如何保证两步都执行成功

通过前面的介绍,我们知道要保证数据库与缓存数据一致,可以采用先更新数据库,再删除缓存的操作,但是这是基于删除缓存的操作不失败的前提下的,如果更新数据库后,删除缓存失败,那数据必然不一致。如何保证成功删除缓存呢? 有两种常见方案:

  • 异步重试

image.png

我们可以引入消息队列作为解决方案,将第二步删除缓存的操作写入队列,由消费者从队列获取后再去操作缓存,并且由于消息队列的特性,队列中的消息成功消费之前不会丢失,并且生产者会重复投递。但是该方案可能存在写消息队列失败的情况。

  • 订阅数据库变更日志,再操作缓存

此种方案通过开源的中间件Cannal,自动将数据库变更日志投递给下游的消息队列

image.png