面试还不懂 Redis 与 MySQL 数据一致性,看这篇就够了

news2024/11/14 17:19:50

1. 什么是数据库与缓存一致性

数据一致性指的是:

  • 缓存中存有数据,缓存的数据值 = 数据库中的值;

  • 缓存中没有该数据,数据库中的值 = 最新值。

反推缓存与数据库不一致:

  • 缓存的数据值 ≠ 数据库中的值;

  • 缓存或者数据库存在旧的数据,导致线程读取到旧数据。

为何会出现数据一致性问题呢?

把 Redis 作为缓存的时候,当数据发生改变我们需要双写来保证缓存与数据库的数据一致。

数据库跟缓存,毕竟是两套系统,如果要保证强一致性,势必要引入 2PC 或 Paxos 等分布式一致性协议,或者分布式锁等等,这个在实现上是有难度的,而且一定会对性能有影响。

如果真的对数据的一致性要求这么高,那引入缓存是否真的有必要呢?

2. 缓存的使用策略

在使用缓存时,通常有以下几种缓存使用策略用于提升系统性能:

  • Cache-Aside Pattern(旁路缓存,业务系统常用)

  • Read-Through Pattern

  • Write-Through Pattern

  • Write-Behind Pattern

2.1 Cache-Aside (旁路缓存)

所谓「旁路缓存」,就是读取缓存、读取数据库和更新缓存的操作都在应用系统来完成业务系统最常用的缓存策略

2.1.1 读取数据

图片

读取数据逻辑如下:

  1. 当应用程序需要从数据库读取数据时,先检查缓存数据是否命中。

  2. 如果缓存未命中,则查询数据库获取数据,同时将数据写到缓存中,以便后续读取相同数据会命中缓存,最后再把数据返回给调用者。

  3. 如果缓存命中,直接返回。

时序图如下:

图片

旁路缓存读时序图

优点

  • 缓存中仅包含应用程序实际请求的数据,有助于保持缓存大小的成本效益。

  • 实现简单,并且能获得性能提升。

实现的伪代码如下:

String cacheKey = "公众号:码哥字节";
String cacheValue = redisCache.get(cacheKey);
//缓存命中
if (cacheValue != null) {
  return cacheValue;
} else {
  //缓存缺失, 从数据库获取数据
  cacheValue = getDataFromDB();
  // 将数据写到缓存中
  redisCache.put(cacheValue)
}

缺点

由于数据仅在缓存未命中后才加载到缓存中,因此初次调用的数据请求响应时间会增加一些开销,因为需要额外的缓存填充和数据库查询耗时。

2.1.2 更新数据

使用 cache-aside 模式写数据时,如下流程。

图片

旁路缓存写数据

  1. 写数据到数据库;

  2. 将缓存中的数据失效或者更新缓存数据;

使用 cache-aside 时,最常见的写入策略是直接将数据写入数据库,但是缓存可能会与数据库不一致。

我们应该给缓存设置一个过期时间,这个是保证最终一致性的解决方案。

如果过期时间太短,应用程序会不断地从数据库中查询数据。同样,如果过期时间过长,并且更新时没有使缓存失效,缓存的数据很可能是脏数据。

最常用的方式是删除缓存使缓存数据失效

为啥不是更新缓存呢?

性能问题

当缓存的更新成本很高,需要访问多张表联合计算,建议直接删除缓存,而不是更新缓存数据来保证一致性。

安全问题

在高并发场景下,可能会造成查询查到的数据是旧值,具体待会码哥会分析,大家别急。

2.2 Read-Through(直读)

当缓存未命中,也是从数据库加载数据,同时写到缓存中并返回给应用系统。

虽然 read-through 和 cache-aside 非常相似,在 cache-aside 中应用系统负责从数据库获取数据和填充缓存。

而 Read-Through 将获取数据存储中的值的责任转移到了缓存提供者身上。

图片

Read-Through

Read-Through 实现了关注点分离原则。代码只与缓存交互,由缓存组件来管理自身与数据库之间的数据同步。

2.3 Write-Through 同步直写

