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

一旦决定使用缓存,那必然要面临一致性问题,任何一种解决方案都无法保证绝对意义上的数据一致性。
“性能和一致性就像天平的两端,无法做到都满足要求。”
缓存和数据库的一致性问题不是一成不变的,分析一致性问题,需要考虑以下3点:
缓存数据库操作的原子性(成功执行)、并发、缓存击穿
缓存有两种模式:读写缓存、只读缓存。
读写缓存:若要对数据进行增删改,需要在Cache进行。 同时根据采取的写回策略,决定是否同步写回DB。
只读缓存:若要对数据进行增删改,只需在DB进行。同时根据缓存更新策略,决定何时更新或者删除。

四种常用的缓存更新策略:

对于写操作,有四种常用的缓存更新策略,分别是:

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

写操作的缓存更新策略分析

双更策略:

第一个问题**-无法保证数据库和缓存读操作的原子性:**
当更新操作无法成功执行时,无论先更新哪一个,但凡第二步操作发生异常,就会导致数据不一致,对业务造成影响。

第二个问题-并发:(缓存脏数据)
场景:两个写操作线程并发更新同一条数据,此时可能会由于执行时序发生错乱,而导致数据不一致问题。
以先更DB再更Cache的策略为例,写操作A和写操作B先后更新key1,在操作A未完成时操作B便开始执行,假如执行顺序如下:A-write db B-write db B-write redis A-write redis。最终数据不一致。
其他问题:
双更策略的缓存利用率不高,每次数据发生变更,都会更新缓存,但是缓存中的数据不一定会被立即读取,这就会导致缓存中可能存放了很多非热点数据,浪费缓存资源。(最主要的原因!)
而且很多情况下,写到缓存中的值,并不是与数据库中的值一一对应的,很有可能是先查询数据库,再经过一系列计算得出一个值再写到缓存中,对于非热点数据的一系列计算,还会造成机器性能的浪费。
结论:因此,在大多数场景下,都不建议采用双更策略。

删除策略

第一个问题-无法保证数据库和缓存读操作的原子性:
同上分析,无论先更新还是先删除,但凡第二步操作异常,都会导致数据不一致。

第二个问题-并发:需要分别考虑
1 先删除缓存再更新数据库
场景:写操作和读操作并发执行。
两个条件:先写再读,更新DB的时间 > 读DB+写Cache时间。
A-delete redis B-read redis miss B- redis db B-write redis A-update db
2 先更新数据库再删除缓存
场景:写操作和读操作并发执行。
三个条件:缓存中的key刚好被清除(也许是失效、也许是写操作执行的删除),先读再写,读DB+写Cache > 写DB+删Cache
1 A.read redis miss 2.A-read db 3.B-write db 4.B-del redis 5.A-write redis
请求1从DB读取数据A -> 请求2写更新数据A到数据库再删除cache中的A数据 -> 请求1将数据A写入缓存。
后者这种场景出现的概率很低,尤其是第三个条件发生的概率其实是非常低的。因为,写数据库一般会先加锁,所以写数据库,通常是要比读数据库的时间更长的。
第三个问题-缓存击穿
任何删除Cache的行为,在高并发场景下,都有可能导致缓存击穿。可以采用读操作互斥、定时更新的方案,缓解缓存击穿问题。

结论:大多场景下,建议采用“先更新数据库,再删除缓存策略”,可以最大程度上保证数据一致性。
注:后删策略也是Spring-cache中使用的更新策略,Cache Aside Pattern旁路缓存模式中的更新策略。

redis分布式缓存与数据库的数据一致性

重要:缓存是通过牺牲强一致性来提高性能的。
这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。
CAP理论,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
I. 什么是 一致性、可用性和分区容错性
分区容错性:指的分布式系统中的某个节点或者网络分区出现了故障的时候,整个系统仍然能对外提供满足一致性和可用性的服务。也就是说部分故障不影响整体使用。
事实上我们在设计分布式系统是都会考虑到bug,硬件,网络等各种原因造成的故障,所以即使部分节点或者网络出现故障,我们要求整个系统还是要继续使用的(不继续使用,相当于只有一个分区,那么也就没有后续的一致性和可用性了)
可用性: 一直可以正常的做读写操作。简单而言就是客户端一直可以正常访问并得到系统的正常响应。用户角度来看就是不会出现系统操作失败或者访问超时等问题。
**一致性:**在分布式系统完成某写操作后任何读操作,都应该获取到该写操作写入的那个最新的值。相当于要求分布式系统中的各节点时时刻刻保持数据的一致性。
所以,如果需要数据库和缓存数据保持强一致,就不适合使用缓存。
所以使用缓存提升性能,就是会有数据更新的延迟。这需要我们在设计时结合业务仔细思考是否适合用缓存。然后缓存一定要设置过期时间,这个时间太短、或者太长都不好:
太短的话请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势。
太长的话缓存中的脏数据会使系统长时间处于一个延迟的状态,而且系统中长时间没有人访问的数据一直存在内存中不过期,浪费内存。
但是,通过一些方案优化处理,是可以保证弱一致性,最终一致性的。

