如何保证缓存(redis)与数据库(MySQL)的一致性

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 Tair(兼容Redis),内存型 2GB
简介: 【说明】  对于热点数据(经常被查询,但不经常被修改的数据),我们可以将其放入redis缓存中,以增加查询效率,但需要保证从redis中读取的数据与数据库中存储的数据最终是一致的。本文基于“孤独烟”与“58沈剑”两位的文章,针对一致性的问题进行了汇总总结,两位的原文链接见文末。

【说明】
  对于热点数据(经常被查询,但不经常被修改的数据),我们可以将其放入redis缓存中,以增加查询效率,但需要保证从redis中读取的数据与数据库中存储的数据最终是一致的。本文基于“孤独烟”与“58沈剑”两位的文章,针对一致性的问题进行了汇总总结,两位的原文链接见文末。

【前言】

  客户端对数据库中的数据主要有两类操作,读(select)与写(DML)。针对放入redis中缓存的热点数据,当客户端想读取的数据在缓存中就直接返回数据,即命中缓存(cache hit),当读取的数据不在缓存内,就需要从数据库中将数据读入缓存,即未命中缓存(cache miss)。所以读操作并不会导致缓存与数据库中的数据不一致。
  对于写操作(DML),缓存与数据库中的内容都需要被修改,但两者的执行必定存在一个先后顺序,这可能会导致缓冲与数据库中的数据不再一致,此时主要需要考虑两个问题:
  1、执行顺序的问题:先更新缓存还是先更新数据库?
  2、更新缓存的策略问题:当缓存中的内容变化时,是选择修改缓存(update),还是直接淘汰缓存(delete)?

针对这两点问题,一共可以分为四种方案:
  1、先更新缓存,再更新数据库;
  2、先更新数据库,再更新缓存;
  3、先淘汰缓存,再更新数据库;
  4、先更新数据库,再淘汰缓存。

【疑问一】更新cache还是淘汰cache?

  我们先来讨论缓存更新的策略问题:即更新缓存时,是直接淘汰cache中的旧数据,还是将更新操作也放在缓存中进行?

淘汰cache:
优点:操作简单,无论更新操作是否复杂,直接将缓存中的旧值淘汰
缺点:淘汰cache后,下一次查询无法在cache中查到,会有一次cache miss,这时需要重新读取数据库
更新cache:
  更新chache的意思就是将更新操作也放到缓冲中执行,并不是数据库中的值更新后再将最新值传到缓存
优点:命中率高,直接更新缓存,不会有cache miss的情况
缺点:更新cache消耗较大
  当更新操作简单,如只是将这个值直接修改为某个值时,更新cache与淘汰cache的消耗差不多
  但当更新操作的逻辑较复杂时,需要涉及到其它数据,如用户购买商品付款时,需要考虑打折等因素,这样需要缓存与数据库进行多次交互,将打折等信息传入缓存,再与缓存中的其它值进行计算才能得到最终结果,此时更新cache的消耗要大于直接淘汰cache
所以选择直接淘汰缓存更好,如果之后需要再次读取这个数据,最多会有一次缓存失败

【更新cache的另一个问题】
  我们现在已经知道直接淘汰cache比更新cache要更好,现在再进一步思考下更新cache的其它问题。
  对于上文列举的四种方案的前两种,即:
    1、先更新(update)缓存,再更新数据库;
    2、先更新数据库,再更新(update)缓存;
  当并发较大,同时有两个线程需要对同一个数据进行更新时,可能会出现以下问题:
方案一、先更新(update)缓存,再更新数据库
  线程A更新了缓存
  线程B更新了缓存
  线程B更新了数据库
  线程A更新了数据库
方案二、先更新数据库,再更新(update)缓存
  线程A更新了数据库
  线程B更新了数据库
  线程B更新了缓存
  线程A更新了缓存
如果不同的线程对同一个数据进行更新时,更新的先后顺序有明确要求,那么上述两种方案都会导致数据的不一致
解决的思路是“串行化”,即对同一个数据的修改,要以串行化的方式先后执行

结论:更新cache的消耗更大,且很有可能造成数据的不一致,所以推荐直接淘汰cache