与 Read-Through 类似,发生写请求时,Write-Through 将写入责任转移到缓存系统,由缓存抽象层来完成缓存数据和数据库数据的更新,时序流程图如下:

图片

Write-Through

Write-Through 的主要好处是应用系统的不需要考虑故障处理和重试逻辑,交给缓存抽象层来管理实现。

优缺点

单独直接使用该策略是没啥意义的,因为该策略要先写缓存,再写数据库,对写入操作带来了额外延迟。

Write-Through 与 Read-Through 配合使用,就能成分发挥 Read-Through 的优势,同时还能保证数据一致性,不需要考虑如何将缓存设置失效。

图片

Write-Through

这个策略颠倒了 Cache-Aside 填充缓存的顺序,并不是在缓存未命中后延迟加载到缓存,而是在数据先写缓存,接着由缓存组件将数据写到数据库

优点

  • 缓存与数据库数据总是最新的;

  • 查询性能最佳,因为要查询的数据有可能已经被写到缓存中了。

缺点

不经常请求的数据也会写入缓存,从而导致缓存更大、成本更高。

2.4 Write-Behind

这个图一眼看去似乎与 Write-Through 一样,其实不是的,区别在于最后一个箭头的箭头:它从实心变为线。

这意味着缓存系统将异步更新数据库数据,应用系统只与缓存系统交互

应用程序不必等待数据库更新完成,从而提高应用程序性能,因为对数据库的更新是最慢的操作。

图片

Write-Behind

这种策略下,缓存与数据库的一致性不强,对一致性高的系统不建议使用。

3. 旁路缓存下的一致性问题分析

业务场景用的最多的就是 Cache-Aside (旁路缓存) 策略,在该策略下,客户端对数据的读取流程是先读取缓存,如果命中则返回;未命中,则从数据库读取并把数据写到缓存中,所以读操作不会导致缓存与数据库的不一致。

重点是写操作,数据库和缓存都需要修改,而两者就会存在一个先后顺序,可能会导致数据不再一致。针对写,我们需要考虑两个问题:

  • 先更新缓存还是更新数据库?

  • 当数据发生变化时,选择修改缓存(update),还是删除缓存(delete)?

将这两个问题排列组合,会出现四种方案:

  1. 先更新缓存,再更新数据库;

  2. 先更新数据库,再更新缓存;

  3. 先删除缓存,再更新数据库;

  4. 先更新数据库,再删除缓存。

接下来的分析大家不必死记硬背,关键在于在推演的过程中大家只需要考虑以下两个场景会不会带来严重问题即可:

  • 其中第一个操作成功,第二个失败会导致什么问题?

  • 在高并发情况下会不会造成读取数据不一致?

为啥不考虑第一个失败,第二个成功的情况呀?

你猜?

既然第一个都失败了,第二个就不用执行了,直接在第一步返回 50x 等异常信息即可,不会出现不一致问题。

只有第一个成功,第二个失败才让人头痛,想要保证他们的原子性,就涉及到分布式事务的范畴了。

3.1 先更新缓存,再更新数据库

图片

先更新缓存再更新数据库

如果先更新缓存成功,写数据库失败,就会导致缓存是最新数据,数据库是旧数据,那缓存就是脏数据了。

之后,其他查询立马请求进来的时候就会获取这个数据,而这个数据数据库中却不存在。

数据库都不存在的数据,缓存并返回客户端就毫无意义了。

该方案直接 Pass

3.2 先更新数据库,再更新缓存

一切正常的情况如下:

  • 先写数据库,成功;

  • 再 update 缓存,成功。

更新缓存失败

这时候我们来推断下,假如这两个操作的原子性被破坏:第一步成功,第二步失败会导致什么问题?

会导致数据库是最新数据,缓存是旧数据,出现一致性问题。

该图我就不画了,与上一个图类似,对调下 Redis 和 MySQL 的位置即可。

高并发场景

谢霸歌经常 996,腰酸脖子疼,bug 越写越多,想去按摩推拿放提升下编程技巧。

疫情影响,单子来之不易,高端会所的技师都争先恐后想接这一单,高并发啊兄弟们。

