数据库和缓存一致性的常用解决方案

123 阅读6分钟

数据库和缓存的组合使用是提高系统性能的常见策略。Redis 作为一种高性能的内存数据库,常被用作缓存来加速数据的访问。然而,确保数据库和 Redis 缓存之间的数据同步是一个关键问题。

一、问题背景

当应用程序频繁访问数据库时,可能会导致数据库负载过高,响应时间变长。引入 Redis 缓存可以将热点数据存储在内存中,减少对数据库的访问次数,从而提高系统的性能和响应速度。但是,如果数据库中的数据发生变化,而缓存中的数据没有及时更新,就会导致数据不一致的问题。

数据库和缓存数据不一致的根本原因就是数据库操作和缓存写入不是原子操作导致的,比如数据库写入成功了但是缓存写入失败,或者缓存写入成功了但是数据库写入失败了。还有一种情况是因为程序是并发执行的,一个线程写入,一个线程读取,因为操作不是原子的,会导致读写数据库和读写缓存是交替执行的,会导致先写入更新后的新值,然后又写入更新之前的脏数据。一般来说发生这种情况的概率很低,但是一旦发生了,因为错误数据的堆积,造成后面要修正错误的数据是非常麻烦的。

当然要保证缓存和数据库的实时完全一致是很难做到的,或者说是做不到的,我们只能做到数据的最终一致性。

二、常用解决方案:

1、MQ方案解决:

将数据更新操作推入MQ,这样所有的数据都是有迹可循的,在任何操作数据库和缓存失败的时候都能通过MQ进行重试,保证操作的成功。这种方案最稳妥,但是在大部分情况下显得太重。

2、先写数据库,再删除缓存:

image.png

首先更新DB,然后再删除redis缓存,当有读请求时,发现redis没有数据,就从DB读取数据,然后写入到redis缓存。在大部分情况下这样做是没有问题的,但是在更新DB成功后,如果删除redis缓存数据时失败了,这个时候redis缓存中的数据就是旧的脏数据,如果之前缓存在redis中的数据是永久的,就会导致这个数据永久的脏下去。

如果在更新DB之前先删除缓存能不能解决这个问题了,我们来看下面这种情况。

3、先删除缓存,再写数据库,最后再删除缓存:

image.png 这里不管哪一步失败都不会都不会导致缓存是脏数据,表面上看好像是解决了上面那种情况的弊端,但问题坏就坏在程序是并发执行的:

1)、在第一步删除redis成功后,刚好有个读请求到达,读请求发现缓存没数据后就从数据库读取并更新到缓存,然后再执行第二步的更新DB,但是在第三步删除redis时却失败了,这样缓存里面存储的就是脏数据,而且会脏很长一段时间。

2)、我们的数据库往往是主备结构式的,写数据时先写入到主数据库,然后再同步到备份数据库,读数据时是从备份数据库读取的,而主数据库的数据同步到备份数据库是有一定的时间差的,在第三步删除了redis数据之后,主备同步之前有读请求到达,就会读取到脏数据并存入缓存,造成数据库和缓存的不一致,而且会脏很长一段时间。

4、延时双删除:

延时双删是业界使用的非常普遍的一种解决数据库和缓存一致性问题的方法,其操作步骤如下: image.png

这个策略能解决第三种方法中主备数据同步导致脏数据的问题,但是依然存在问题:

1)、延时删除的时间如果设置太短会出现主备同步没有完成,脏数据依然存在。如果设置过长,会导致在这段时间内的请求打到DB上,会增加系统的响应时间,降低系统性能;

2)、更新DB之后,如果后面两步的删除缓存失败了,因为网络或者服务器短时间的故障,也会导致缓存不一致的情况;

5、双写方案:

image.png

在更新之前可以先将数据临时写入缓存,5分钟后失效,然后更新DB,最后将数据写入缓存一个较长时间,比如2小时或者其他时间值。

这种方案不能保证实时绝对的缓存一致性,但是能保证最终一致性,如果第一步成功后面的失败,则数据最多只脏5分钟,如果第二步成功第三步失败了,则5分钟后缓存数据自动失效,读请求发现缓存没数据就会读取DB并更新到缓存。

在有些情况下,第一步的临时写入缓存数据可能有些困难,比如要经过一个复杂的计算,而计算又依赖于数据库的内容,因此这一步也可以改成先删除数据。

这种方案就是利用的缓存自动失效来保证最终一致性的,因此任何时候写入缓存不能是永久的,否则一旦出现脏数据就有可能永久的脏下去了。

这种方案的缺点就是一旦有某个步骤执行失败,会导致一段时间内出现脏数据,因为在对缓存数据一致性要求比较高的场合并不适用。

三、总结:

数据库和 Redis 缓存同步是提高系统性能和数据一致性的关键问题。在实际应用中,并没有永远的银弹,也没有一套放之四海而皆准的策略,上面的五种方案各有优缺点,需要根据具体的业务需求和系统架构来选择合适的同步策略,并注意处理各种异常情况,以确保系统的稳定性和数据的一致性。