缓存与数据库一致性:从问题到解决方案全解析
一、⼀致性问题的由来:为什么会不一致?
我们先从现实例子出发,来看为什么会出现一致性问题:
📦 场景举例:电商下单业务
- 用户提交订单 → 服务写入数据库订单表;
- 同时更新缓存(比如用户的订单数量缓存);
- 如果在更新缓存前服务宕机了,那么缓存没有更新;
- 数据库是最新的,缓存却是旧的。
这个时候就出现了“缓存与数据库数据不一致”的问题。
二、可能出现不一致的典型操作顺序
操作顺序 | 描述 | 风险 |
---|---|---|
✅ 正常流程 | 先更新数据库,再删除缓存 | 无 |
❌ 反向顺序 | 先删除缓存,再更新数据库 | 如果更新慢或失败,可能被其他请求重新写入旧缓存 |
❌ 异步删除缓存 | 更新数据库后异步删除缓存 | 异步失败就不会删掉缓存了 |
❌ 多线程并发写 | 请求并发,缓存与数据库互不协调 | 脏数据风险高 |
三、解决方案详解
✅ 方案 1:先更新数据库,再删除缓存(强一致)
这是大多数系统采用的方式:
1. update DB
2. delete cache
目前最流行的缓存读写策略 Cache Aside Pattern(旁路缓存模式)就是采用的先写数据库,再删缓存的方式。
- 失效:应用程序先从缓存读取数据,如果数据不存在,再从数据库中读取数据,成功后,放入缓存。
- 命中:应用程序从缓存读取数据,如果数据存在,直接返回。
- 更新:先把数据写入数据库,成功后,再让缓存失效。
优点:只要删除成功,下一次读请求就会重新回源数据库拿到最新值再写入缓存。
缺点:并发高时,如果删缓存和数据库之间被另一个请求读到了旧缓存,就不一致。
✅ 方案 2:延迟双删策略(推荐)
为了防止缓存刚删完就被其他线程重新写回老数据,可以在第一次删缓存后等待一段时间再次删一次:
updateDB();
deleteCache();
Thread.sleep(500);
deleteCache();
适用于读多写少的场景,第二次延迟删除是为了避免“击穿后写入旧数据”。
✅ 方案 3:消息队列补偿策略(最终一致性)
流程如下:
1. 更新数据库成功后,发送一条消息到 MQ(如 Kafka、RocketMQ)
2. 缓存服务订阅这个消息,接收到后再删除缓存
可以专门起一个服务(比如 Canal,阿里巴巴 MySQL binlog 增量订阅&消费组件)去监听 MySQL 的 binlog,获取需要操作的数据。
然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除。
这种方式虽然降低了对业务的侵入,但增加了整个系统的复杂度,适合分布式系统中异步解耦场景。
📌 要求 MQ 高可靠、要做消息幂等处理
✅ 方案 4:读写都走缓存(缓存作为主存)
比如电商秒杀场景,订单库存都缓存于 Redis,数据库作为持久化。
- 所有写操作先写 Redis
- 后台定时同步到 DB(MySQL)
- 或使用 binlog 异步写入(如 Canal)
风险:系统崩溃前未同步的缓存可能丢失,适用于对一致性要求不极端高的场景。
四、附图:缓存与数据库一致性解决策略图
五、常见问题及应对策略
面试提问 | 建议回答 |
---|---|
如何保证缓存和数据库一致性? | 描述“先更新数据库,再删除缓存”的基本原则,提出“延迟双删”、“消息队列”等高级策略 |
如果缓存刚删完就被旧值写回了怎么办? | 回答“延迟双删”或“使用分布式锁防止并发更新” |
缓存更新失败怎么办? | 回答“使用 MQ 补偿机制,做幂等重试” |