| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 大数据 -> 掘地三尺搞定 Redis 与 MySQL 数据一致性问题 -> 正文阅读 |
|
[大数据]掘地三尺搞定 Redis 与 MySQL 数据一致性问题 |
1. 什么是数据库与缓存一致性数据一致性指的是:
反推缓存与数据库不一致:
把 Redis 作为缓存的时候,当数据发生改变我们需要双写来保证缓存与数据库的数据一致。 数据库跟缓存,毕竟是两套系统,如果要保证强一致性,势必要引入 如果真的对数据的一致性要求这么高,那引入缓存是否真的有必要呢? 2. 缓存的使用策略在使用缓存时,通常有以下几种缓存使用策略用于提升系统性能:
2.1 Cache-Aside (旁路缓存)所谓「旁路缓存」,就是读取缓存、读取数据库和更新缓存的操作都在应用系统来完成,业务系统最常用的缓存策略。 2.1.1 读取数据 读取数据逻辑如下:
时序图如下: 优点
实现的伪代码如下:
缺点 由于数据仅在缓存未命中后才加载到缓存中,因此初次调用的数据请求响应时间会增加一些开销,因为需要额外的缓存填充和数据库查询耗时。 2.1.2 更新数据 使用
使用 我们应该给缓存设置一个过期时间,这个是保证最终一致性的解决方案。 如果过期时间太短,应用程序会不断地从数据库中查询数据。同样,如果过期时间过长,并且更新时没有使缓存失效,缓存的数据很可能是脏数据。 最常用的方式是删除缓存使缓存数据失效。
性能问题 当缓存的更新成本很高,需要访问多张表联合计算,建议直接删除缓存,而不是更新缓存数据来保证一致性。 安全问题 在高并发场景下,可能会造成查询查到的数据是旧值,具体待会码哥会分析,大家别急。 2.2 Read-Through(直读)当缓存未命中,也是从数据库加载数据,同时写到缓存中并返回给应用系统。 虽然 而 Read-Through 将获取数据存储中的值的责任转移到了缓存提供者身上。 Read-Through 实现了关注点分离原则。代码只与缓存交互,由缓存组件来管理自身与数据库之间的数据同步。 2.3 Write-Through 同步直写与 Read-Through 类似,发生写请求时,Write-Through 将写入责任转移到缓存系统,由缓存抽象层来完成缓存数据和数据库数据的更新,时序流程图如下:
优缺点 单独直接使用该策略是没啥意义的,因为该策略要先写缓存,再写数据库,对写入操作带来了额外延迟。 当 这个策略颠倒了 优点
缺点 不经常请求的数据也会写入缓存,从而导致缓存更大、成本更高。 2.4 Write-Behind这个图一眼看去似乎与 这意味着缓存系统将异步更新数据库数据,应用系统只与缓存系统交互。 应用程序不必等待数据库更新完成,从而提高应用程序性能,因为对数据库的更新是最慢的操作。 这种策略下,缓存与数据库的一致性不强,对一致性高的系统不建议使用。 3. 旁路缓存下的一致性问题分析业务场景用的最多的就是 重点是写操作,数据库和缓存都需要修改,而两者就会存在一个先后顺序,可能会导致数据不再一致。针对写,我们需要考虑两个问题:
将这两个问题排列组合,会出现四种方案:
接下来的分析大家不必死记硬背,关键在于在推演的过程中大家只需要考虑以下两个场景会不会带来严重问题即可:
你猜? 既然第一个都失败了,第二个就不用执行了,直接在第一步返回 50x 等异常信息即可,不会出现不一致问题。 只有第一个成功,第二个失败才让人头痛,想要保证他们的原子性,就涉及到分布式事务的范畴了。 3.1 先更新缓存,再更新数据库如果先更新缓存成功,写数据库失败,就会导致缓存是最新数据,数据库是旧数据,那缓存就是脏数据了。 之后,其他查询立马请求进来的时候就会获取这个数据,而这个数据数据库中却不存在。 数据库都不存在的数据,缓存并返回客户端就毫无意义了。 该方案直接 3.2 先更新数据库,再更新缓存一切正常的情况如下:
更新缓存失败 这时候我们来推断下,假如这两个操作的原子性被破坏:第一步成功,第二步失败会导致什么问题? 会导致数据库是最新数据,缓存是旧数据,出现一致性问题。 该图我就不画了,与上一个图类似,对调下 Redis 和 MySQL 的位置即可。 高并发场景 谢霸歌经常 996,腰酸脖子疼,bug 越写越多,想去按摩推拿放提升下编程技巧。 疫情影响,单子来之不易,高端会所的技师都争先恐后想接这一单,高并发啊兄弟们。 在进店以后,前台会将顾客信息录入系统,执行 如下图所示:
最后发现,数据库的值 = 520 号技师在缓存中的最新数据被 98 号技师的旧数据覆盖了。 所以,在高并发的场景中,多线程同时写数据再写缓存,就会出现缓存是旧值,数据库是最新值的不一致情况。 该方案直接 pass。
3.3 先删缓存,再更新数据库按照「码哥」前面说的套路,假设第一个操作成功,第二个操作失败推断下会发生什么?高并发场景下又会发生什么? 第二步写数据库失败 假设现在有两个请求:写请求 A,读请求 B。 写请求 A 第一步先删除缓存成功,写数据到数据库失败,就会导致该次写数据丢失,数据库保存的是旧值。 接着另一个读请 B 求进来,发现缓存不存在,从数据库读取旧数据并写到缓存中。 高并发下的问题
**这样子会出现缓存的是旧数据,在缓存过期之前无法读取到最数据。**肖菜鸡本就被 98 号技师接单了,但是大堂经理却以为没人接待。 该方案 pass,因为第一步成功,第二步失败,会造成数据库是旧数据,缓存中没数据继续从数据库读取旧值写入缓存,造成数据不一致,还会多一次 cahche。 不论是异常情况还是高并发场景,会导致数据不一致。 miss。 3.4 先更新数据库,再删缓存经过前面的三个方案,全都被 pass 了,分析下最后的方案到底行不行。 按照「套路」,分别判断异常和高并发会造成什么问题。 该策略可以知道,在写数据库阶段失败的话就直返返回客户端异常,不需要执行缓存操作了。 所以第一步失败不会出现数据不一致的情况。 删缓存失败 重点在于第一步写最新数据到数据库成功,删除缓存失败怎么办? 可以把这两个操作放在一个事务中,当缓存删除失败,那就把写数据库回滚。
如果不回滚,那就出现数据库是新数据,缓存还是旧数据,数据不一致了,咋办? 所以,我们要想办法让缓存删除成功,不然只能等到有效期失效那可不行。 使用重试机制。 比如重试三次,三次都失败则记录日志到数据库,使用分布式调度组件 xxl-job 等实现后续的处理。 在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。 亦或是利用 Canal 框架订阅 MySQL binlog 日志,监听对应的更新请求,执行删除对应缓存操作。 高并发场景 再来分析下高并发读写会有什么问题……
读请求可能出现少量读取旧数据的情况,但是很快旧数据就会被删除,之后的请求都能获取最新数据,问题不大。 还有一种比较极端的情况,缓存自动失效的时候又遇到了高并发读写的情况,假设这会有两个请求,一个线程 A 做查询操作,一个线程 B 做更新操作,那么会有如下情形产生:
不要慌,发生这个情况的概率微乎其微,发生上述情况的必要条件是:
通常 MySQL 单机的 QPS 大概 5K 左右,而 TPS 大概 1k 左右,(ps:Tomcat 的 QPS 4K 左右,TPS = 1k 左右)。 数据库读操作是远快于写操作的(正是因为如此,才做读写分离),所以步骤(3)要比步骤(2)更快这个情景很难出现,同时还要配合缓存刚好失效。 所以,在用旁路缓存策略的时候,对于写操作推荐使用:先更新数据库,再删除缓存。 4. 一致性解决方案有哪些?最后,针对 Cache-Aside (旁路缓存) 策略,写操作使用先更新数据库,再删除缓存的情况下,我们来分析下数据一致性解决方案都有哪些? 4.1 缓存延时双删如果采用先删除缓存,再更新数据库如何避免出现脏数据?
这样子最多只会出现 500 毫秒的脏数据读取时间。关键是这个休眠时间怎么确定呢? 延迟时间的目的就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 所以我们需要自行评估项目的读数据业务逻辑的耗时,在读耗时的基础上加几百毫秒作为延迟时间即可。 4.2 删除缓存重试机制
使用重试机制,保证删除缓存成功。 比如重试三次,三次都失败则记录日志到数据库并发送警告让人工介入。 在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。 第(5)步如果删除失败且未达到重试最大次数则将消息重新入队,直到删除成功,否则就记录到数据库,人工介入。 该方案有个缺点,就是对业务代码中造成侵入,于是就有了下一个方案,启动一个专门订阅 数据库 binlog 的服务读取需要删除的数据进行缓存删除操作。 4.3 读取 binlog 异步删除
总结缓存策略的最佳实践是 **Cache Aside Pattern。**分别分为读缓存最佳实践和写缓存最佳实践。 读缓存最佳实践:先读缓存,命中则返回;未命中则查询数据库,再写到数据库。 写缓存最佳实践:
在以上最佳实践下,为了尽可能保证缓存与数据库的一致性,我们可以采用延迟双删。 防止删除失败,我们采用异步重试机制保证能正确删除,异步机制我们可以发送删除消息到 mq 消息中间件,或者利用 canal 订阅 MySQL binlog 日志监听写请求删除对应缓存。 那么,如果我非要保证绝对一致性怎么办,先给出结论: 没有办法做到绝对的一致性,这是由 CAP 理论决定的,缓存系统适用的场景就是非强一致性的场景,所以它属于 CAP 中的 AP。 所以,我们得委曲求全,可以去做到 BASE 理论中说的最终一致性。 其实一旦在方案中使用了缓存,那往往也就意味着我们放弃了数据的强一致性,但这也意味着我们的系统在性能上能够得到一些提升。 所谓 tradeoff 正是如此。
?
|
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/23 12:52:20- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |