缓存
约 1239 字大约 4 分钟
2025-07-24
Cache-Aside Pattern
Cache-Aside 模式的原理是什么?如何解决数据一致性问题?如果先更新数据库,缓存宕机了,这种情况怎么解决?
Cache-Aside (缓存旁路) 模式是应用最广泛的缓存读写模式,其核心思想是,应用程序直接与缓存和数据库进行交互,并由应用程序自身来维护两者的数据一致性。
原理
读操作 (Read):
应用先从缓存中读取数据。
如果缓存命中 (Cache Hit),则直接返回数据。
如果缓存未命中 (Cache Miss),则从数据库中读取数据。
将从数据库中读到的数据写入缓存。
返回数据。
写操作 (Write):
- 先更新数据库,再删除缓存 (delete-cache)。
数据一致性问题解决方案
在写操作中,选择“先更新数据库,再删除缓存”而不是“更新缓存”,是为了解决并发场景下的数据一致性问题。
为什么不更新缓存?
写后读并发问题:如果线程 A 更新数据库后更新缓存,同时线程 B 读取。可能出现 B 在 A 更新缓存前读取了旧缓存,导致数据不一致。
懒加载思想:不是所有写入数据库的数据都会被立即读取。删除缓存可以确保下次读取时,从数据库加载最新的数据到缓存中,是一种懒加载思想,避免了无效的缓存写操作。
为什么是“先更新 DB,再删除缓存”?
场景分析:
先删除缓存,再更新 DB:假设线程 A 先删除缓存,此时尚未更新数据库。线程 B 发起读请求,发现缓存失效,于是从数据库读取旧值并写入缓存。之后,线程 A 完成数据库更新。此时,缓存中存储的是旧数据,而数据库是新数据,造成了数据不一致。
先更新 DB,再删除缓存:假设线程 A 更新数据库。在它删除缓存之前,线程 B 发起读请求,读到的是缓存中的旧数据。这会产生一个短暂的数据不一致。但当线程 A 完成缓存删除后,后续的读请求会从数据库加载最新数据,从而保证最终一致性。这种短暂的不一致在大多数业务场景下是可以接受的。
极端情况:在“先更新 DB,再删除缓存”的策略下,如果一个读请求在写请求更新完 DB 之后、删除缓存之前,读取了旧缓存,然后写请求完成了缓存删除,另一个读请求再次发起,就会读到新数据。虽然有短暂不一致,但系统能自动恢复。
解决“更新数据库成功,缓存删除失败”的问题
这是一个经典的数据一致性难题。如果更新数据库操作成功,但随后的删除缓存操作因为缓存服务宕机、网络抖动等原因失败了,会导致数据库是新数据,而缓存是旧数据,且这个不一致会一直存在。
解决方案通常有以下几种:
消息队列 (Message Queue) 实现异步重试:
这是业界最主流的方案。将“删除缓存”这一操作,不直接在业务线程中执行,而是发送到一个消息队列中(如 RabbitMQ, RocketMQ)。
流程:
业务线程更新数据库。
向消息队列发送一条“删除缓存 Key”的消息。
由一个专门的消费者服务订阅该消息队列。
消费者服务接收到消息后,执行删除缓存的操作。
可靠性保障:
如果删除缓存的操作失败(例如缓存宕机),消息队列的消费确认机制 (ACK) 会让该消息重新入队,消费者会不断重试,直到缓存恢复并删除成功为止。
为了防止消息丢失,需要配置好生产端的发送确认机制和消息队列的持久化。
订阅数据库变更日志 (如 Canal):
通过工具(如阿里巴巴的 Canal)订阅 MySQL 的
binlog
。当 Canal 解析到数据库有
UPDATE
或DELETE
操作时,它会自动将变更信息推送到消息队列。下游的消费者服务接收到消息后,根据变更的表和主键,去删除对应的缓存。
优点:该方案将数据同步逻辑与业务代码完全解耦,业务代码只需关心数据库操作,可靠性更高。
这两种方案都将缓存操作异步化,并通过重试机制确保了操作的最终成功,从而保障了数据库和缓存的最终一致性。