【疑问二】执行顺序的问题

  究竟是先淘汰缓存还是先更新数据库?
这里主要分为两个方面来考虑:
  1、更新数据库与淘汰缓存是两个步骤,只能先后执行,如果在执行过程中后一步执行失败,哪种方案的影响最小?
  2、如果不考虑执行失败的情况,但更新数据库与淘汰缓存必然存在一个先后顺序,在上一个操作执行完毕,下一个操作还未完成时,如果并发较大,仍旧会导致数据库与缓存中的数据不一致,在这种情况下,用哪种方案影响最小?

另外,对于数据库而言,读写操作可以只作用在同一台服务器上,即底层只有一个数据库,也可以将读操作放在从库,写操作放在主库,即底层是主从架构,对于主从架构还需要考虑主从延迟,本文针对的是单节点模式。

【数据库是单节点】

情景一:更新数据库与淘汰缓存需要先后执行,如果在执行过程中后一步执行失败,哪种方案对业务的影响最小?
  方案一、先淘汰缓存,再更新数据库
如果第一步淘汰缓存成功,第二步更新数据库失败,此时再次查询缓存,最多会有一次cache miss
  方案二、先更新数据库,再淘汰缓存
如果第一步更新数据库成功,第二部淘汰缓存失败,则会出现数据库中是新数据,缓存中是旧数据,即数据不一致
解决办法:为确保缓存删除成功,需要用到“重试机制”,即当删除缓存失效后,返回一个错误,由业务代码再次重试,直到缓存被删除。

但对于方案一,如果更新数据库失败其实也是一个问题,为了确保数据库中的数据被正常更新,也需要“重试机制”,即当数据库中的数据更新失败后,也需要人工或业务代码再次重试,直到更新成功。

【结论】总体而言,虽然方案二导致数据不一致的可能性更大,但在业务中,无论是淘汰缓存还是更新数据库,我们都需要确保它们真正完成了,所以个人认为在情景一下两种方案并没有什么优劣之分。

重试机制的原理图:
_1

情景二:假设没有操作会执行失败,但执行前一个操作后无法立即完成下一个操作,在并发较大的情况下,可能会导致数据不一致。此时,哪种方案对业务的影响最小?

方案一、先淘汰缓存,再更新数据库

1、在正常情况下,A、B两个线程先后对同一个数据进行读写操作:
  A线程进行写操作,先淘汰缓存,再更新数据库
  B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取更新后的新数据
此时没有问题
2、在并发量较大的情况下,采用同步更新缓存的策略:
  A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
  B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但此时A线程还未完成更新操作,所以读取到的是旧数据,并且B线程将旧数据放入缓存。注意此时是没有问题的,因为数据库中的数据还未完成更新,所以数据库与缓存此时存储的都是旧值,数据没有不一致
  在B线程将旧数据读入缓存后,A线程终于将数据更新完成,此时是有问题的,数据库中是更新后的新数据,缓存中是更新前的旧数据,数据不一致。如果在缓存中没有对该值设置过期时间,旧数据将一直保存在缓存中,数据将一直不一致,直到之后再次对该值进行修改时才会在缓存中淘汰该值
此时可能会导致cache与数据库的数据一直或很长时间不一致

3、在并发量较大的情况下,采用异步更新缓存的策略:
  A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
  B线程进行读操作,发现缓存中没有想要的数据,从数据库中读取数据,但B线程只是从数据库中读取想要的数据,并不将这个数据放入缓存中,所以并不会导致缓存与数据库的不一致
  A线程更新数据库后,通过订阅binlog来异步更新缓存
此时数据库与缓存的内容将一直都是一致的

进一步分析:
如果采取同步更新缓存的策略,即如果缓存中没有数据,就读取数据库并将数据直接放入缓存,可能会导致数据长时间的不一致
在这种情况下,可以用一些方法来进行优化:
1、用串行化的思路
  即保证对同一个数据的读写严格按照先后顺序串行化进行,避免并发较大的情况下,多个线程同时对同一数据进行操作时带来的数据不一致性。
  关于如何用串行化保证一致性,详见“58沈剑”的文章“缓存与数据库一致性保证”,原文链接见文末。
