背景概述最近团队里我们在密集的讨论 redis 缓存一致性相关的问题,电商核心的域如商品、营销、库存、订单等实际上在缓存的选择上各有特色,那么在这些差异的业务背后,我们有没有一些最佳实践可供参考呢?本文尝试着来讨论这个问题,并给出一些建议 。
在讨论之前,有两个重点我们需要达成一致:
- 分布式场景下无法做到强一致:不同于 CPU 硬件缓存体系采用的 MESI 协议(参考资料)以及硬件的强时钟控制,分布式场景下我们无法做到缓存与底层数据库的强一致,即把缓存和数据库的数据变更做成一个原子操作 。硬件工程师设计了内存屏障(Memory Barrier)的概念,提供给软件开发者不同的一致性选项在性能与一致性上进行权衡 。
- 就算是达到最终一致性也很难:分布式场景下,要做到最终一致性,就要求缓存中存储的是最新版本的数据(或者缓存为空),而且是在数据库更新后很迅速的就要达到这个一致性的状态,要做到是极其困难的 。我们会面临硬件、软件、通信等等组件非常多的异常情况 。
文章插图
缓存的一致性问题一般化来说,我们面临的是这样的一个问题,如下图所示,数据库的数据会有 5 次更新,产生 6 个版本,V1~V6,图中每个方框的长度代表这个版本持续的时间 。我们期望,在数据库中的数据变化后,缓存层需要尽快的感知到并作出反应,如下图所示,缓存层方框中的间隔代表这个时间段缓存数据不存在,V2、V3 以及 V5 版本在缓存中不存在并不会破坏我们的最终一致性要求,只要数据库的最终版本和缓存的最终版本是相同的就可以了 。
文章插图
缓存是如何写入的缓存写入的代码通常情况下都是和缓存使用的代码放在一起的,包含 4 个步骤,如下图所示:W1 读取缓存,W2 判断缓存是否存在,W3 组装缓存数据(这通常需要向数据库进行查询),W4 写入缓存 。每一个步骤间可能会停顿多久是没有办法控制的,尤其是 W3、W4 之间的停顿最为要命,它很可能让我们将旧版本的数据写入到缓存中 。
我们可能会想,W4 步的写入,带上 W2 的假设,即使用 WriteIfNotExists 语义,会不会有所改善?
文章插图
考虑如下的情形,假设有 3 个缓存写入的并发执行,由于短时间数据库大量的更新,它们分别组装的是 V1、V2、V3 版本的数据 。使用 WriteIfNotExists 语义,其中必然有 2 个执行会失败,哪一个会成功根本无法保证 。我们无法简单的做决策,需要再次将缓存读取出来,然后判断是否我们即将写入的一样,如果一样那就很简单;如果不一样的话,我们有两种选择:
【一文弄懂Redis缓存一致性最佳实践参考案例】1)将缓存删除,让后续别的请求来处理写入 。
2)使用缓存提供的原子操作,仅在我们的数据是较新版本时写入 。
文章插图
如何感知数据库的变化数据库的数据发生变化后,我们如何感知到并进行有效的缓存管理呢?通常情况下有如下的 3 种做法:
使用代码执行流通常我们会在数据库操作完成后,执行一些缓存操作的代码 。这种方式最大的问题是可靠性不高,应用重启、机器意外当机等情况都会导致后续的代码无法执行 。
使用事务消息作为如何感知数据库的变化的改进,在数据库操作完成后发出事务消息,然后在消息的消费逻辑里执行缓存的管理操作 。可靠性的问题就解决了,只是业务侧要为此增加事务消息的逻辑,以及运行成本 。
使用数据变更日志数据库产品通常都支持在数据变更后产生变更日志,比如 MySQL 的 binlog 。可以让中间件团队写一款产品,在接收到变更后执行缓存的管理操作,比如阿里的精卫 。可靠性有保证,同时还可以进行某个时间段变更日志的回放,功能就比较强大了 。
最佳实践一:数据库变更后失效缓存这是最常用和简单的方式,应该被作为首选的方案,整体的执行逻辑如下图所示:
文章插图
W4 步使用最基本的 put 语义,这里的假设是写入较晚的请求往往也是携带的最新的数据,这在大多的情形下都是成立的 。D1 步使用监听 DB binlog 的方式来删除缓存,即前述使用事务消息中介绍的方法 。
推荐阅读
- 一文看穿跨域BGP/MPLS IP VPN三方案
- canal+Kafka实现mysql与redis数据同步
- 一文读懂Access数据库,从此不用Access数据库
- Java业务开发常见错误
- Same Origin Policy 简单弄懂同源政策 与跨网域 (CORS)
- 一文详解Liquibase如何自动化数据库脚本部署
- 一文带你搞定TCP连接队列
- 微软|Win11中的祖传UI从Win9X流传至今!一文了解详情
- 一文看懂编程的本质,程序员有前途么?
- 一文掌握SQL基础