在进店以后,前台会将顾客信息录入系统,执行 set xx的服务技师 = 待定的初始值表示目前无人接待保存到数据库和缓存中,之后再安排技师按摩服务。

如下图所示:

图片

高并发先更新数据库,再更新缓存

  1. 98 号技师先下手为强,向系统发送 set 谢霸歌的服务技师 = 98 的指令写入数据库,这时候系统的网络出现波动,卡顿了,数据还没来得及写到缓存

  2. 接下来,520 号技师也向系统发送 set 谢霸哥的服务技师 = 520写到数据库中,并且也把这个数据写到缓存中了。

  3. 这时候之前的 98 号技师的写缓存请求开始执行,顺利将数据 set 谢霸歌的服务技师 = 98 写到缓存中。

最后发现,数据库的值 = set 谢霸哥的服务技师 = 520,而缓存的值= set 谢霸歌的服务技师 = 98

520 号技师在缓存中的最新数据被 98 号技师的旧数据覆盖了。

所以,在高并发的场景中,多线程同时写数据再写缓存,就会出现缓存是旧值,数据库是最新值的不一致情况。

该方案直接 pass。

如果第一步就失败,直接返回 50x 异常,并不会出现数据不一致。

3.3 先删缓存,再更新数据库

按照「码哥」前面说的套路,假设第一个操作成功,第二个操作失败推断下会发生什么?高并发场景下又会发生什么?

第二步写数据库失败

假设现在有两个请求:写请求 A,读请求 B。

写请求 A 第一步先删除缓存成功,写数据到数据库失败,就会导致该次写数据丢失,数据库保存的是旧值

接着另一个读请 B 求进来,发现缓存不存在,从数据库读取旧数据并写到缓存中。

高并发下的问题

图片

先删缓存,再写数据库

  1. 还是 98 号技师先下手为强,系统接收请求把缓存数据删除,当系统准备将 set 肖菜鸡的服务技师 = 98写到数据库的时候发生卡顿,来不及写入。

  2. 这时候,大堂经理向系统执行读请求,查下肖菜鸡有没有技师接待,方便安排技师服务,系统发现缓存中没数据,于是乎就从数据库读取到旧数据 set 肖菜鸡的服务技师 = 待定,并写到缓存中。

  3. 这时候,原先卡顿的 98 号技师写数据 set 肖菜鸡的服务技师 = 98到数据库的操作完成。

这样子会出现缓存的是旧数据,在缓存过期之前无法读取到最数据。肖菜鸡本就被 98 号技师接单了,但是大堂经理却以为没人接待。

该方案 pass,因为第一步成功,第二步失败,会造成数据库是旧数据,缓存中没数据继续从数据库读取旧值写入缓存,造成数据不一致,还会多一次 cahche。

不论是异常情况还是高并发场景,会导致数据不一致。miss。

3.4 先更新数据库,再删缓存

经过前面的三个方案,全都被 pass 了,分析下最后的方案到底行不行。

按照「套路」,分别判断异常和高并发会造成什么问题。

该策略可以知道,在写数据库阶段失败的话就直返返回客户端异常,不需要执行缓存操作了。

所以第一步失败不会出现数据不一致的情况。

删缓存失败

重点在于第一步写最新数据到数据库成功,删除缓存失败怎么办?

可以把这两个操作放在一个事务中,当缓存删除失败,那就把写数据库回滚。

高并发场景下不合适,容易出现大事务,造成死锁问题。

如果不回滚,那就出现数据库是新数据,缓存还是旧数据,数据不一致了,咋办?

所以,我们要想办法让缓存删除成功,不然只能等到有效期失效那可不行。

使用重试机制。

比如重试三次,三次都失败则记录日志到数据库,使用分布式调度组件 xxl-job 等实现后续的处理。

在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。

亦或是利用 Canal 框架订阅 MySQL binlog 日志,监听对应的更新请求,执行删除对应缓存操作。

高并发场景

再来分析下高并发读写会有什么问题……

图片