2、延时双删+设置缓存的超时时间
  不一致的原因是,在淘汰缓存之后,旧数据再次被读入缓存,且之后没有淘汰策略,所以解决思路就是,在旧数据再次读入缓存后,再次淘汰缓存,即淘汰缓存两次(延迟双删)
引入延时双删后,执行步骤变为下面这种情形:
  A线程进行写操作,先成功淘汰缓存,但由于网络或其它原因,还未更新数据库或正在更新
  B线程进行读操作,从数据库中读入旧数据,共耗时N秒
  在B线程将旧数据读入缓存后,A线程将数据更新完成,此时数据不一致
  A线程将数据库更新完成后,休眠M秒(M比N稍大即可),然后再次淘汰缓存,此时缓存中即使有旧数据也会被淘汰,此时可以保证数据的一致性
  其它线程进行读操作时,缓存中无数据,从数据库中读取的是更新后的新数据

利用延迟双删,可以很好的解决数据不一致的问题,其中A线程休眠的M秒,需要根据业务上读取的时间来衡量,只要比正常读取消耗的实际稍大就可以。但是个人感觉实际业务中需要根据场景来设置休眠的时间,这个不好确定。

引入延时双删后,存在两个新问题:
  1、A线程需要在更新数据库后,还要休眠M秒再次淘汰缓存,等所有操作都执行完,这一个更新操作才真正完成,降低了更新操作的吞吐量
解决办法:用“异步淘汰”的策略,将休眠M秒以及二次淘汰放在另一个线程中,A线程在更新完数据库后,可以直接返回成功而不用等待。
  2、如果第二次缓存淘汰失败,则不一致依旧会存在
解决办法:用“重试机制”,即当二次淘汰失败后,报错并继续重试,直到执行成功个人

“异步淘汰”策略:
_2
A线程执行完步骤2不再休眠Ms,而是往消息总线esb发送一个消息,发送完成之后马上就能返回

【小结】
在单节点下,用“先删缓存,再更新”的策略,如果采用同步更新缓存的策略,可能会导致数据长时间的不一致,可以通过一些方法来尽量避免不一致;如果采用异步更新缓存的策略,就不会导致数据不一致

方案二、先更新数据库,再淘汰缓存

在正常情况下:
  A线程进行写操作,更新数据库,淘汰缓存
  B线程进行读操作,从数据库中读取新的数据
不会有问题

在并发较大的情况下,情形1:
  A线程进行写操作,更新数据库,还未淘汰缓存
  B线程从缓存中可以读取到旧数据,此时数据不一致
  A线程完成淘汰缓存操作
  其它线程进行读操作,从数据库中读入最新数据,此时数据一致
不过这种情况并没有什么大问题,因为数据不一致的时间很短,数据最终是一致的

在并发较大的情况下,情形2:
  A线程进行写操作,更新数据库,但更新较慢,缓存也未淘汰
  B线程进行读操作,读取了缓存中的旧数据
但这种情况没什么问题,毕竟更新操作都还未完成,数据库与缓存中都是旧数据,没有数据不一致

在并发较大的情况下,情形3:
  A线程进行读操作,缓存中没有相应的数据,将从数据库中读数据到缓存,
此时分为两种情况,还未读取数据库的数据,已读取数据库的数据,不过由于网络等问题数据还未传输到缓存
  B线程执行写操作,更新数据库,淘汰缓存
  B线程写操作完成后,A线程才将数据库的数据读入缓存,对于第一种情况,A线程读取的是B线程修改后的新数据,没有问题,对于第二种情况,A线程读取的是旧数据,此时数据会不一致
不过这种情况发生的概率极低,因为一般读操作要比写操作要更快
万一担心存在这种可能,可以用“延迟双删”策略,在A线程读操作完成后再淘汰一次缓存

【小结】
在该方案下,无论是采用同步更新缓存(从数据库读取的数据直接放入缓存中),还是异步更新缓存(数据库中的数据更新完成后,再将数据同步到缓存中),都不会导致数据的不一致
该方案主要只需要担心一个问题:如果第二步淘汰缓存失败,则数据会不一致
解决办法之前也提到过,用“重试机制”就可以,如果淘汰缓存失败就报错,然后重试直到成功

