本文总结于:美团2面:亿级流量,如何保证Redis与MySQL的一致性?操作失败 如何设计 补偿?
保证缓存和数据库数据一致性通常有四种策略:
1.Read-Through(读穿透):策略和旁路缓存(cache-aside)的读策略很像,具体就是先从缓存读,读不到就从数据库加载数据到缓存中。与旁路缓存不同的是,从数据库加载数据并不是客户端的任务,而是由缓存系统透明的向底层数据库加载数据,客户端只需操作缓存得到数据而无需关注数据是从哪里来的。(也可以是客户端直接操作中间件,中间件实现上述透明过程)
2.Write-Through(写穿透):客户端向缓存系统发起写请求,缓存系统首先更新缓存数据,然后更新数据库数据,当数据在数据中完成更新后整个写操作才算完成。
3.Write-behind (异步缓存写入):当数据在缓存中被更新时,并非立即同步更新到数据库,而是将更新操作暂存起来,随后以异步的方式批量地将缓存中的更改写入持久化存储。异步缓存写入提高了系统的性能,但无法维护数据的强一致性。
以上三种操作,通过自动化的方式隐藏了缓存与持久化存储之间的交互细节(从数据库加载数据和更新数据库对于用户来说都是透明的),简化了客户端的处理逻辑。
4.Cache-aside(旁路缓存)。旁路缓存与上述三种操作最大的不同在于,该策略直接访问缓存和数据库,通过应用程序实现数据的一致性,在这个模式中,数据以数据库为主存储,缓存作为提升读取效率的辅助手段。
Cache-aside(旁路缓存)
策略一:先更数据库,再更缓存。(先更缓存,再更数据库)
问题:这两种”双更“的策略都存在着一个共性问题,即在多线程下,线程A先完成更新数据库操作后,线程B完成了更新数据库和更新缓存的两步操作,接着线程A才完成更新缓存的操作,按照这样的顺序执行,数据库的数据是线程B更新的数据,而缓存中是线程A更新的数据,出现数据不一致现象,并且数据不一致时间长。
策略二:先删除缓存,再更新数据库
问题:线程A先完成删除缓存,接着线程B想要查询该数据,由于缓存中不存在该数据,线程B就从数据库中查询该数据,并将该数据加载到缓存中,接着线程A完成数据库的更新。按照这样的顺序执行,缓存中存放的仍是旧数据,线程B读取到的是旧数据,缓存和数据库仍然数据不一致,直到缓存过期或者有操作更新了数据,数据才恢复一致,否则缓存中一直都是旧数据。
延申:为什么是删除缓存而不是更新缓存?
1.减少脏数据的产生,当策略一中不是更新缓存而是删除缓存,两个执行写操作的线程就不会发生数据不一致的现象。
2.提高性能。当缓存中的值是需要经过复杂计算的(如需要多表联查),更新频率太高就会导致性能下降。并且有些数据不经常被访问,或者是写多读少,这类数据更新直接删除,需要时再从数据库中查出。
策略三:先更新数据库,再删除缓存。
问题:线程A先完成更新数据库操作,如果发生严重网络波动,造成删除缓存的操作延后,此时线程B从缓存中读取数据,就会造成数据库和缓存不一致性。相比于策略二,策略三数据不一致性在线程A完成删除缓存后就恢复正常,数据不一致时间较短,而策略二需要等到下一次对该数据的再次更新才恢复一致性,数据不一致时间长,因此常选择策略三而并非策略二。
新问题:策略三如果线程A删除缓存失败了,就会造成长时间的数据不一致。
策略四:延迟双删(先删除缓存,再更新数据库,延迟一段时间后再删除缓存)
问题同策略二,线程A先完成删除缓存,线程B的读操作从数据库查询该数据并将数据加载到缓存中,线程A再完成更新数据库,此时出现数据不一致,但这种策略数据不一致的时间较短,因为线程A完成更新数据库操作后,延迟一段时间后就会将缓存数据删掉,保障数据一致性。
优点:
1.通过延迟双删可以一定程度上减少数据不一致的时间,保证数据一致性
2.相比于其他缓存一致性的实现方案,延迟双删可以直接通过简单代码实现。
缺点:
1.延迟时间不好确定:时间过短,不能保证所有读取旧数据的请求均已完成;时间过长,容易造成长时间数据不一致。
2.多次删除对缓存造成一定的压力
3.如果第二次删除缓存失败了,就会造成长时间的数据不一致性。
延申:分布式系统下策略三与策略四的策略选择
在分布式系统下,数据库采用主从分布,主库与从库的数据同步需要时间,在这种情况下,策略三在完成删除缓存后,此时有线程访问数据,数据库主从同步未完成,线程从从库中读取到的仍是脏数据,加载到缓存的仍是脏数据,此时再次出现数据不一致,而策略四延迟时间后删除缓存便可以很好的解决该问题,延迟的时间确定为主从同步的时间,等到主从同步完成后在删除缓存,后来的线程访问到的都是一致的数据,因此延迟时间过短,还会造成旧数据回种到缓存中。
选 Cache-Aside(简单策略):
-
若业务容忍毫秒级不一致(如社交动态的点赞数),且主从同步延迟极低(如 < 50ms)。
选延迟双删(强一致策略):
-
若业务要求最终一致性,且主从同步延迟较高(如 > 100ms)。
策略五:异步删除
异步删除是针对策略三的改进,策略三的操作顺序为先更新数据库再删除缓存,在策略三中,这两个操作是在一个线程中执行的,我们引入异步删除,即将删除缓存的操作加入到任务队列中,同时用一个异步线程去消费任务队列中的删除缓存操作,这样做有两个好处:
1.通过异步解耦,主线程在更新完数据库后直接返回,无需等待删除缓存操作完成,提高了系统性能。
2.策略三中如果删除缓存失败了,就会造成长时间的数据不一致,通过异步删除引入队列,我们可以实现补偿机制确保删除缓存的任务能够被消费完成。
基于队列的异步删除,又可以分为三种类型:
5.1.基于内存队列删除缓存
5.2.基于消息队列删除缓存
5.3.基于binlog+消息队列删除缓存
策略5.1 基于内存队列删除缓存
此策略基于jvm进程的内部队列(常用阻塞队列),引入内存队列,将删除缓存的任务加入到内存队列中,同时用一个异步线程去消费任务执行删除缓存的操作,如果缓存删除失败,可以重试多次确保删除成功。
这种策略数据不一致仍然出现在一个线程更新完数据库后删除缓存的操作仍未执行,此时来了另一个线程执行查询就会读到缓存中的脏数据,在删除缓存前仍会出现数据不一致,但是在缓存删除后数据就恢复一致,同时引入重试机制确保缓存能够被删除,解决了删除失败问题,确保数据不一致是短暂的。如果出现大量写操作,队列中任务堆积较多来不及消费,就会造成数据不一致时间延长,此时可以开启多个线程消费队列中删除缓存的任务,减小数据不一致时间。
策略5.1作为加强后的策略三,与策略四相比优势在于:
1.延迟双删需要删除两次缓存,而该策略只需删除一次,减小了对缓存的压力
2.延迟双删同样有删除缓存失败造成长时间数据不一致的问题,该策略引入重试机制确保删除缓存的操作能够完成。
3.该策略通过队列将更新数据库和删除缓存的操作解耦,模块职责更加单一。
问题:1.程序复杂度上升,需要引入内存队列和消费线程。
2.最主要的问题:基于jvm进程的内存队列并不是高可靠的,即当程序崩溃后,内存队列的任务会全部丢失,恢复后无法消费丢失的删除缓存的操作,从而导致数据长时间不一致。
策略5.2 基于消息队列删除缓存
为解决程序崩溃导致的消息丢失,我们引入高可用组件——消息队列(RabbitMQ、RocketMQ、Kafka),就算程序崩溃,恢复后消息也不会丢失,并且通过合理配置还可实现消息持久化保证高可靠性。引入消息队列后,线程在更新数据库后,同步发送一条删除缓存操作的消息到消息队列中,然后由专门的消费者进行消息消费,同时利用消息队列ACK机制保证消息能够被消费,如果一直不能被成功消费,在重复投递一定的次数之后(默认16次),消息会进入死信队列。
通过引入消息队列,避免了因JVM崩溃所导致的内存队列中的记录丢失的问题。
在该策略中,线程需要发送删除缓存操作消息到消息队列中(线程还需要将这个操作消息序列化成MQ消息),那么可不可以将这个过程从线程流程中剥离,交给其他中间件执行呢?
策略5.3 基于binlog+消息队列删除缓存
针对策略5.2最后提出的问题,可以引入基于binlog+消息队列这种方式解决,这种方案的实现方式具体如下:
1.利用阿里的Canal中间件,采集在数据写入时Mysql生成的binlog日志,Canal将日志发送到消息队列中。
2.在消费端,可以编写一个专门的消费者(Cache Delete Consumer)完成缓存binlog日志订阅,筛选出其中的更新类型log,解析之后进行对应Cache的删除操作,并且通过RocketMq队列ACK机制确认处理这条更新log,保证Cache删除能够得到最终的删除。
这种方案好处有:
1.删除缓存的工作彻底从线程中剥离出来,线程只需要完成更新数据库操作即可。
2.该方案无需入侵业务代码,策略5.1、5.2都需要编写相应的将消息写入内存队列或者是发送消息到消息队列中的代码,实现无侵入。
三种策略的对比
1.基于内存队列删除缓存
延迟范围:毫秒级(通常 <10ms)。
延迟来源:1.内存队列本身为内存操作,无网络传输与磁盘I/O开销。2.消费者线程直接处理队列任务,无中间组件依赖。
适用场景:1.高并发瞬时故障恢复(如网络抖动)。2.对延迟敏感但允许短暂数据不一致的业务(如秒杀库存缓存删除)。
2. 基于消息队列删除缓存
延迟范围:毫秒至秒级(通常 100ms~2s)。
延迟来源:1.消息队列需完成 持久化存储(如Kafka落盘)和 网络传输(生产者→Broker→消费者)。
2.消费者需处理消息反序列化、重试策略(如指数退避)。
适用场景:1.跨服务解耦场景(如分布式系统间缓存同步)2.允许稍高延迟但需高可靠性的业务(如订单状态更新)。
3. 基于binlog+消息队列删除缓存
延迟范围:秒级至分钟级(通常 1s~30s)。
延迟来源:1.数据库主从同步延迟:Binlog从主库同步到从库存在延迟(尤其高负载时)。
2.Binlog解析与分发:需通过Canal等工具解析并投递到消息队列,增加处理链路。
适用场景:1.强数据一致性要求且业务与缓存更新逻辑解耦的场景(如用户账户余额同步)。
2.可接受分钟级最终一致性的低频更新场景(如商品分类信息变更)。
方案 | 延迟水平 | 可靠性 | 适用场景 |
---|---|---|---|
内存队列 | 最低(毫秒)(通常 <10ms) | 较低 | 瞬时故障恢复、高并发低延迟场景 |
消息队列 | 中等(秒级)(通常 100ms~2s) | 高 | 分布式解耦、异步可靠删除 |
Binlog+消息队列 | 最高(分钟)(通常 1s~30s) | 最高 | 强一致性、业务无侵入式同步 |
方案选型:
-
延迟敏感型业务:优先选择内存队列,但需容忍短暂不一致风险 。
-
平衡型场景:选择消息队列,兼顾可靠性与延迟 。
-
弱一致性场景:接受更高延迟,采用Binlog+消息队列兜底 。
策略5.4 异步删除缓存的三级补偿设计
为了保证异步删除缓存的操作能够完成,实现数据库与缓存的数据最终一致性,设计三级补偿架构。
第一级补偿:延迟队列删除缓存
第二级补偿:消息队列删除缓存
第三级补偿:定时任务scan比对补偿
具体实现措施如下:
第一级补偿——延时队列重试:将任务放入延迟队列中,设置延迟时间应对网络抖动等情况,同时设置重试次数字段retryCount,记录重试次数。消费线程从延迟队列拿出任务消费,消费失败将retryCount+1,重新设置延迟时间,将未完成的任务重新入队重新消费,当retryCount达到maxRetryCount(该值可以自己设置)时,将该任务转入消息队列开启第二级补偿。
第二级补偿——消息队列重试:第二级补偿将任务加入到消息队列中,防止进程重启或长时间故障下造成的数据丢失,跟第一级补偿操作相同,当消费线程消费失败后,将该任务重新投递到消息队列中重新消费,可以为消息队列设置一个最大重试数(通常为16次),利用指数退避策略,每次重试的时间逐次增加(如第一次延迟1秒,第二次延迟5秒...第16次延迟1800秒),最后达到最大重试数后将任务加入到死信队列中。
三级补偿——定时任务比对兜底:第三级补偿首先利用xxl-job开启定时任务,设置合理的任务执行周期,任务内容是这样的:首先使用redis的scan命令对key进行扫描,将key封装成消息发送到消息队列中,Flink从消息队列中消费消息,获取到redis中key后,从数据库中查询原始数据比对,发现数据不一致就根据业务逻辑更新缓存数据实现数据一致性。
三级补偿总结:
每一级别的延迟级别:
-
延迟队列(延迟50ms,指数退避, 10ms- 100ms毫秒级别)
-
消息队列(延迟重试15次,100ms-30秒 指数退避, 秒级 )
-
定时任务(兜底扫描, 每30分钟-300分钟,小时级 )
层级 | 触发条件 | 实现方式 | 收敛时间 |
---|---|---|---|
内存队列 | Redis删除失败 | 阻塞队列+线程池 | 10ms- 100ms |
消息队列 | 内存队列重试失败 | RocketMQ事务消息 | 100ms-30秒 |
定时任务 | 定时触发/监控报警 | XXL-JOB 扫描关键的key | 30分钟-300分钟 |
-
99%场景:通过阻塞队列和延迟队列在百毫秒内完成删除。
-
0.99%场景:依赖消息队列在秒级至分钟级恢复。
-
0.01%极端场景:由定时任务最终保障一致性。
秒杀系统如何保持缓存数据库数据一致性?
面临一个秒杀系统时,有一种方案是将商品库存放入到缓存中,当秒杀开始时,先扣减缓存里的库存,再发送一条延时消息扣减数据库里的库存,这是为了防止大量请求打到数据库上,从而保护数据库,为什么不用旁路缓存策略呢?
首先,旁路缓存适用于读多写少的情况,而秒杀系统是一个写多读少的情况。其次我的方案有以下几点优势:
-
将库存扣减操作前置到缓存:利用内存操作的高性能(如Redis的原子命令
DECR
)快速响应请求,避免数据库成为瓶颈。实际上是将一致性要求适度放宽,换取系统的高可用和高性能 -
异步延时消息更新数据库:通过消息队列(如RocketMQ)将数据库更新操作异步化、削峰填谷,最终保证数据库与缓存的一致性。
-
最终一致性替代强一致性:允许数据库与缓存在极短时间窗口内不一致(如异步消息处理延迟),但通过业务设计(如预扣库存、订单超时释放)保证最终正确性。
从本质上看,这不是矛盾,而是在不同场景下对一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三者(CAP理论)的权衡取舍不同,CAP理论提出在分布式系统中只能同时实现两点,即从C、A中选择一个与P结合(强一致性与高可用性是矛盾的),事实上系统通常会选择AP,相比于强一致性,系统的可用性首先保证了系统可用。
BASE理论源于对大规模互联网分布式系统实践的总结,作为CAP定理中一致性与可用性矛盾的实践性补充逐步演化形成。该理论主张在无法保证强一致性的场景下,系统可基于业务特性灵活调整架构设计,通过基本可用性保障、允许短暂中间状态等机制,确保数据最终达成一致性状态,从而在分布式环境中实现可靠服务能力与业务需求的平衡。