先写数据库后删缓存

  1. 98 号技师先下手为强,接下肖菜鸡的这笔生意,数据库执行 set 肖菜鸡的服务技师 = 98;还是网络卡顿了下,没来得及执行删除缓存操作

  2. 主管 Candy 向系统执行读请求,查下肖菜鸡有没有技师接待,发现缓存中有数据 肖菜鸡的服务技师 = 待定,直接返回信息给客户端,主管以为没人接待。

  3. 原先 98 号技师接单,由于卡顿没删除缓存的操作现在执行删除成功。

读请求可能出现少量读取旧数据的情况,但是很快旧数据就会被删除,之后的请求都能获取最新数据,问题不大。

还有一种比较极端的情况,缓存自动失效的时候又遇到了高并发读写的情况,假设这会有两个请求,一个线程 A 做查询操作,一个线程 B 做更新操作,那么会有如下情形产生:

图片

缓存忽然失效

  1. 缓存的过期时间到期,缓存失效。

  2. 线程 A 读请求读取缓存,没命中,则查询数据库得到一个旧的值(因为 B 会写新值,相对而言就是旧的值了),准备把数据写到缓存时发送网络问题卡顿了

  3. 线程 B 执行写操作,将新值写数据库。

  4. 线程 B 执行删除缓存。

  5. 线程 A 继续,从卡顿中醒来,把查询到的旧值写到入缓存。

码哥,这咋玩,还是出现了不一致的情况啊。

不要慌,发生这个情况的概率微乎其微,发生上述情况的必要条件是:

  1. 步骤 (3)的写数据库操作要比步骤(2)读操作耗时短速度快,才可能使得步骤(4)先于步骤(5)。

  2. 缓存刚好到达过期时限。

通常 MySQL 单机的 QPS 大概 5K 左右,而 TPS 大概 1k 左右,(ps:Tomcat 的 QPS 4K 左右,TPS = 1k 左右)。

数据库读操作是远快于写操作的(正是因为如此,才做读写分离),所以步骤(3)要比步骤(2)更快这个情景很难出现,同时还要配合缓存刚好失效。

所以,在用旁路缓存策略的时候,对于写操作推荐使用:先更新数据库,再删除缓存。

4. 一致性解决方案有哪些?

最后,针对 Cache-Aside (旁路缓存) 策略,写操作使用先更新数据库,再删除缓存的情况下,我们来分析下数据一致性解决方案都有哪些?

4.1 缓存延时双删

如果采用先删除缓存,再更新数据库如何避免出现脏数据?

采用延时双删策略。

  1. 先删除缓存。

  2. 写数据库。

  3. 休眠 500 毫秒,再删除缓存。

这样子最多只会出现 500 毫秒的脏数据读取时间。关键是这个休眠时间怎么确定呢?

延迟时间的目的就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

所以我们需要自行评估项目的读数据业务逻辑的耗时,在读耗时的基础上加几百毫秒作为延迟时间即可

4.2 删除缓存重试机制

缓存删除失败怎么办?比如延迟双删的第二次删除失败,那岂不是无法删除脏数据。

使用重试机制,保证删除缓存成功。

比如重试三次,三次都失败则记录日志到数据库并发送警告让人工介入。

在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。

图片

重试机制

第(5)步如果删除失败且未达到重试最大次数则将消息重新入队,直到删除成功,否则就记录到数据库,人工介入。

该方案有个缺点,就是对业务代码中造成侵入,于是就有了下一个方案,启动一个专门订阅 数据库 binlog 的服务读取需要删除的数据进行缓存删除操作。

4.3 读取 binlog 异步删除

图片

binlog异步删除

  1. 更新数据库;

  2. 数据库会把操作信息记录在 binlog 日志中;

  3. 使用 canal 订阅 binlog 日志获取目标数据和 key;

  4. 缓存删除系统获取 canal 的数据,解析目标 key,尝试删除缓存。

  5. 如果删除失败则将消息发送到消息队列;

  6. 缓存删除系统重新从消息队列获取数据,再次执行删除操作。

总结

缓存策略的最佳实践是 Cache Aside Pattern。分别分为读缓存最佳实践和写缓存最佳实践。