【单节点下两种方案对比】
先淘汰cache,再更新数据库:
  采用同步更新缓存的策略,可能会导致数据长时间不一致,如果用延迟双删来优化,还需要考虑究竟需要延时多长时间的问题——读的效率较高,但数据的一致性需要靠其它手段来保证
  采用异步更新缓存的策略,不会导致数据不一致,但在数据库更新完成之前,都需要到数据库层面去读取数据,读的效率不太好——保证了数据的一致性,适用于对一致性要求高的业务
先更新数据库,再淘汰cache:
  无论是同步/异步更新缓存,都不会导致数据的最终不一致,在更新数据库期间,cache中的旧数据会被读取,可能会有一段时间的数据不一致,但读的效率很好——保证了数据读取的效率,如果业务对一致性要求不是很高,这种方案最合适

【其它】
重试机制可以采利用“消息队列MQ”来实现
通过订阅binlog来异步更新缓存,可以通过canal中间件来实现

原文链接:
【58沈剑原文链接】
缓存架构设计细节二三事2016-03-08
缓存与数据库一致性保证2016-03-16
主从DB与cache一致性 2016-03-24
缓存,究竟是淘汰,还是修改?2018-07-02
究竟先操作缓存,还是数据库? 2018-07-09
Cache Aside Pattern 2018-07-11
缓存与数据库不一致,咋办? 2018-07-12

【孤独烟原文链接】
分布式之数据库和缓存双写一致性方案解析 2018-05-15
分布式之数据库和缓存双写一致性方案解析(二) 2018-06-28
分布式之数据库和缓存双写一致性方案解析(三)2018-07-13

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
2月前
|
存储 缓存 数据库
解决缓存与数据库的数据一致性问题的终极指南
解决缓存与数据库的数据一致性问题的终极指南
191 63
|
16天前
|
缓存 物联网 数据库
InfluxDB vs TDengine :2025 年了,谁家用的数据库还不能高效读缓存?
在工业互联网和物联网的大数据应用场景中,实时数据的写入和查询性能至关重要。如何快速获取最新设备状态并实时处理数据,直接影响到业务的高效运转。本文将深入分析 TDengine 和 InfluxDB 在缓存机制上的差异,帮助读者更好地理解这两款主流时序数据库在性能优化方面的优劣。
44 1
|
1月前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
1月前
|
缓存 NoSQL 关系型数据库
mysql和缓存一致性问题
本文介绍了五种常见的MySQL与Redis数据同步方法:1. 双写一致性,2. 延迟双删策略,3. 订阅发布模式(使用消息队列),4. 基于事件的缓存更新,5. 缓存预热。每种方法的实现步骤、优缺点均有详细说明。
|
2月前
|
缓存 监控 算法
小米面试题:多级缓存一致性问题怎么解决
【10月更文挑战第23天】在现代分布式系统中,多级缓存架构因其能够显著提高系统性能和响应速度而被广泛应用。
62 3
|
2月前
|
缓存 弹性计算 NoSQL
新一期陪跑班开课啦!阿里云专家手把手带你体验高并发下利用云数据库缓存实现极速响应
新一期陪跑班开课啦!阿里云专家手把手带你体验高并发下利用云数据库缓存实现极速响应
|
2月前
|
消息中间件 缓存 中间件
缓存一致性问题,这么回答肯定没毛病!
缓存一致性问题,这么回答肯定没毛病!
|
2月前
|
存储 缓存 API
LangChain-18 Caching 将回答内容进行缓存 可在内存中或数据库中持久化缓存
LangChain-18 Caching 将回答内容进行缓存 可在内存中或数据库中持久化缓存
48 6
|
3月前
|
消息中间件 缓存 NoSQL
15)如何保证缓存和数据库之间的数据一致性
15)如何保证缓存和数据库之间的数据一致性
67 1
|
3月前
|
Oracle NoSQL 关系型数据库
主流数据库对比:MySQL、PostgreSQL、Oracle和Redis的优缺点分析
主流数据库对比:MySQL、PostgreSQL、Oracle和Redis的优缺点分析
614 2

推荐镜像

更多
下一篇
DataWorks