数据库缓存一致性问题
本文是完全参考 缓存和数据库一致性问题,看这篇就够了 进行整理的,用于巩固对数据库缓存一致性的认识
在实际开发中,对缓存比较简单的操作是
- 读数据先查缓存,缓存有也从缓存取,缓存没有再查数据库,并添加到缓存中去
- 更新数据则需要刷新数据库和缓存
这里我们关注两个问题
- 缓存利用率
- 数据库缓存的一致性
对于上述的读数据
随着时间的推移,缓存中的数据会越来越多,最极端的情况可能就是与数据库全量同步。对于一些访问频率小的数据而言,存在于缓存无疑是对内存的浪费。因此我们可以针对缓存设置过期时间,使得热点数据能常驻缓存,不被经常访问的数据从缓存中剔除,提高缓存的利用率。
对于上述的写数据
我们考虑以下几种情况
- 并发与非并发
- 非并发下,数据库和缓存刷新的顺序以及第二步出错的情况
假设X=1 更新为 X=2
先刷新缓存,后刷新数据库
非并发
情景:先刷新缓存,后刷新数据库出错
结果:缓存中 X=2,数据库 X=1,数据不一致
并发
情景:先刷新缓存,后刷新数据库,两步都成功
A:更新缓存 X=2
B:更新缓存 X=1
B:更新数据库 X=2 变为 X=1
A:更新数据库 X=1 变为 X=2
结果:缓存X=1,数据库X=2,数据不一致
解决方法:添加分布式锁,保证刷新数据库和缓存为一个原子性操作,但是这样做缓存利用率
先刷新数据库,后刷新缓存
非并发
情景:先刷新数据库,后刷新缓存出错
结果:缓存X=1,数据库X=2,数据不一致
并发
情景:先刷新数据库,后刷新缓存,两者都成功
A:更新数据库 X=1 变为 X=2
B:更新数据库 X=2 变为 X=1
B:更新缓存 X=1
A:更新缓存 X=2
结果:缓存X=2,数据库X=1,数据不一致
解决方法:添加分布式锁,保证刷新数据库和缓存为一个原子性操作,但是这样做缓存利用率
先删除缓存,后刷新数据库
非并发
情景:先删除缓存,后刷新数据库出错
结果:缓存中没有X,数据库X=1,重新读取X时,缓存中X=1,数据一致,但更新失败
并发读写
情景:A写B读,先删除缓存,后刷新数据库
A:删除缓存
B:缓存不存在,读数据库,X=1
A:更新数据库 X=1 变为 X=2
B:更新缓存 X=1
结果:缓存 X=1,数据库X=2,数据不一致
先刷新数据库,后删除缓存
非并发
情景:先刷新数据库,后删除缓存失败
结果:数据库X=2,缓存中X=1,数据不一致
并发读写
情景:缓存不存在,A写B读,先刷新数据库,后删除缓存
B:读缓存,缓存不存在,B读数据库,得到X=1
A:更新数据库 X=1 变为 X=2
A:删除缓存
B:更新缓存X=1
结果:数据库X=2,缓存中X=1,数据不一致
这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?
其实概率「很低」,这是因为它必须满足 3 个条件:
1. 缓存刚好已失效
2. 读请求 + 写请求并发
3. 更新数据库 + 删除缓存的时间,要比读数据库 + 写缓存时间短
仔细想一下,条件 3 发生的概率其实是非常低的。
因为写数据库一般会先「加锁」,所以写数据库,通常是要比读数据库的时间更长的。
这么来看,「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
所以,我们应该采用这种方案,来操作数据库和缓存。
解决了并发问题,我们继续来看前面遗留的,第二步执行「失败」导致数据不一致的问题。
保证两步成功执行
从前面的分析可以看出,只要第二步失败,都会导致数据不一致,那如何保证第二步可以执行成功呢?
第一时间想到就是:重试
这时就需要考虑以下问题:
- 重试时机,立即重试很可能会再次失败
- 重试次数,多少次才算合理
- 重试方式(同步或异步),同步会阻塞,异步更好
异步重试可以考虑消息队列,这时又要考虑消息队列相关的问题:
- 消息队列的可靠性:写到队列中的消息,成功消费之前不会丢失(重启项目也不担心)
- 消息队列保证成功投递:下游从队列拉取消息,成功消费后才会删除消息,否则还会继续投递消息给消费者(符合我们重试的需求)
情景概览
非并发 | 并发 | |
---|---|---|
先刷新缓存,后刷新数据库 | 情景:先刷新缓存,后刷新数据库出错 结果:缓存中 X=2,数据库 X=1,数据不一致 | 情景:先刷新缓存,后刷新数据库,两步都成功 A:更新缓存 X=2 B:更新缓存 X=1 B:更新数据库 X=2 变为 X=1 A:更新数据库 X=1 变为 X=2 结果:缓存X=1,数据库X=2,数据不一致 解决方法:添加分布式锁,保证刷新数据库和缓存为一个原子性操作,但是这样做缓存利用率 |
先刷新数据库,后刷新缓存 | 情景:先刷新数据库,后刷新缓存出错 结果:缓存X=1,数据库X=2,数据不一致 | 类似上述 |
先删除缓存,后刷新数据库 | 情景:先删除缓存,后刷新数据库出错 结果:缓存中没有X,数据库X=1,重新读取X时,缓存中X=1,数据一致,但更新失败 | 情景:A写B读,先删除缓存,后刷新数据库 A:删除缓存 B:缓存不存在,读数据库,X=1 A:更新数据库 X=1 变为 X=2 B:更新缓存 X=1 结果:缓存 X=1,数据库X=2,数据不一致 |
先刷新数据库,后删除缓存 | 情景:先刷新数据库,后删除缓存失败 结果:数据库X=2,缓存中X=1,数据不一致 | 情景:缓存不存在,A写B读,先刷新数据库,后删除缓存 B:读缓存,缓存不存在,B读数据库,得到X=1 A:更新数据库 X=1 变为 X=2 A:删除缓存 B:更新缓存X=1 结果:数据库X=2,缓存中X=1,数据不一致 理论会发生,但概率很低 |