目录
- 为什么会出现Redis缓存与数据库不一致的情况?
- 旁路缓存
- 延迟双删
- 消息队列(最终一致性)
- 监听数据库Binlog(强一致性)
为什么会出现Redis缓存与数据库不一致的情况?
保证Redis缓存与数据库的一致性,是在高并发系统中必须面对的挑战。没有一种“银弹”可以解决所有场景,我们需要根据业务场景、一致性要求和技术成本来选择合适的策略。我们对缓存和数据库的更新不是一个原子操作,它总是一个先一个后。在高并发环境下,无论先更新谁,都可能因为线程交叉执行或操作失败,导致数据不一致。
旁路缓存
旁路缓存策略是最常用、最基础的策略,其核心思想是:缓存是数据库的辅助,所有写操作都直接与数据库交互。
旁路缓存读写操作
- 读操作
-
从缓存读取数据
-
如果缓存命中,直接返回数据
-
如果缓存未命中,从数据库读取数据。将数据库读出的数据写入缓存,然后返回数据
- 写操作
-
更新数据库
-
删除缓存
为什么是”删除缓存”而不是”更新缓存”?
性能浪费: 如果这个数据在下次被读取前又被多次更新,那么中间状态的缓存更新就是不必要的,浪费了资源。
脏数据风险: 在并发写时,可能会出现线程A更新数据库后,线程B又更新了数据库,但更新缓存的顺序却是B先A后,导致缓存中是老数据(A的数据)。
旁路缓存不一致场景
旁路缓存策略在绝大多数情况下它是可靠的,但在一种极端的并发场景下会出问题:
-
时刻1:缓存刚好失效
-
时刻2:线程A发起读请求,未命中缓存,去查询数据库(得到旧值)(读操作步骤1)
-
时刻3:线程B发起写请求,更新了数据库。(写操作步骤1)
-
时刻4:线程B删除缓存。(写操作步骤2)
-
时刻5:线程A将之前读到的旧值写入缓存。(读操作步骤2)
在此场景下,缓存中变成了脏数据(旧值),并且会一直持续到下次缓存过期或被更新。这个场景发生的概率很低,因为它要求缓存失效和并发读写同时发生,并且步骤2的读数据库操作必须在步骤3的写数据库操作之前开始,但步骤5的写缓存操作又必须在步骤4的删除缓存操作之后完成。由于数据库的写操作通常比读操作更慢(加锁等),所以这个时间窗口非常小。
延迟双删
延迟双删策略是在”先更新数据库, 再删除缓存”的基础上,增加一个休眠和二次删除的步骤。
-
更新数据库
-
删除缓存
-
休眠一个短暂的时间(如几百毫秒到1秒)
-
再次删除缓存
为什么休眠?
为了确保主库的更新已经完成,并且可能产生的旧缓存(如上文旁路缓存极端并发场景时刻5)已经被设置。
为什么”再次删除”?
第二次删除就是为了清理这些”脏”缓存。
延迟双删缺点
-
休眠时间难以确定,可能删不掉或等待过久
-
降低了吞吐量。
-
第二次删除仍可能失败
消息队列(最终一致性)
通过消息队列保证最终一致性,通常是在”先更新数据库,再删除缓存”的基础上,结合延迟双删和消息队列的重试机制,来尽可能地保证缓存被删除,从而达到最终一致性。将第二次删除操作作为消息发送到消息队列,由消费者重试,确保删除成功。
-
更新数据库
-
删除缓存(第一次删除)
-
向消息队列发送一条延迟消息(比如延迟1秒),消息内容为要删除的缓存key
-
消息队列在延迟时间过后,将消息投递给消费者
-
消费者再次删除缓存(第二次删除)
注意:这里的延迟时间(1秒)需要根据实际业务场景来设定,通常需要大于数据库主从同步的时间(如果用了主从)以及读请求的耗时。
Q&A
-
第一次删除失败怎么办?
由第二次删除来弥补。
-
发送延迟消息失败怎么办?
重试发送,或者记录日志,由人工干预。
注意:这个方案并不能保证100%的强一致性,但是能够极大地提高一致性。同时,我们还可以通过设置缓存的过期时间来做兜底,即使出现不一致,数据也会在过期时间后自动清除。
消息队列实现一致性优点
保证了操作的最终成功,可靠性高
消息队列实现一致性缺点
系统复杂度增加,需要维护消息队列
监听数据库Binlog(强一致性)
通过监听数据库 Binlog 进行同步,这是最优雅、对业务代码侵入性最小的方案,也是大厂普遍采用的方案。
-
业务代码正常更新数据库。
-
数据库的变更会记录在 Binlog 中。
-
一个中间件(如阿里开源的 Canal)模拟数据库从库,监听并解析 Binlog。
-
Canal 解析出需要删除的缓存 Key,将其发送到消息队列或直接调用 Redis 删除缓存。
-
从消息队列中消费变更事件,并执行缓存删除操作。
监听数据库Binlog实现一致性优点
-
业务代码极致简化。业务层不再关心缓存删除逻辑,只需要操作数据库。
-
高性能。数据库和缓存的同步是异步的,不影响主流程。
-
高可靠。基于 Binlog,顺序性和可靠性有保障。
监听数据库Binlog实现一致性缺点
-
系统架构最复杂,需要引入并维护 Canal 等组件。
-
同步有一定延迟,是最终一致性。