南部网站建设和目网站
news/
2025/9/24 6:22:50/
文章来源:
南部网站建设,和目网站,上海网站排名优化价格,小程序定制价格缓存数据一致性探究 缓存是一种较低成本提升系统性能的方式#xff0c;自它面世第一天起就备受广大开发者的喜爱。然而正如《人月神话》中的那句经典的“没有银弹”中所说#xff0c;软件工程的设计没有银弹。
就像每一次发布上线修复问题的同时#xff0c;也极易引入新的问…缓存数据一致性探究 缓存是一种较低成本提升系统性能的方式自它面世第一天起就备受广大开发者的喜爱。然而正如《人月神话》中的那句经典的“没有银弹”中所说软件工程的设计没有银弹。
就像每一次发布上线修复问题的同时也极易引入新的问题自缓存诞生的第一天起缓存与数据库的数据一致性问题就深深困扰着开发者们。
关键词原子性、事务性、数据一致性、双写一致性
缓存的查询
先查询缓存如果查询失败那么去查询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 的新值为 2Redis 中的 X 的缓存值为 1出现了数据库和缓存数据不一致的问题。 那么后续有访问数据 X 的请求会先在 Redis 中查询因为缓存并没有 诶删除所以会缓存命中但是读到的却是旧值 1。
其实不管是先操作数据库还是先操作缓存只要第二个操作失败都会出现数据一致的问题。
问题原因知道了该怎么解决呢有两种方法 重试机制。 订阅 MySQL binlog再操作缓存。 重试机制 我们可以引入消息队列将第二个操作删除缓存要操作的数据加入到消息队列由消费者来操作数据。 如果应用删除缓存失败可以从消息队列中重新读取数据然后再次删除缓存这个就是重试机制。当然如果重试超过一定次数还是没有成功我们就需要向业务层发送报错信息了。 如果删除缓存成功就要把数据从消息队列中移除避免重复操作否则就继续重试。
举个例子来说明重试机制的过程。 订阅 MySQL binlog再操作缓存 「先更新数据库再删缓存」的策略的第一步是更新数据库那么更新数据库成功就会产生一条变更日志记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志拿到具体要操作的数据然后再执行缓存删除阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议把自己伪装成一个 MySQL 的从节点向 MySQL 主节点发送 dump 请求MySQL 收到请求后就会开始推送 Binlog 给 CanalCanal 解析 Binlog 字节流之后转换为便于读取的结构化数据供下游程序订阅使用。
下图是 Canal 的工作原理 所以如果要想保证「先更新数据库再删缓存」策略第二个操作能执行成功我们可以使用「消息队列来重试缓存的删除」或者「订阅 MySQL binlog 再操作缓存」这两种方法有一个共同的特点都是采用异步操作缓存。
总结
1、cache aside并非万能 虽然说catch aside可以被称之为缓存使用的最佳实践但与此同时它引入了缓存的命中率降低的问题每次都删除缓存自然导致更不容易命中了因此它更适用于对缓存命中率要求并不是特别高的场景。如果要求较高的缓存命中率依然需要采用更新数据库后同时更新缓存的方案。 2、缓存数据不一致的解决方案
前面已经说了在更新数据库后同时更新缓存会在并发的场景下出现数据不一致那我们该怎么规避呢方案也有两种。
引入分布式锁
在更新缓存之前尝试获取锁如果已经被占用就先阻塞住线程等待其他线程释放锁后再尝试更新。但这会影响并发操作的性能。
设置较短缓存时间
设置较短的缓存过期时间能够使得数据不一致问题存在的时间也比较长对业务的影响相对较小。但是与此同时其实这也使得缓存命中率降低又回到了前面的问题里...
所以综上所述没有永恒的最佳方案只有不同业务场景下的方案取舍。
行文至此不由得默念一声“There is no silver bullet!”并再次为《人月神话》作者的精准洞见而感叹。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/915066.shtml
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!