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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
简介: 【说明】  对于热点数据(经常被查询,但不经常被修改的数据),我们可以将其放入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
相关文章
|
17天前
|
SQL 缓存 关系型数据库
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
在40岁老架构师尼恩的读者交流群中,近期有小伙伴因未能系统梳理MySQL缓存机制而在美团面试中失利。为此,尼恩对MySQL的缓存机制进行了系统化梳理,包括一级缓存(InnoDB缓存)和二级缓存(查询缓存)。同时,他还将这些知识点整理进《尼恩Java面试宝典PDF》V175版本,帮助大家提升技术水平,顺利通过面试。更多技术资料请关注公号【技术自由圈】。
美团面试:Mysql 有几级缓存? 每一级缓存,具体是什么?
|
8天前
|
SQL 关系型数据库 MySQL
go语言数据库中mysql驱动安装
【11月更文挑战第2天】
22 4
|
6天前
|
SQL 关系型数据库 MySQL
12 PHP配置数据库MySQL
路老师分享了PHP操作MySQL数据库的方法,包括安装并连接MySQL服务器、选择数据库、执行SQL语句(如插入、更新、删除和查询),以及将结果集返回到数组。通过具体示例代码,详细介绍了每一步的操作流程,帮助读者快速入门PHP与MySQL的交互。
19 1
|
9天前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
15天前
|
缓存 NoSQL 关系型数据库
mysql和缓存一致性问题
本文介绍了五种常见的MySQL与Redis数据同步方法:1. 双写一致性,2. 延迟双删策略,3. 订阅发布模式(使用消息队列),4. 基于事件的缓存更新,5. 缓存预热。每种方法的实现步骤、优缺点均有详细说明。
|
15天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第27天】本文深入探讨了MySQL的索引策略和查询性能调优技巧。通过介绍B-Tree索引、哈希索引和全文索引等不同类型,以及如何创建和维护索引,结合实战案例分析查询执行计划,帮助读者掌握提升查询性能的方法。定期优化索引和调整查询语句是提高数据库性能的关键。
76 1
|
17天前
|
关系型数据库 MySQL Linux
在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。
本文介绍了在 CentOS 7 中通过编译源码方式安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务、登录设置等。同时,文章还对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据需求选择最合适的方法。通过具体案例,展示了编译源码安装的灵活性和定制性。
59 2
|
2天前
|
运维 关系型数据库 MySQL
安装MySQL8数据库
本文介绍了MySQL的不同版本及其特点,并详细描述了如何通过Yum源安装MySQL 8.4社区版,包括配置Yum源、安装MySQL、启动服务、设置开机自启动、修改root用户密码以及设置远程登录等步骤。最后还提供了测试连接的方法。适用于初学者和运维人员。
26 0
|
16天前
|
监控 关系型数据库 MySQL
数据库优化:MySQL索引策略与查询性能调优实战
【10月更文挑战第26天】数据库作为现代应用系统的核心组件,其性能优化至关重要。本文主要探讨MySQL的索引策略与查询性能调优。通过合理创建索引(如B-Tree、复合索引)和优化查询语句(如使用EXPLAIN、优化分页查询),可以显著提升数据库的响应速度和稳定性。实践中还需定期审查慢查询日志,持续优化性能。
46 0
|
1月前
|
存储 关系型数据库 MySQL
Mysql(4)—数据库索引
数据库索引是用于提高数据检索效率的数据结构,类似于书籍中的索引。它允许用户快速找到数据,而无需扫描整个表。MySQL中的索引可以显著提升查询速度,使数据库操作更加高效。索引的发展经历了从无索引、简单索引到B-树、哈希索引、位图索引、全文索引等多个阶段。
61 3
Mysql(4)—数据库索引