3种方案保证数据库与缓存的一致性

  • 延时双删策略
  • 删除缓存重试机制
  • 读取biglog异步删除缓存

延时双删策略

延时双删的步骤:
1 先删除缓存
2 再更新数据库
3 休眠一会(比如1秒),再次删除缓存。
在这里插入图片描述
为什么需要第一次删除缓存?
第一次删除缓存的目的是为了防止一个线程在更新数据库之后,删除缓存之前,其它线程的读请求读取缓存中的脏数据。

缓存延迟双删策略,即在线程 A更新完数据库、 删除缓存之后,先休眠一会,再删除一次缓存。
延迟时间要大于线程 B 读取数据库 + 写入缓存的时间。但是,这个时间在分布式和高并发场景下,其实是很难评估的。凭借经验大致估算这个延迟时间,只能尽可能地降低不一致的概率,极端情况下,还是会发生不一致现象。
所以实际使用中,还是建议采用先更新数据库,再删除缓存的策略。

删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢?删除失败会导致脏数据哦~,删除失败就多删除几次,保证删除缓存成功,所以可以引入删除缓存重试机制。
在这里插入图片描述
删除缓存重试机制的大致步骤:

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

为什么一定要写入消息队列?
在执行失败的线程中一直重试时,如果项目重启了,那这次重试请求就会丢失,这条数据就会一直不一致。
消息队列的优势:
消息队列保证可靠性-写到队列中的消息,成功消费之前不会丢失,重启项目也不担心;
消息队列保证消息成功投递-下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者,符合重试的场景。

需要考虑的问题:写消息队列的操作也可能会失败,引入消息队列会增加维护成本。
操作缓存和写消息队列,同时失败的概率其实是很小的;项目中一般都会用到消息队列,维护成本并没有新增很多。引入消息队列来解决这个问题,是比较合适的。

同步biglog异步删除缓存

重试删除缓存机制还可以,就是会造成好多业务代码入侵。其实,还可以通过数据库的binlog来异步淘汰key。
在这里插入图片描述
以mysql为例 可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性。如果这时候消息消费失败,例如数据库异常,余额不足扣款失败等一切业务认为消息需要重试的场景,只要返回失败,RocketMQ就会认为这批消息消费失败了。为了保证消息是肯定被至少消费成功一次,RocketMQ会把这批消费失败的消息重发回Broker,在延迟的某个时间点(默认是10秒,业务可设置)后,再次投递到这个ConsumerGroup。而如果一直这样重复消费都持续失败到一定次数,就会投递到DLQ死信队列。应用可以监控死信队列来做人工干预。

使用这种方案的优点在于:
无需考虑写消息队列失败情况:只要写 MySQL 成功,Binlog 肯定会有
自动投递到下游队列:canal 自动把数据库变更日志投递给下游的消息队列
缺点:需要投入精力去维护 canal 的高可用和稳定性。

主从数据库通过biglog异步删除
因为主从DB同步存在延时时间。如果删除缓存之后,数据同步到备库之前已经有请求过来时, 会从备库中读到脏数据,如何解决呢?解决方案如下流程图:
在这里插入图片描述
缓存与数据的一致性的保障策略总结
综上所述,在分布式系统中,缓存和数据库同时存在时,如果有写操作的时候,「先操作数据库,再操作缓存」。如下:
1.读取缓存中是否有相关数据
2.如果缓存中有相关数据value,则返回
3.如果缓存中没有相关数据,则从数据库读取相关数据放入缓存中key->value,再返回
4.如果有更新数据,则先更新数据库,再删除缓存
5.为了保证第四步删除缓存成功,使用binlog异步删除
6.如果是主从数据库,binglog取自于从库
7.如果是一主多从,每个从库都要采集binlog,然后消费端收到最后一台binlog数据才删除缓存,或者为了简单,收到一次更新log,删除一次缓存

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值