Redis 缓存和数据库一致性讨论

缓存模型

根据 缓存是否接收写请求 可以将缓存分为 只读缓存 读写缓存

只读缓存

读策略:所有的 读请求直接发到缓存,缓存中不存在再去数据库查询数据并加载到缓冲中。

写策略:所有的 写请求直接发送到后端数据库,针对删除和修改,数据可能存在于缓存中,因此还需要 删除缓存中的数据。因为写请求是直接发送到后端数据库的,因此 数据的最新状态存在数据库中,不会发生数据的丢失。

读写缓存

读请求和写请求都发送到缓存,即直接在缓存中进行增删改查。

直接操作缓存可以极大地提升系统的响应性能,当然,因为数据的最新状态位于缓存中,可能出现在将数据同步到数据库之前就发生异常,导致数据丢失,所以存在一定的业务风险。

如何将数据写回到数据库,分为 同步直写异步写回 两种策略。

同步直写

写请求 同时发给缓存 和 后端数据库。这样即使缓存数据丢失,数据库的数据也是最新的,保证了数据的可靠性。当然,此时应用的响应又因为数据库的操作而变慢了。

异步写回

写请求只发到缓存,然后直接返回,等到缓存中的数据要被淘汰时才写回数据库,或者通过异步任务定时将数据写回到数据库。这种写回策略提高了系统响应性能,但是无法保证数据的准确性。

一致性问题

读写缓存一致性讨论

对于 异步写回策略 而言,写请求只发送到缓存,再异步写到数据库,一旦缓存发生故障就可能造成数据丢失,数据准确性都保证不了,更谈不上一致性了。

再来看 同步直写策略,同步直写包含 更新数据库更新缓存 两个步骤,按执行先后顺序分为:

  • 先更新数据库,后更新缓存
  • 先更新缓存,后更新数据库

无论哪种方案,只要 2 个步骤不在一个事务中进行,就可能出现 部分失败 的情况,造成数据库和缓存不一致。

针对部分失败提出的解决方案一般有 通过 MQ 异步重试 或者 订阅数据库变更日志

如果考虑 并发,比如 A 和 B 两个请求同时对记录 X 作更新, 即对数据的操作存在 写写并发, 分析两种方案可能存在的执行顺序:

可以看到,不管哪种方案, X 的值在缓存和数据库中都可能出现不一致的情况。

如果要保证数据的一致性,需要采用互斥锁来保证同一时间只有一个线程可以对数据进行操作,而在分布式应用中还需要使用分布式锁,但是,引入锁就会不可避免的给性能带来影响。

只读缓存一致性讨论

只读缓存在进行数据的删除和更新时包含 2 个步骤,按执行先后顺序分为:

  • 先更新数据库,后删除缓存
  • 先删除缓存,后更新数据库

同样,我们先考虑 部分失败 的情况:

如果先更新数据库成功,后删除缓存失败,下次请求查询缓存数据是旧数据,数据不一致;

如果先删除缓存成功,后更新数据库失败,下次请求先查缓存,缓存不存在,去数据库查询并更新缓存,此时缓存和数据库数据是一致的。

再来看下存在 并发 的情况,假如 A 和 B 两个请求同时对记录 X 作读写操作,

如果 先变更数据库,后删缓存,可能出现:

  1. 请求 A 读取缓存,数据 X 不存在
  2. 请求 A 读取数据库,得到值 X = 1
  3. 请求 B 更新数据库 X = 2
  4. 请求 B 删除缓存
  5. 请求 A 将 X = 1 值写入缓存

最终 在缓存中 X = 1,而在数据库中 X = 2。

当然,由这种竞态导致的不一致是很少见的,因为 请求 A 和 请求 B 同时开始的情况下,A 读数据库会快于 B 写数据库,因此很难出现上述的那样 步骤 5 出现在 步骤 3 和 步骤 4 的后面。

如果 先删缓存,后变更数据库,可能出现:

  1. 请求 A 要更新 X = 2,先删除缓存
  2. 请求 B 读缓存,发现不存在
  3. 请求 B 从数据库中读取 X = 1
  4. 请求 B 写入缓存 X = 1
  5. 请求 A 写入数据库 X = 2

最终 在缓存中 X = 1,而在数据库中 X = 2。

可以看到,在 读 + 写 并发 的场景下,也是没法保证数据一致性的。

业界给出的方案:缓存延迟双删。

先删缓存,再变更数据库 来讲,在请求 A 的 将 数据写入数据库后,程序 sleep 一小会,然后再执行一次缓存删除,这样就可以消除请求 B 写回到缓存中的旧数据。

当然, sleep 一小会,这个时间其实是没有标准可循的,比较可靠的方式是在业务程序运行的时候,通过统计线程读数据和写缓存的操作时间来进行估算。

小结

至此,我们讨论了两种缓存模式下的数据一致性问题,可以看到不管采用哪种模式,都可能出现数据不一致的情况,虽然有相应的方案来尽量保证数据一致性,但是要么方案太重,要么会降低系统响应性能,因此,在系统设计时要权衡一下是否值得去保证这种数据的 强一致性

如果不考虑强一致性,在实际应用中,可以通过 对缓存数据加上过期时间 来达到数据的最终一致性(即使缓存数据有问题,也会在过期时间到达时被删除掉,达到 最终一致)。同时,对缓存数据加过期时间还可以避免过多的垃圾数据占用缓存空间,一举两得。

案例分享

从可能性分析来看,不管以何种方式使用缓存,都在一定场景下存在一致性问题,并且采用不同的方案对业务的影响程度也不一样。既然没有一个完美的方案,那势必要根据实际的场景出发,选择对业务影响最小的那一个。

这里也分享一下,本人实际使用的案例:

业务简介

支付业务,每一笔支付请求中,都需要查询 商户信息 客户信息,缓存商户信息和客户信息可以很大程度上的提高支付接口性能。

商户信息缓存

读写分析:

  1. 商户信息新增后基本不改动,即使改动也 不存在 写写 并发,而且改动也只是更新很少的属性值;
  2. 一个商户下有很多客户,每个客户操作都要访问这个商户信息;

首先,综合以上 2 点,可以看到商户信息访问频繁,但只存在读写并发,不存在 写写并发,采用 读写缓存模式 不会因为 读写并发 出现一致性问题;

然后,商户信息很少改动,不存在频繁的写操作,因此,使用 同步直写 + 事务 的方式,并不会存在性能问题,同时可以避免部分失败导致的不一致问题。

协议信息缓存

读写分析:

  1. 协议信息新增后改动也比较少,基本不存在 写写并发;
  2. 就单个协议而言,也不存在 读写并发,因为从业务上看,协议的操作是有时间顺序的。

首先,不存在 写写并发,完全可以采用和商户信息一样的策略,但是考虑到 改动的场景大部分是关闭这个协议即以后不再需要访问这条数据,所以采用了 只读缓存 的模式,在数据变更时直接删除缓存数据,这样可以尽量减少垃圾数据占用缓存空间;

然后,读写并发从业务上来讲也基本是不存在的,现在唯一剩下的就是部分失败的影响,从之前的分析中可以看到,采用 先删除缓存,后修改数据库 的方式可以保证数据的一致性,即使数据库更新失败,也不会存在业务影响。

最后,给缓存数据加上一个过期时间,一是为了减少脏数据对缓存的占用,二是虽然业务上分析不存在读写并发,但保不住网络延时等各种异常导致并发的形成,所以加上过期时间,即使出现缓存不一致,在时间过期后至少可以保证最终一致性,也算是对异常的一个兜底。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值