什么是“缓存和数据库一致性“问题?

在业务系统开发过程中,缓存经常会被使用,通过将数据存储在读写性能较高的存储介质中,来提升系统查询性能。在工作中经常使用的缓存有:本地缓存(进程内缓存),分布式缓存(redis)等,最常见使用使用缓存的场景:将数据库中的部分数据放到缓存中,来降低数据库的请求量,从而提升系统 整体性能,因为一份数据存储在两个地方:缓存和数据库,所以在实际场景中,难免会出现"一致性"的问题。

什么是一致性

在讨论缓存一致性问题前,我们有必要定义清楚什么是一致性,这里仍然以数据库和缓存数据一致性为例,"数据一致性"主要包含以下两种情况:

1.缓存中有数据,那么缓存中的值需要和数据库中值相同。

2.缓存中本身没有数据,那么,数据库中的值必须是最新值。

除了数据一致性定义之外,我们还需要讨论一下一致性的分类:强一致性最终一致性
强一致性:在关系型数据库中比较常见,主要就是要保证数据时刻一致,如关系型数据库中的ACID中的 “C”。

最终一致性:这个在分布式系统中比较常见,由于在分布式系统中的一些物理因素的影响,如网络延迟,要想做到强一致性比较困难。最终一致性允许短暂时间内数据不一致,但是经过一段时间后,数据会保持一致。

缓存和数据库的数据一致性

由于缓存实际使用场景不同,不同的缓存读写模式下,数据库和缓存的不一致情况也是不同的。缓存常用的使用方式分为两种:读写缓存只读缓存

下面我们分别讨论一下这两种场景下产生不一致的情况以及解决方案。

读写缓存

读写缓存情况下,缓存会对客户端暴露"读"和"写"接口,当客户端进行数据的读写时,请求首先由缓存来处理。"增\删\改"的最新数据,在缓存中。此时被代理的后端数据库中的数据是"脏"数据。
在这里插入图片描述

但是由于缓存存储介质自身的原因,缓存相比数据库而言是不稳定的,最新的数据存放在缓存中是有一定风险的,如果缓存出现故障,那么就会存在数据丢失的风险。

根据业务对数据风险的敏感程度,以及数据库和缓存数据一致性的要求高低,对于读写缓存中最新数据更新到数据库中的策略分为两种:同步直写异步写回

同步直写

同步直写:当写请求发送给缓存后,同时也发送给后端数据库,当后端数据更新完毕后,此次写请求才算完成。
在这里插入图片描述

优点:这种做法保证数据库和缓存的最大一致性,当数据缓存出现故障中,数据丢失的概率大大降低,但是还是会存在数据丢失的可能,因为更新缓存和更新数据库这两个操作不具备事务特性。

缺点:每个写操作都需要同时操作数据库和缓存,而且都操作完毕后,本次请求才算完成,而数据库数据更新效率要远小于缓存,所以写操作的整体效率较低。

异步写回:

异步写回:是指写请求在缓存端处理完成后,就向客户端返回处理成功,对于客户端而言请求就算处理完毕了,就可以进行下一次的请求了。请求的处理效率较高。但是在缓存端的最新数据何时同步到数据库中,是一个比较难以决断的问题。因为写请求持续不断,无论缓存向数据库中更新多少次数据,只要最新的数据没有从缓存中更新到数据库,那么数据库中的数就是脏数据。所以中间多次的数据同步操作都是没有意义的,对于异步写回这种策略,数据更新到数据库的时间点,通常选择在缓存置换的是时候,这个时候进行数据同步,是成本最低,效果却是相同的。但是缓存置换的时间点完全有缓存决定,不受上层业务系统控制。
在这里插入图片描述

优点:写请求的处理效率高,数据更新到缓存中,数据写入请求就算处理结束了。

缺点:最新数据保存在缓存中,数据丢失风险较高。

总结来说,异步写回和同步直写是系统设计过程中,对性能和稳定性两个指标的选择,不同的设计带来不同的结果,也再次验证了架构的设计,一定要结合业务,架构的设计过程就是取舍的过程。

对于异步写回的方式,最新的数据保存在缓存中,不符合数据一致性定义的第2条,该方案本身就无法提供数据一致性。

而对于同步直写的方式,相比异步写回,很大程度上提升了保证了数据的一致性,但是导致数据不一致的场景依然存在,以修改场景为例:

