【分布式系统-缓存系统架构设计疑难点系列】mysql、redis数据一致性怎么解决?

1、需求背景

     在高并发的业务场景下,数据库大多数情况都是用户并发访问最薄弱的环节。所以,就需要使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库。读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题

      一般的项目大家都是不会怎么在意数据库和缓存的数据一致性的,都是一个api接口先查询redis,有则返回,没有则表示数据过期了,去数据库取,然后存入redis。也即是直接通过设置缓存数据的有效时间来做的简单最终一致性。

       上面的针对大量读请求,而且能接收脏读,倒没什么问题(其实也有,如果并发过高,可能会出现缓存穿透问题,但要是项目确实需要处理数据一致性呢?

       也就是要保证数据库的数据和缓存中的数据是一致的,一般你可能看到过或者想到的是两种:先删除缓存再更新数据库;先更新数据库再删除缓存。

为什么会有顺序差异呢(删除缓存和更新数据库)?

        因为并发会引入“间断性”、“不可封闭性”、“不可再现性”问题(也即失去了“顺序性”)。当然你如果是把“加锁”处理(即加锁删除缓存和更新数据库),那么这两个操作的步骤顺序无所谓。但加锁就意味着并发不高(自损),所以一般都不会这么做。

那这两个步骤顺序不一致,我们选择哪一种呢?

2、思考

你可能稍加思索,会说“先删除缓存,再更新数据库”

1,删除缓存;2,更新数据库。

     假设1成功2失败了,则缓存没数据了,数据库是旧的。那么此时来了一个读请求,读缓存miss(缓存已被删了),则去读数据库读到旧数据,然后再更新缓存(旧数据),数据一致(缓存和数据库中都是旧数据)。

如果是先更新数据库再删除缓存

1,更新数据库;2,删除缓存。

       假设1成功2失败了,则数据库是新数据,缓存是旧数据。那么此时来了一个读请求,读缓存命中(读到旧数据),不用再去读数据库了,那么读到的是旧数据,而且数据不一致(缓存是旧数据,数据库是新数据)。

那这么分析下来,是该选择“先删除缓存,再更新数据库”。

因为只是会一次缓存miss而已。

      但如果是读请求并发非常高呢?而且缓存本来就是针对读多写少的场景(如果写多,那缓存就没啥意义了)。我们看下下面场景:当请求A写数据时,此时读请求并发非常高。

1,请求A删除缓存

2,请求B读数据,读缓存miss(因为请求A已删除缓存)

3,请求B到数据库读取数据(此时其实读取到的是旧数据)

4,请求B更新缓存(将数据库的旧数据读取放入到缓存)

5,请求A将新数据写入到数据库

      这下数据不一致了(缓存中为旧数据,数据库为新数据),如果这个缓存数据没有过期时间的话,会一直不一致状态(当然一般不会这么神操作)。

       那我们再换成“先更新数据库,再删除缓存”,看下面场景:当请求C写数据时,此时读请求并发非常高,而且刚好该数据的缓存过期了(主要是为了模拟出读请求缓存miss)。

1,缓存数据刚好过期

2,请求D读数据,读缓存miss(上面缓存数据刚好过期或者已过期)

3,请求D从数据库读取数据(读取到旧数据)

4,请求C向数据库写数据(写入新数据)

5,请求C删除缓存

6,请求D更新缓存(将旧数据放入缓存)

      这下数据不一致了(缓存为旧数据,数据库为新数据),但注意一个问题,出现这种场景的话,步骤4一定要比步骤3更快才会出现步骤5比步骤6先执行。也即是“请求C向数据库写数据”要比“请求D从数据库读数据”快才行,也就是说更新数据库要比读数据库数据快,你觉得这概率大吗?mysql读数据不加锁(mvcc方式快照读),写数据加锁(mdl锁,行锁,gap锁,next锁),为的就是提升读写并发,读一般肯定比写快(不然为嘛搞读写分离这些事)。那么也就是说上面的场景出现的概率非常低。而“先删除缓存,再更新数据库”在读高并发下出现不一致场景的概率会非常的高。

所以,当读多写少(读高并发)时,mysql和redis数据一致性解决采用“先更新数据库,再删除缓存”会合适一点。

3、改进

       问题又来了,我们采用“先更新数据库,再删除缓存”还是会出现不一致性问题,而且也会出现脏读(有请求在更新数据库删除缓存期间会拿到旧数据【可能从缓存中拿到可能从数据库拿到】)。

      那怎么办?而且没有绝对的数据一致性解决方案吗?

      绝对的数据一致性,没法,分布式cap,本身带缓存就是ap,但我们还是可以优化的。

双删缓存

1,先更新数据库

2,删除缓存

3,隔x秒后再次删除缓存

      恩?干嘛x秒后又删除一次缓存?这是尽量减小有请求读取到旧数据的影响,那具体多少秒?这个看业务场景,根据实际耗时时间来,也就是从更新数据库开始到X时间可能会有请求读到旧数据。

还有其他方案吗?

有啊,订阅数据库binlog结合mq消息队列处理。

     读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

      其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

       这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

你可能感兴趣的:(【分布式系统-缓存系统架构设计疑难点系列】mysql、redis数据一致性怎么解决?)