缓存数据一致性探究

缓存数据一致性探究

图片

缓存是一种较低成本提升系统性能的方式,自它面世第一天起就备受广大开发者的喜爱。然而正如《人月神话》中的那句经典的“没有银弹”中所说,软件工程的设计没有银弹。

就像每一次发布上线修复问题的同时,也极易引入新的问题,自缓存诞生的第一天起,缓存与数据库的数据一致性问题就深深困扰着开发者们。

关键词:原子性、事务性、数据一致性、双写一致性

缓存的查询

先查询缓存,如果查询失败,那么去查询DB,之后重建缓存,基本上不存在异议。

缓存的更新

先更新DB还是先更新缓存?是更新缓存还是删除缓存?在常规情况下,怎么操作都可以,但一旦面对高并发场景,就值得细细思量了。

1、先更新数据库再更新缓存

线程A:更新数据库(第1s)——> 更新缓存(第10s)

线程B:更新数据库 (第3s)——> 更新缓存(第5s)

并发场景下,这样的情况是很容易出现的,每个线程的操作先后顺序不同,这样就导致请求B的缓存值被请求A给覆盖了,数据库中是线程B的新值,缓存中是线程A的旧值,并且会一直这么脏下去直到缓存失效(如果你设置了过期时间的话)。

图片

2、先更新缓存再更新数据库

线程A:更新缓存(第1s)——> 更新数据库(第10s)

线程B: 更新缓存(第3s)——> 更新数据库(第5s)

和前面一种情况相反,缓存中是线程B的新值,而数据库中是线程A的旧值。

图片

前两种方式之所以会在并发场景下出现异常,本质上是因为更新缓存和更新数据库是两个操作,我们没有办法控制并发场景下两个操作之间先后顺序,也就是先开始操作的线程先完成自己的工作。

如果把它化简,更新时只更新数据库,同时删除缓存。等待下一次查询时命中不到缓存,再去重建缓存,是不是就解决了这个问题?

基于此,后面的两种方案应运而生。

3、先删除缓存再更新数据库

通过这种方式,我们很惊喜地发现,前面困扰我们的并发场景的问题确实被解决了!两个线程都只修改数据库,不管谁先,数据库以之后修改的线程为准。

但这个时候,我们来思考另一个场景:两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的。很显然,这种状况也不是我们想要的。

图片



延时双删

在这种方案下,拓展出了延时双删的解决手段。

1.删除缓存

2.更新数据库

3.睡眠一段时间

4.再次删除缓存

加了个睡眠时间,主要是为了确保请求 A 在睡眠的时候,请求 B 能够在这这一段时间完成「从数据库读取数据,再把缺失的缓存写入缓存」的操作,然后请求 A 睡眠完,再删除缓存。

所以,请求 A 的睡眠时间就需要大于请求 B 「从数据库读取数据 + 写入缓存」的时间。

但是具体睡眠多久其实是个玄学,很难评估出来,所以这个方案也只是尽可能保证一致性而已,极端情况下,依然也会出现缓存不一致的现象。

因此,还是不太建议这种方案。

4、先更新数据库再删除缓存(cache aside)

这种方式,在方案3的基础上,又将二者的顺序进行了调换。我们再把前面的场景在这种方案下进行验证:一个是查询操作,一个是更新操作的并发,我们先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会方案3一样,后续的查询操作一直在取老的数据。

而这,也正是缓存使用的标准的design pattern,也就是cache aside。包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。

那么,是否这种方案就是万无一失的完美策略呢?其实也并不然,再来看看这种场景:一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

图片

但是这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

所以,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间,这样,即使数据出现了不一致,也能在一段时间之后失效,更新上一致的数据。

操作失败

上面虽然列举了不少较为复杂的并发场景,但实际上还是理想情况:即,对数据库和缓存的操作都是成功的。然而在实际生产中,由于网络抖动、服务下线等等原因,操作是有可能失败的。

举例说明:应用要把数据 X 的值从 1 更新为 2,先成功更新了数据库,然后在 Redis 缓存中删除 X 的缓存,但是这个操作却失败了,这个时候数据库中 X 的新值为 2,Redis 中的 X 的缓存值为 1,出现了数据库和缓存数据不一致的问题。

图片

那么,后续有访问数据 X 的请求,会先在 Redis 中查询,因为缓存并没有 诶删除,所以会缓存命中,但是读到的却是旧值 1。

其实不管是先操作数据库,还是先操作缓存,只要第二个操作失败都会出现数据一致的问题。

问题原因知道了,该怎么解决呢?有两种方法:

  • 重试机制。

  • 订阅 MySQL binlog,再操作缓存。

重试机制

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

  • 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

  • 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

举个例子,来说明重试机制的过程。

图片



订阅 MySQL binlog,再操作缓存

先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。

Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

下图是 Canal 的工作原理:

图片

所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。

总结

1、cache aside并非万能

虽然说catch aside可以被称之为缓存使用的最佳实践,但与此同时,它引入了缓存的命中率降低的问题,(每次都删除缓存自然导致更不容易命中了),因此它更适用于对缓存命中率要求并不是特别高的场景。如果要求较高的缓存命中率,依然需要采用更新数据库后同时更新缓存的方案。