1.原子性导致数据不一致
由于更新缓存和更新数据时两个独立的操作,可能存在缓存更新成功,数据库更新失败的情况,此时缓存和数据库中的数据时不一致的。

对于这类问题,常用的做法是"重试",对于失败的操作进行重试,或者在业务层面实现更新数据库和更新缓存的原子性。

2.并发场景导致数据不一致
当多个线程同时更新数据时,会导致缓存和数据库数据不一致,如线程T1和线程T2同时更新数据,线程T1将数据更新成A,线程T2将数据更新成B,当更新时序如下时,就会导致数据不一致:

1.T1将缓存数据更新为A。

2.T2将缓存数据更新为B。

3.T2将数据库数据更新为B。

4.T1将数据库数据更新为A。

此时缓存中数据为B,而数据库数据为A。

对于并发场景下,同步直写导致数据不一致,只能通过加锁的方式,使并行操作,变成串行,而串行写入的方式在高并发场景下,会对性能产生影响,这个方案的选择就需要在性能和数据一致性之间做权衡。

只读缓存

通常来说,只读缓存是将redis作为缓存,对外只提供读接口,对于数据的 增/删/改请求,缓存则不作处理,而是将这些请求转发给后端数据库。对于新增数据,缓存中刚开始是没有的,新增到数据库中后,客户端从数据库中读取数据,再加载到缓存中。对于"删除","修改"来说,数据已经存在于缓存中,不过数据的最新状态在数据库中,此时就会导致缓存和数据库数据不一致。
在这里插入图片描述

在只读缓存场景下,解决数据一致性的问题常用的方法是:更新数据库,然后删除缓存数据。但是如果直接这样做,在一些特殊场景下,仍然会产生数据不一致,具体场景如下:

1.原子性导致数据不一致
和读写缓存中介绍原子性问题一样,解决方式也是类似的:失败重试和业务层面实现原子性。

2.并发场景导致数据不一致
采用"先更新数据库,然后删除缓存"的方式可以在很大程度上,避免数据不一致,但是在并发场景下,仍然会出现一些"意外"的情况,具体如下:

存在三个线程T1,T2和T3,T1和T2更新数据,T3读取数据,当出现如下时序操作时,会产生数据不一致:

1.T1将数据库中数据更新为A,然后删除缓存中数据。

2.T3读取数据,发现缓存未命中,从数据库中读取数据A,在未将数据写回到缓存时,执行第3步。

3.T2将数据库中数据更新为B,然后删除缓存中数据。

4.线程T3将从数据库中查询的数据A写回缓存。

此时数据库中数据为B,缓存中数据为A,数据不一致。

以上导致数据不一致的场景发生概率较小:T3在T2更新数据之前读取旧数据,在T2更新数据,然后删除缓存后,才将旧数据写回缓存。但是高并发的场景下确实会发生,要解决上述场景导致的数据不一致,我们先要确认产生不一致的根本原因是是什么,上述数据不一致根本原因在于:线程T3写回缓存的操作,发生在T2删除缓存操作之后导致的,也就是T3拿到数据后,到写入缓存,这之间的时间"太长了"。

解决上面这个问题的一个比较直接的手段,把线程T2更新完数据库后,到删除缓存之间的时间间隔拉长,拉长到大于线程T3拿到数据后,将数据写回缓存的时间。虽然这个方法不能完全避免并发环境下数据不一致的问题,但是却可以将不一致问题发生的概率大幅度降低,除非T2将数据写入缓存的时间特别长。但是上述方案不足之处在于,间隔时间没有一个放之四海而皆准的值,需要根据业务场景来定。

总结

上面讨论了缓存在读写模式和只读模式下,可能出现缓存和数据库数据不一致的场景和相应的解决方案。但是上述方案都无法保证强一致性,因为缓存和数据库更新有先后,再加上在分布式的环境下,很难实现强一致性。上面的方法主要在优化最终一致性,尽量将达到最终一致性状态的时间变得最小。

如果想要到达强一致性,就需要使用"锁",将写操作串行化,写数据的过程中,其他操作都阻塞。但是这样必定会使系统性能降低。而我们使用缓存的初衷,是通过将一部分数据放到另外的一个组件中,来提升系统性能,而数据多区域存放,必然会带来数据一致性问题,现在为了解决一致性问题,又要牺牲系统性能,这种操作有点本末倒置了。

如果说,系统的一致性是不可妥协的,那么系统的性能就要做出一定的妥协,而使用缓存的方案,在最开始的时候,可能就需要考虑是否应该采用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值