读缓存最佳实践:先读缓存,命中则返回;未命中则查询数据库,再写到缓存中。

写缓存最佳实践:

  • 先写数据库,再操作缓存;

  • 直接删除缓存,而不是修改,因为当缓存的更新成本很高,需要访问多张表联合计算,建议直接删除缓存,而不是更新,另外,删除缓存操作简单,副作用只是增加了一次 chache miss,建议大家使用该策略。

在以上最佳实践下,为了尽可能保证缓存与数据库的一致性,我们可以采用延迟双删。

防止删除失败,我们采用异步重试机制保证能正确删除,异步机制我们可以发送删除消息到 mq 消息中间件,或者利用 canal 订阅 MySQL binlog 日志监听写请求删除对应缓存。

那么,如果我非要保证绝对一致性怎么办,先给出结论:

没有办法做到绝对的一致性,这是由 CAP 理论决定的,缓存系统适用的场景就是非强一致性的场景,所以它属于 CAP 中的 AP。

所以,我们得委曲求全,可以去做到 BASE 理论中说的最终一致性

其实一旦在方案中使用了缓存,那往往也就意味着我们放弃了数据的强一致性,但这也意味着我们的系统在性能上能够得到一些提升。

所谓 tradeoff 正是如此。

如果感觉本文对你有帮助,点赞关注支持一下,想要了解更多Java后端,大数据,算法领域最新资讯可以关注我公众号【架构师老毕】私信666还可获取更多Java后端,大数据,算法PDF+大厂最新面试题整理+视频精讲

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/802042.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【雕爷学编程】MicroPython动手做(11)——搭建掌控板IDE开发环境四种

为了能够打好基础,系统学习MicroPython,特地入手了二块掌控板 知识点:什么是掌控板? 掌控板是一块普及STEAM创客教育、人工智能教育、机器人编程教育的开源智能硬件。它集成ESP-32高性能双核芯片,支持WiFi和蓝牙双模通…

数字人解决方案——NeRF实现实时对话数字人环境配置与源码

前言 1.这是一个能实时对话的虚拟数字人demo,使用的是NeRF(Neural Radiance Fields),训练方式可以看看我前面的博客。 2.文本转语音是用了VITS语音合成,项目git:https://github.com/jaywalnut310/vits . 3.语言模型是用了新开…

Oracle 最高安全架构

​在当今世界中,数据库是存储敏感信息的宝贵资料库,攻击者总是在寻找目标。这导致网络安全威胁的增加,因此有必要采取适当的保护措施。Oracle Maximum Security Architecture(MSA)就是一种提供数据库端到端安全的解决方…

YOLOv6 论文学习

1. 解决了什么问题? 吸收了学术圈和工业界最新的目标检测方法,包括网络结构、训练策略、测试技巧、量化和优化方法。 作者有如下几点发现: 目前还没有人深入研究 RepVGG 重参数化对检测任务的影响。直接缩放 RepVGG 模块的效果并不好&…

苹果电脑系统优化工具:Ventura Cache Cleaner for mac

Ventura Cache Cleaner for Mac是一款专门为苹果电脑开发的系统优化工具,旨在帮助用户清理和优化Mac电脑,提高系统性能和速度。该软件由美国公司Northern Softworks开发,已经推出了多个版本,适用于不同版本的Mac操作系统。 Ventu…

pdf文件太大了不能上传怎么办?这几招值得学

PDF文件是一种常见的文档格式,但有时会遇到文件太大无法上传的问题,这时候简单的做法就是直接压缩文件的大小,但很多朋友还不知道怎么操作,下面就给大家介绍几个简单好用的,一起来看看吧。 工具一、嗨格式压缩大师 这…

二十七、响应式布局练习- 美图导航栏1

目录: 准备工作界面结构开发样式设计 - style.less 一、准备工作 开发响应式设计的网页需要注意以下几点: 1.移动端优先 - 先写完移动端的界面,再写网页。 因为现在都是手机用的比较多; 2.渐进增强 - 当移动端写完后,…

【Spring Cloud Gateway 新一代网关】—— 每天一点小知识