2、缓存数据不一致的解决方案

前面已经说了,在更新数据库后同时更新缓存,会在并发的场景下出现数据不一致,那我们该怎么规避呢?方案也有两种。

引入分布式锁

在更新缓存之前尝试获取锁,如果已经被占用就先阻塞住线程,等待其他线程释放锁后再尝试更新。但这会影响并发操作的性能。

设置较短缓存时间

设置较短的缓存过期时间能够使得数据不一致问题存在的时间也比较长,对业务的影响相对较小。但是与此同时,其实这也使得缓存命中率降低,又回到了前面的问题里...

所以,综上所述,没有永恒的最佳方案,只有不同业务场景下的方案取舍。

行文至此,不由得默念一声:“There is no silver bullet!”,并再次为《人月神话》作者的精准洞见而感叹。


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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

element el-collapse折叠面板箭头在前显示

::v-deep .el-collapse-item__arrow {position: absolute;left: 30px;}

【山河送书第三期】:《Python机器学习:基于PyTorch和Scikit-Learn 》赠书四本!!

【山河送书第三期】:《Python机器学习:基于PyTorch和Scikit-Learn 》 前言内容简介作者简介参与方式 前言 近年来,机器学习方法凭借其理解海量数据和自主决策的能力,已在医疗保健、 机器人、生物学、物理学、大众消费和互联网服务…

PyTorch 安装

本文基于conda安装,请确保已经安装好Anaconda,可参考上一篇文章安装Anaconda。 一、确定本机CUDA版本 基于N卡,基于N卡,基于N卡 nvidia-smi #N卡用此命名查看,N卡如果没有此命令,先去更新显卡驱动二、安…

基于单片机电子密码锁射频卡识别指纹门禁密码锁系统的设计与实现

功能介绍 通过指纹进行开锁或者是按键输入当前的密码,修改密码,对IC卡可以进行注册,删除。当有RFID卡进入到读卡器的读卡范围内时,则会自动读取卡序列号,单片机根据卡的序列号对卡进行判断。若该卡是有效卡&#xff0c…

波奇学Linux:git和gdb调试

git用来版本控制,同样是版本控制的软件还有svn等。 git的特定是具有网络功能的版本控制器,开源,client和server是一体的。(去中心化分布式管理) client和server一体意味着远程仓库和本地仓库是平等地位,远程仓库是特殊的仓库而已…

CSS详解

这里写目录标题 cssCSS 是什么基本语法规范引入方式内部样式表行内样式表外部样式关于缓存: 选择器的功能选择器的种类 类选择器语法细节:作用特点 id 选择器后代选择器选择器 作用 注意事项 其他 css CSS 是什么 层叠样式表 (Cascading Style Sheets). CSS 能够对网页中元素…

参数名的映射,小心使用strict=False

从vgg16-397923af.pth里读取的数值应该和加载预训练模型后model.load_state_dict参数一致。 而我的不一致! 原因:在载入参数到模型键值的不匹配,所以使用了strictFalse。 解决办法: 进行参数名的映射,将不匹配的参数名…

【论文阅读】《Distilling the Knowledge in a Neural Network》

【论文阅读】《Distilling the Knowledge in a Neural Network》 推荐指数: 1. 动机 (1)虽然一个ensemble的模型可以提升模型的效果,但是在效率方面实在难以接受,尤其是在每个模型都是一个大型的网络模型的时候。 &…

我在VScode学Java类与对象(Java构造方法 、JavaBean)第二辑 + VScode怎么在预览模式中点击另外一个文件,不会被替换掉

我的个人博客主页:如果’真能转义1️⃣说1️⃣的博客主页 关于Java基本语法学习---->可以参考我的这篇博客:《我在VScode学Java》 关于Java数组学习、JVM中的堆和栈—>可以参考我的这篇文章我在VScode学Java(Java一维数组、二维数组、JVM中的堆和栈…

C#图片处理

查找图片所在位置 原理:使用OpenCvSharp对比查找小图片在大图片上的位置 private static System.Drawing.Point Find(Mat BackGround, Mat Identify, double threshold 0.8) {using (Mat res new Mat(BackGround.Rows - Identify.Rows 1, BackGround.Cols - Iden…

移远通信发布新款5G/4G、LPWA和GNSS天线,进一步优化物联网终端性能

2023年7月17日,全球领先的物联网整体解决方案供应商移远通信宣布,再次推出三款新型天线产品,以更优的通信和定位性能,满足各类物联网终端在5G/4G、LPWA和GNSS等技术上的更高设计需求。这三款天线包括: YEMN926J1A&…

如何将jar 包下载到自定义maven仓库

下载命令 mvn install:install-file -Dfileartifactid-version.jar -DgroupIdgroupid -DartifactIdartifactid -Dversionversion -Dpackagingjar -DlocalRepositoryPath. -DcreateChecksumtrue参数解释 在上述命令中,需要替换以下参数: artifactid-vers…