Redis缓存与数据库一致性问题

在这里插入图片描述

1.缓存的引入

1.1 概述

在分布系统中,一般会使用Redis缓存来提高数据读写性能,减轻数据库的访问压力,但是Redis与数据库分属于不同的系统,就可能出现缓存与数据库中数据不一致的问题。
在小型业务系统中,由于并发量不高、数据量小,因此,一般直接操作数据库即可。随着业务的增长,并发量和数据量也会随之增加,频繁访问数据可能会影响数据库的读写性能,因此,可以通过引入缓存来提高数据读写性能。具体做法是将数据库中的数据全量加载到Redis缓存中,读取数据时从缓存中读取。

如图所示:
在这里插入图片描述
处理流程

1)将数据库中的全量数据预加载到Redis缓存(不设置缓存数据过期时间)中。

2)读数据时,先从Redis缓存中读取,如果缓存中不存在,则从数据库中读取。

3)写数据时,将数据写入数据库。

4)通过异步任务定时将数据库中的数据更新到Redis缓存中。

该方案的优点是性能高,缺点:

1)缓存利用率低,缓存数据未设置缓存过期时间,会存在大量访问率低或者无效的数据。

2)数据不一致,采用异步任务定时更新缓存,会因缓存数据未及时更新而读取到旧数据。

针对以上缺陷,很自然就想到以下解决方案,即:

  • 设置缓存数据过期时间。

  • 及时更新缓存数据。

1.2 缓存利用率问题

要想提高缓存数据的利用率,就需要动态更新缓存,及时淘汰无效的缓存数据。具体做法是设置缓存数据过期时间,并及时更新缓存数据。

如图所示:

处理流程:

1)数据更新时,及时更新缓存数据。

2)更新缓存数据时,设置缓存数据过期时间。

这样,无效数据会随着时间的推移逐渐被淘汰,热点数据也会得到及时更新。

2 数据一致性问题

要想保证缓存数据与数据库数据的一致性,就需要更新数据库中的数据时,同步更新缓存数据。常见的缓存数据更新策略有两种:

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

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

2.1 更新数据库+更新缓存

2.1.1 问题概述

根据数据库与缓存的操作顺序,可分为两种方案:先更新缓存,再更新数据库。 先更新数据库,再更新缓存。
正常情况,二者没有差别,都能保证缓存数据与数据库数据的一致性。数据一致性问题主要发生在第一步执行成功,第二步执行失败的场景。

先更新缓存,再更新数据库
在更新缓存成功、更新数据库失败的场景中,读取到的数据为更新后的最新值,但数据库中存储的是更新前的旧值,造成数据一致性问题。一旦缓存失效,就会从数据库中加载得到更新前的旧值,对业务造成影响。

先更新数据库,再更新缓存
在更新数据库成功、更新缓存失败的场景中,读取到的数据为更新前的旧值,而数据库中存储的是更新后的最新值,造成数据一致性问题。缓存失效后,会从数据库中加载得到更新后的最新值,对业务造成短暂影响。

2.1.2 高并发场景

线程A和线程B对同一条数据X进行操作时,会存在以下情况:
先更新缓存、再更新数据库:

  1. 线程A更新X的缓存值为a,由于网络延迟等原因导致线程A未及时将X对应的缓存值a更新到数据库中。
  2. 线程B更新X的缓存值为b,并将X对应的缓存值b更新到数据库中
  3. 线程B更新完成后,线程A恢复并将X对应的数据库值b更新为a。造成X的缓存值b与数据库值a不一致。

先更新数据库、再更新缓存:

  1. 线程A更新X的数据库值为a,由于网络延迟等原因导致线程A未及时将X对应的数据库值a更新到缓存中。
  2. 线程B更新X的数据库值为b,并将X对应的数据库值b更新到缓存中
  3. 线程B更新完成后,线程A恢复并将X对应的缓存值b更新为a。造成X的数据库值b与缓存值a不一致。

由此可见,无论是先更新数据库还是先更新缓存,都有可能造成缓存数据与数据库数据的不一致,此外,每次更新数据都同步更新缓存,不仅会影响Redis中其他数据的写入性能,还会造成内存资源的浪费(如:数据更新到缓存,但是很少被使用)。因此,一般不建议使用更新数据库+更新缓存的策略。

2.2 更新数据库+删除缓存

2.2.1 问题概述

与更新数据库+更新缓存类似,根据数据库与缓存的操作顺序,可分为两种方案:先删除缓存,再更新数据库。先更新数据库,再删除缓存。正常情况,二者都能保证缓存数据与数据库数据的一致性。数据一致性问题主要发生在第一步执行成功,第二步执行失败的场景。

