缓存与数据库一致性
#1. Cache Aside Pattern
标准的方案,facebook 就是使用这种方式。
核心概念 | 说明 |
---|---|
失效 | 应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。 |
命中 | 应用程序从 cache 中取数据,取到后返回。 |
更新 | 先把数据存到数据库中,成功后,再让缓存失效。 |
#读流程:
步骤 | 说明 |
---|---|
1 | 读缓存,命中则直接返回 |
2 | 如果没命中,读数据库 |
3 | 更新缓存 |
#写流程:
步骤 | 说明 |
---|---|
1 | 更新数据库 |
2 | 删缓存,使缓存失效 |
#2. 双写并发问题
Cache Aside Pattern 方案能解决 双写并发
问题:
双写并发:简而言之,
张三的写操作
一旦和李四的写操作
交织在一起,就会导致缓存中的数据错误。
# | 用户 | 操作 | 数据库中的值 | 缓存中的值 |
---|---|---|---|---|
1 | 张三-1 | 更新数据库 | 10 -> 20 |
10 |
3 | 李四-1 | 更新数据库 | 20 -> 30 |
30 |
2 | 李四-2 | 删除缓存 | 30 |
无 |
4 | 张三-2 | 删除缓存 | 30 |
无 |
7 | 最终 | - | 30 |
无 |
#3. 读写并发问题
Cache Aside Pattern 方案能解决不了全部的 读写并发
问题:
读写并发:简而言之,
张三的写操作
一旦和李四的读操作
交织在一起,就会导致缓存中的数据错误。
一部分的 读写并发
问题,Cache Aside Pattern 方案能解决。例如:
# | 用户 | 操作 | 数据库中的值 | 缓存中的值 |
---|---|---|---|---|
1 | 李四-1 | 读缓存,没命中 | 10 | 无 |
2 | 张三-1 | 更新数据库 | 10 -> 20 |
10 |
3 | 张三-2 | 删除缓存 | 10 | 无 |
4 | 李四-2 | 读数据库 | 20 | 无 |
5 | 李四-3 | 更新缓存,同步数据 | 20 | 20 |
6 | 最终 | - | 20 | 20 |
但是,如果是下面这样的交织时序,Cache Aside Pattern 方案也无能为力:
# | 用户 | 操作 | 数据库中的值 | 缓存中的值 |
---|---|---|---|---|
2 | 李四-读-1 | 读缓存,没命中 | 10 | 无 |
3 | 李四-读-2 | 读数据库 | 10 | 无 |
4 | 张三-写-1 | 更新数据库 | 10 -> 20 |
无 |
5 | 张三-写-2 | 删除缓存 | 20 | 无 |
6 | 李四-读-3 | 更新缓存 | 20 | 10 |
7 | 最终 | - | 20 | 10 |
#4. Cache Aside Pattern 方案总结
这个方案足够简单,容易理解,容易实现。只是面对『部分读写并发问题无能为力』,不过,实际上出现这种概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
#5. Cache Aside Pattern 方案的改进
TIP
这些改建方案不一定用得上。原因在于:
- 有数据不一致的窗口期,这是可接受的。
- 改进方案虽然改进了问题,但是同时带来了复杂性。
#方案 1:延迟删除
将写操作的『删除 Redis』操作改为异步的延迟删除。例如:更新完数据库,1 秒钟之后再删除缓存。
这种情况下,读写并发造成的数据不一致问题最多也就存在 1 秒。
这个改进方案的问题在于:你要延迟多久?延迟的时间短了没有解决读写并发问题;延迟的时间越长不一致隐患就越大。
当然,在一致性要求不是那么高的情况下,有 3、5 秒的窗口期数据不一致很正常。
#方案 2:借助消息队列,将删存缓存的工作委托给第三方
-
读数据的人,在发现缓存中没有数据时,不再由他自己来刷新缓存,而是由『别人』来刷新;
-
写数据的人,在更新完数据库之后,不再由他自己来删除缓存,而是由『别人』来删除;
简单来说:『别人』先查后刷,查刷一体 。
分析:『别人』是串行化接收、处理消息,在更新缓存时,他是先读 DB,再写 Cache ,这个过程中是没有『其它的别人』插入的。