66head :
77 - - meta
88 - name : keywords
9- content : 缓存读写策略,Cache Aside,Read Through,Write Through,一致性,失效
9+ content : 缓存读写策略,Cache Aside,Read Through,Write Through,Write Behind,Write Back,缓存一致性,缓存失效,旁路缓存,读写穿透,异步缓存写入,Redis缓存策略,缓存更新策略
1010 - - meta
1111 - name : description
12- content : 总结三种常见缓存读写策略及适用场景,分析一致性与失效处理,指导业务选型与问题规避。
12+ content : 深入对比 Cache Aside、Read/Write Through、Write Behind 三种缓存读写策略,附详细时序图、一致性问题分析及生产级解决方案,Redis 实战必备!
1313---
1414
1515看到很多小伙伴简历上写了“** 熟练使用缓存** ”,但是被我问到“** 缓存常用的 3 种读写策略** ”的时候却一脸懵逼。
@@ -22,104 +22,137 @@ head:
2222
2323### Cache Aside Pattern(旁路缓存模式)
2424
25- ** Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。 **
25+ 这是我们日常开发中 ** 最常用、最经典 ** 的一种模式,几乎是互联网应用缓存方案的事实标准,尤其适合 ** 读多写少 ** 的业务场景。
2626
27- Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
27+ 这个模式之所以被称为** “旁路”(Aside)** ,是因为应用程序的** 写操作完全绕过了缓存,直接操作数据库** 。
28+
29+ 应用程序扮演了数据流转的“指挥官”,需要同时维护 Cache 和 DB 两个数据源。
2830
2931下面我们来看一下这个策略模式下的缓存读写步骤。
3032
31- ** 写 ** :
33+ ** 写操作 : **
3234
33- - 先更新 db
34- - 然后直接删除 cache 。
35+ 1 . 应用 ** 先更新 DB ** 。
36+ 2 . 然后 ** 直接删除 Cache ** 中对应的数据 。
3537
3638简单画了一张图帮助大家理解写的步骤。
3739
3840![ ] ( https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-write.png )
3941
40- ** 读 ** :
42+ ** 读操作: **
4143
42- - 从 cache 中读取数据,读取到就直接返回
43- - cache 中读取不到的话,就从 db 中读取数据返回
44- - 再把数据放到 cache 中 。
44+ 1 . 应用先从 Cache 读取数据。
45+ 2 . 如果命中(Hit),则直接返回。
46+ 3 . 如果未命中(Miss),则从 DB 读取数据,成功读取后, ** 将数据写回 Cache ** ,然后返回 。
4547
4648简单画了一张图帮助大家理解读的步骤。
4749
4850![ ] ( https://oss.javaguide.cn/github/javaguide/database/redis/cache-aside-read.png )
4951
5052你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。
5153
52- 比如说面试官很可能会追问:“** 在写数据的过程中,可以先删除 cache ,后更新 db 么?** ”
54+ 比如说面试官很可能会追问:
55+
56+ 1 . 为什么写操作是“先更新 DB,后删除 Cache”?顺序能反过来吗?
57+ 2 . 那“先更新 DB,后删除 Cache”就绝对安全吗?
58+ 3 . 为什么是“删除 Cache”,而不是“更新 Cache”?
5359
54- ** 答案: ** 那肯定是不行的!因为这样可能会造成 ** 数据库(db)和缓存(Cache)数据不一致 ** 的问题 。
60+ 接下来我会以此分析解答这些问题 。
5561
56- 举例:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。
62+ ** 1. 为什么写操作是“先更新 DB,后删除 Cache”?顺序能反过来吗? **
5763
58- 这个过程可以简单描述为:
64+ ** 答: ** 绝对不能。如果“先删 Cache,后更新 DB”,在高并发下会引入经典的数据不一致问题。
5965
60- > 请求 1 先把 cache 中的 A 数据删除 -> 请求 2 从 db 中读取数据->请求 1 再把 db 中的 A 数据更新
66+ - ** 时序分析 (请求 A 写, 请求 B 读):**
67+ 1 . 请求 A: 先将 Cache 中的数据删除。
68+ 2 . 请求 B: 此时发现 Cache 为空,于是去 DB 读取** 旧值** ,并准备写入 Cache。
69+ 3 . 请求 A : 将** 新值** 写入 DB。
70+ 4 . 请求 B: 将之前读到的** 旧值** 写入了 Cache。
71+ - ** 结果:** DB 中是新值,而 Cache 中是旧值,数据不一致。
6172
62- 当你这样回答之后,面试官可能会紧接着就追问:“ ** 在写数据的过程中, 先更新 db ,后删除 cache 就没有问题了么 ?** ”
73+ ** 2. 那“ 先更新 DB ,后删除 Cache”就绝对安全吗 ?**
6374
64- ** 答案:** 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多 。
75+ ** 答案:** 也不是绝对安全的!因为这样也可能会造成 ** 数据库和缓存数据不一致 ** 的问题 。
6576
66- 举例:请求 1 先读数据 A,请求 2 随后写数据 A,并且数据 A 在请求 1 请求之前不在缓存中的话,也有可能产生数据不一致性的问题。
77+ - ** 时序分析 (请求 A 读, 请求 B 写):**
78+ 1 . 请求 A : 缓存未命中,从 DB 读取到** 旧值** 。
79+ 2 . 请求 B: 迅速完成了 DB 的更新,并将 Cache 删除。
80+ 3 . 请求 A : 将自己之前拿到的** 旧值** 写入了 Cache。
81+ - ** 结果:** DB 中是新值,Cache 中又是旧值。
82+ - ** 为什么概率极小?** 这个问题本质上是一个并发时序问题:只要“读 DB → 写 Cache”这段时间窗口内,恰好有写请求完成了 DB 更新,就有可能产生不一致。在大多数业务里,这个窗口时间相对较短,而且还需要与写请求并发“撞车”,所以发生概率不算高,但绝不是不可能。
6783
68- 这个过程可以简单描述为:
84+ ** 3. 为什么是“删除 Cache”,而不是“更新 Cache”? **
6985
70- > 请求 1 从 db 读数据 A-> 请求 2 更新 db 中的数据 A(此时缓存中无数据 A ,故不用执行删除缓存操作 ) -> 请求 1 将数据 A 写入 cache
86+ - ** 性能开销:** 写操作往往只更新了对象的部分字段,如果为了“更新 Cache”而去重新查询或计算整个缓存对象,开销可能很大。相比之下,“删除”是一个轻量级操作。
87+ - ** 懒加载思想:** “删除”操作遵循懒加载原则。只有当数据下一次被真正需要(被读取)时,才触发从 DB 加载并写入缓存,避免了无效的缓存更新。
88+ - ** 并发安全:** “更新缓存”在高并发下可能出现更新顺序错乱的问题导致脏数据的概率会更大。
89+
90+ 当然,这一切都建立在一个重要的前提之上:我们缓存的数据,是可以通过数据库进行确定性重建的,并且业务上可以容忍从‘缓存删除’到‘下一次读取并回填’之间这个极短时间窗口内的数据不一致。
7191
7292现在我们再来分析一下 ** Cache Aside Pattern 的缺陷** 。
7393
74- ** 缺陷 1:首次请求数据一定不在 cache 的问题**
94+ ** 缺陷 1:首次请求数据一定不在 Cache 的问题**
7595
76- 解决办法:可以将热点数据可以提前放入 cache 中 。
96+ 解决办法:对于访问量巨大的热点数据,可以在系统启动或低峰期进行缓存预热 。
7797
78- ** 缺陷 2:写操作比较频繁的话导致 cache 中的数据会被频繁被删除,这样会影响缓存命中率 。**
98+ ** 缺陷 2:写操作比较频繁的话导致 Cache 中的数据会被频繁被删除,这样会影响缓存命中率 。**
7999
80100解决办法:
81101
82- - 数据库和缓存数据强一致场景:更新 db 的时候同样更新 cache ,不过我们需要加一个锁/分布式锁来保证更新 cache 的时候不存在线程安全问题。
83- - 可以短暂地允许数据库和缓存数据不一致的场景:更新 db 的时候同样更新 cache ,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。
102+ - 数据库和缓存数据强一致场景:更新 DB 的时候同样更新 Cache ,不过我们需要加一个锁/分布式锁来保证更新 Cache 的时候不存在线程安全问题。
103+ - 可以短暂地允许数据库和缓存数据不一致的场景:更新 DB 的时候同样更新 Cache ,但是给缓存加一个比较短的过期时间(如 1 分钟) ,这样的话就可以保证即使数据不一致的话影响也比较小。
84104
85105### Read/Write Through Pattern(读写穿透)
86106
87- Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
107+ 在这种模式下,应用程序将** Cache 视为唯一的、主要的存储** 。所有的读写请求都直接打向 Cache,而 Cache 服务自身负责与 DB 进行数据同步。
108+
109+ 对应用程序** 透明** ,应用开发者无需关心 DB 的存在。
88110
89- 这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入 db 的功能。
111+ 这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 本身并没有提供 Cache 将数据写入 DB 的功能,需要我们在业务侧或中间件里自己实现 。
90112
91113** 写(Write Through):**
92114
93- - 先查 cache,cache 中不存在,直接更新 db 。
94- - cache 中存在,则先更新 cache ,然后 cache 服务自己更新 db( ** 同步更新 cache 和 db ** ) 。
115+ - 先查 Cache,Cache 中不存在,直接更新 DB 。
116+ - Cache 中存在,则先更新 Cache ,然后 Cache 服务自己更新 DB。只有当 Cache 和 DB 都写入成功后,才向上层返回成功 。
95117
96118简单画了一张图帮助大家理解写的步骤。
97119
98120![ ] ( https://oss.javaguide.cn/github/javaguide/database/redis/write-through.png )
99121
100122** 读(Read Through):**
101123
102- - 从 cache 中读取数据,读取到就直接返回 。
103- - 读取不到的话,先从 db 加载,写入到 cache 后返回响应。
124+ - 应用从 Cache 读取数据。
125+ - 如果命中,直接返回。
126+ - 如果未命中,由** Cache 服务自己** 负责从 DB 加载数据,加载成功后先写入自身,再返回给应用。
104127
105128简单画了一张图帮助大家理解读的步骤。
106129
107130![ ] ( https://oss.javaguide.cn/github/javaguide/database/redis/read-through.png )
108131
109- Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache ,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。
132+ Read-Through 实际只是在 Cache-Aside 之上进行了封装。在 Cache-Aside 下,发生读请求的时候,如果 Cache 中不存在对应的数据,是由客户端自己负责把数据写入 Cache ,而 Read Through 则是 Cache 服务自己来写入缓存的,这对客户端是透明的。
110133
111- 和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。
134+ 从实现角度看,Read-Through 本质上是把 Cache-Aside 中“读 Miss → 读 DB → 回填 Cache”的逻辑,下沉到了缓存服务内部,对客户端透明。
135+
136+ 和 Cache Aside 一样, Read-Through 也有首次请求数据一定不再 Cache 的问题,对于热点数据可以提前放入缓存中。
112137
113138### Write Behind Pattern(异步缓存写入)
114139
115- Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
140+ Write Behind(也常被称为 Write-Back) Pattern 和 Read/Write Through Pattern 很相似,两者都是由 Cache 服务来负责 Cache 和 DB 的读写。
141+
142+ 但是,两个又有很大的不同:** Read/Write Through 是同步更新 Cache 和 DB,而 Write Behind 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。**
143+
144+ ** 写操作 (Write Behind):**
116145
117- 但是,两个又有很大的不同:** Read/Write Through 是同步更新 cache 和 db,而 Write Behind 则是只更新缓存,不直接更新 db,而是改为异步批量的方式来更新 db。**
146+ 1 . 应用将数据写入 Cache,然后** 立即返回** 。
147+ 2 . Cache 服务将这个写操作放入一个队列中。
148+ 3 . 通过一个独立的异步线程/任务,将队列中的写操作** 批量地、合并地** 写入 DB。
118149
119- 很明显,这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了 。
150+ 这种模式对数据一致性带来了挑战(例如:Cache 中的数据还没来得及写回 DB,系统就宕机了),因此不适用于需要强一致性的场景(如交易、库存) 。
120151
121- 这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
152+ 但是,它的异步和批量特性,带来了 ** 无与伦比的写性能 ** 。它在很多高性能系统中都有广泛应用:
122153
123- Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
154+ - ** MySQL 的 InnoDB Buffer Pool 机制:** 数据修改先在内存 Buffer Pool 中完成,然后由后台线程异步刷写到磁盘。
155+ - ** 操作系统的页缓存(Page Cache):** 文件写入也是先写到内存,再由操作系统异步刷盘。
156+ - ** 高频计数场景:** 对于文章浏览量、帖子点赞数这类允许短暂数据不一致、但写入极其频繁的场景,可以先在 Redis 中快速累加,再通过定时任务异步同步回数据库。
124157
125158<!-- @include: @article-footer.snippet.md -->
0 commit comments