先删除缓存,再更新数据库
在删除缓存成功、更新数据库失败的场景中,读取到的数据为数据库中未更新成功的旧值,造成数据一致性问题(即:期望值为更新后的最新值,返回值为更新前的旧值)。对业务造成影响。

先更新数据库,再删除缓存

在更新数据库成功、删除缓存失败的场景中,读取到的数据为更新前的旧值,而数据库中存储的是更新后的最新值,造成数据一致性问题。缓存失效后,读取数据时会从数据库中加载得到更新后的最新值,对业务造成短暂影响。

2.2.2 高并发场景

当线程A和线程B对同一条数据X进行操作时,会存在以下情况:
先删除缓存、再更新数据库:
1)线程A删除X的缓存值(旧值1),由于网络延迟等原因导致线程A未及时将X对应的新值2更新到数据库中。
2)线程B读取X的缓存值时未命中(已被线程A删除),从数据库中读取X的值(旧值1)并将其更新到缓存中。
3)线程B更新完成后,线程A恢复并将数据库中的值(旧值1)更新为新值2。造成X的缓存值与数据库值不一致
先更新数据库、再删除缓存:
1)线程A更新X的数据库值为新值2,由于网络延迟等原因导致线程A未及时删除X对应的缓存值(旧值1)。
2)线程B读取X的缓存值(造成X的数据库值(2)与缓存值(1)不一致)。

由此可见,无论是先更新数据库还是先删除缓存,都有可能造成缓存数据与数据库数据的不一致。一般建议使用先更新数据库、再删除缓存的策略,原因是先删除缓存、再更新数据库策略有可能因缓存缺失而直接访问数据库(即:缓存穿透),给数据库带来压力。

3 数据一致性问题解决方案

通过前面的场景分析,无论是更新数据库+更新缓存还是更新数据库+删除缓存,只要第二步执行失败,就有可能导致缓存数据与数据库数据的不一致。因此,解决数据不一致问题的关键是保证第二步执行成功,常用的解决方案是:异步重试。

异步重试目前比较主流的方案有两种:
基于MQ的异步重试方案
基于Canal+MQ的异步重试方案。
基于MQ的异步重试方案
如图所示:
在这里插入图片描述
处理流程:

1)写数据时,先将数据写入数据库中,写入成功后通过发送消息的方式将数据写入MQ中进行备份。

2)更新/删除到缓存时,如果更新/删除缓存成功,则删除MQ中的备份数据;如果更新/删除缓存失败,则使用MQ中的备份数据进行异步重试。

基于Canal+MQ的异步重试方案
如图所示:
在这里插入图片描述

处理流程:

1)写数据时,先将数据写入数据库中,写入成功后通过binlog(即:Canal组件)方式将数据写入MQ中进行备份。

2)更新/删除到缓存时,与基于MQ的异步重试方案一致。

分布式锁方案

在分析更新数据库+更新缓存的高并发场景时,会存在因多个线程并发执行的顺序而导致数据不一致的问题。这种情况可以通过加分布式锁来保证同一时刻只有一个线程对数据进行操作的方式来解决。

处理流程:

1)写数据时,通过加分布式锁的方式保证同一时刻只有一个线程操作数据库和缓存。

2)将其他未获得分布式锁的线程操作加入到队列中,等待获得分布式锁的线程释放锁。

注意:使用加分布式锁的方式处理数据一致性问题时,会对处理性能造成一定影响。

延时双删方案

在分析更新数据库+删除缓存的高并发场景时,以下两种情况有可能造成数据不一致:

1)先删除缓存、再更新数据库:会因多个线程并发执行的顺序而导致数据不一致的问题。

2)先更新数据库、再删除缓存:会因主从延迟而导致数据不一致的问题。

以上两种情况可以通过延迟双删的方式来进行处理。

处理流程:

问题一:线程A删除缓存、更新完数据库后,先休眠一小段时间,确保线程B能够先从数据库读取数据,再把缺失的数据写入缓存,然后线程A再次删除缓存。后续其它线程读取数据时,未命中缓存,会从数据库中读取最新值。

问题二:线程A删除缓存、更新完数据库后,向消息队列中发送延时消息,待主从同步完成后,消费延时消息再次删除缓存。

延时时长设置:

问题一:延迟时间大于线程B读取数据库+写入缓存的时间。

问题二:延迟时间大于「主从复制」的延迟时间。

具体延时时长很难评估,一般根据测试结果和过往经验值进行综合评估,如:延时1~5秒。

强一致性策略

以上数据一致性问题解决方案只能保证数据的最终一致性。要想做到强一致,最常见的方案是使用2PC、3PC、Paxos、Raft这类一致性协议,但它们的性能往往比较差,而且实现方案复杂,还需要考虑各种容错问题。如果业务要求必须读取数据的强一致性,可以采用加分布式锁的方式实现,处理流程->上面的「分布式锁方案」。

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值