💧 S p r i n g C l o u d G a t e w a y 新一代网关 \color{#FF1493}{Spring Cloud Gateway 新一代网关} SpringCloudGateway新一代网关💧 🌷 仰望天空,妳我亦是行人.✨ 🦄 个人主页——微风撞见云的博客&a…

js学习 记录(二)

js学习 记录(二) 匿名函数 函数表达式

节省近2400亿,中国芯片拿下7纳米工艺,外媒:挡不住中国芯了

从去年至今中国进口的芯片减少了1400亿颗,芯片进口金额减少了300多亿美元(约合近2400亿元人民币),尤为让人高兴的是近期频频传出中国或已搞定接近7纳米的N1工艺,并将为一家中国芯片企业生产芯片。 一、中国芯片夯实成熟…

InnoDB 的隔离级别是如何实现的

点击上方↑“追梦 Java”关注,一起追梦! MySQL 数据库 InnoDB 存储引擎的隔离级别是通过锁和 MVCC 的机制实现的。 1 了解 MySQL 中锁的机制 锁是用于避免不同事务对共享资源的并发访问的产生读一致性的问题的机制。 1、表锁和行锁 InnoDB 存储引擎支持行…

如何与 Zappos 建立 EDI 连接?

Zappos 是一家享誉全球的知名在线鞋类和服饰零售商,经营范围涵盖各类时尚品牌的鞋类、服饰及配饰等,使命是为广大消费者提供方便、愉悦、优质的购物体验,让每一位顾客都能找到心仪的产品。多年来,Zappos 卖场凭借卓越的服务与产品…

通用VS垂直,讯飞星火与网易子曰不同的“大模型解法”

配图来自Canva可画 随着大模型商业化应用的提速,全世界各国都开始孵化和孕育各自的行业大模型。在此背景下,国内各个细分行业的垂直大模型,日益受到产业界的关注和重视。 相比通用大模型,垂直大模型具有门槛较低、数据质量较好且…

安装虚拟机

分区 根分区5个G 同理 交换分区 最后一个分区,默认所有

串口环形缓冲区

文章目录 一、串口环形缓冲区概念二、STC12例程(1)环形串口缓冲区结构体(2)串口环形缓冲区存和取数据(3)完整工程demo 一、串口环形缓冲区概念 串口环形缓冲区应用于嵌入式、物联网开发中处理接收串口数据…

windows C++多线程同步<3>-互斥量

windows C多线程同步<3>-互斥量 概念,如下图: 另外就是互斥对象谁拥有,谁释放 那么一个线程允许多次获取互斥对象吗? 答案是允许,但是申请多次就要释放多次,否则其他线程获取不到互…

在线进销存-亿发移动进销存管理系统,助力批发零售企业线上线下同步经营

随着移动互联网的蓬勃发展,商品进销存管理日益变得复杂而关键,数字化转型已经成为批发零售企业增强竞争力的有效工具。移动进销存管理系统为批发零售企业提供一体化 解决方案,实现线上线下同步经营,帮助企业实现对商品的有效管理&…

TDengine Cloud 加入 AWS 合作伙伴网络,助力出海企业数字化转型

近日,全托管的时序数据云平台 TDengine Cloud 正式入驻 AWS Marketplace(海外区),用户可通过 AWS Marketplace 轻松实现 TDengine Cloud 的订阅与部署,以最低的成本搭建最高效的数据处理架构。此外,早在 20…

python新手如何系统学习,走过这4个阶段成为高手

目录 python初级阶段学习 python中级阶段学习 python高级阶段学习 python进阶阶段学习 Python是一种简洁而强大的编程语言,广泛应用于软件开发、数据科学、人工智能等领域。很多新手如何系统学习python,今天我们从4个阶段来学习,就能成为高…

SpringBoot实现的旅游酒店管理系统源码附带视频运行教程

基于SpringBootMybatissThymeleaf框架系统主要有以下功能:分为前后台 前台用来展示数据,预定酒店、预定旅游、预定车票、购买保险等 后台:旅游路线管理、景点管理、酒店管理、车票管理、保险管理、攻略管理、留言管理、订单管理等后台管理员 …