数据库与缓存数据一致性的全部方案

背景

工作中我们经常会有以下的场景

(1)先查缓存,缓存有数据就直接返回

(2)缓存没数据就查数据库,数据库有数据返回,并且将数据写入缓存

(3)数据库也没有数据就返回空

 如果只是简单的查询和新增倒是可以,但是如果涉及更新删除这个流程存在两个问题

(1)如果需要更新缓存中的数据和更新数据库中的数据任意一个更新失败,会导致缓存中存在错误的数据

(2)如果任意一个删除失败会导致缓存中存在脏数据

本文将详细讲解下几种经典保证数据库和缓存一致性的方案

一、先更新数据库,再删除缓存

这是 最常用、最经典的缓存模式,也被叫做 旁路缓存模式(Cache-Aside)

流程:

写请求:
[客户端] → 更新 DB → 删除缓存 → 返回

读请求:
[客户端] → 查缓存?
    ↳ 有 → 返回
    ↳ 无 → 查 DB → 写入缓存 → 返回

✅ 优点:

  • 简单易懂,广泛使用(Redis 官方推荐)
  • 缓存只保存热点数据,节省内存

❌ 缺点 & 风险:

问题场景
缓存未删除成功删除失败 → 下次读取仍返回旧值
并发写导致脏读A 更新 DB → B 查询(旧缓存)→ A 删除缓存失败 → 脏数据
缓存穿透查不到的数据反复查 DB

🛡️ 改进措施:

  • 删除失败重试(最多几次)
  • 给缓存设置合理过期时间(兜底)
  • 使用消息队列异步删除(见后文)

二、延迟双删

针对“先更新 DB 后删缓存”可能存在的 并发问题 提出的优化。

问题场景:

T1: 线程A 更新 DB(新值)
T2: 线程B 查询缓存(命中旧值) ← 危险! 
T3: 线程A 删除缓存 → 用户读到了旧数据

解决方案:

在更新前后各删一次缓存

✅ 作用:

  • 第一次删:防止后续请求命中旧缓存
  • 延迟:让可能正在执行的读请求完成
  • 第二次删:清理在这期间被重建的旧缓存

缺点:

  • sleep 影响性能
  • 时间不好控制(太短无效,太长卡顿)
  • 不能彻底解决问题

仅作为临时缓解手段,不推荐线上长期使用

三、基于消息队列的异步更新(异步解耦)

用 MQ 解耦“更新 DB”和“删除缓存”,实现可靠最终一致性。

流程:

[应用] → 更新 DB → 发送 MQ 消息(user:update:1)
                     ↓
               [消费者] → 删除缓存

✅ 优点:

  • 删除失败可重试(MQ 保障可达性)
  • 解耦业务逻辑与缓存操作
  • 可支持多级缓存同步

❌ 缺点:

  • 引入 MQ,系统复杂度上升
  • 存在一定延迟(最终一致性)
  • 需处理消息幂等性

🎯 适用场景:

  • 对一致性要求较高但允许短暂延迟
  • 已有 MQ 基础设施(如 RocketMQ、Kafka)

四:监听数据库日志(Canal / Debezium)

流程:

  1. 应用更新 DB
  2. Canal 捕获 binlog 变更
  3. 解析出 table、pk、type(insert/update/delete)
  4. 自动删除对应缓存 key

✅ 优点:

  • 完全解耦应用与缓存
  • 即使应用崩溃也能保证缓存更新
  • 支持跨语言、多消费端

❌ 缺点:

  • 运维成本高(需部署 Canal 集群)
  • 延迟略高(ms ~ s 级别)
  • 需要维护 key 映射规则(如 user:1 → user表 pk=1)

🎯 适用场景:

  • 大型互联网公司(阿里、滴滴等广泛使用)
  • 多个服务共用同一数据库
  • 要求高可靠性、低侵入性

五、Read/Write Through 模式(封装缓存层)

将缓存操作封装在一个“缓存服务”中,对外提供统一接口。

流程:

Write-Through(写穿透):

写请求 → 缓存层 → 缓存层同时更新 DB → 返回

Read-Through(读穿透):

读请求 → 缓存层 → 若无,则自动加载 DB 并写入缓存

✅ 优点:

  • 对调用方透明
  • 易于统一管理重试、降级、监控

❌ 缺点:

  • 必须确保缓存层高可用(否则成单点)
  • 写操作变慢(必须等 DB 完成)
  • 实现复杂(需处理失败回滚)

📌 更适合做中间件(如自研缓存平台),不适合普通项目。

五种方案总结:

方案一致性复杂度性能推荐指数适用场景
1. 先更新 DB 再删缓存中(最终)⭐⭐⭐⭐⭐⭐⭐⭐⭐☆通用推荐
2. 延迟双删⭐⭐⭐⭐⭐⭐不推荐
3. 消息队列异步删除高(可靠)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐☆已有 MQ 的中大型系统
4. Binlog 监听(Canal)很高⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐☆大厂、高可靠性要求
5. Write-Through⭐⭐⭐⭐⭐⭐⭐⭐⭐自研缓存平台

生产环境选型建议

团队规模推荐方案
初创公司 / 小团队✅ 方案1 + 设置 TTL + 空值缓存
中型项目 / 已有 MQ✅ 方案1 + MQ 异步删除(失败重试)
大型互联网公司✅ Binlog 监听 + 缓存自动失效
高一致性要求✅ Binlog 监听 或 MQ + 分布式锁 + 监控告警
电商系统中保证数据库缓存数据一致性可从多个方面着手,以下是一些常见的解决方案: ### 最终一致性策略 多数场景可接受短暂不一致,可通过重试、定时任务(如每天全量同步)修复数据不一致问题。同时,设置合理的 TTL(生存时间),自动淘汰旧数据,还需监控缓存命中率、数据库 Redis 的数据差异,及时发现不一致并处理[^2]。 ### 常见一致性方案 1. **先更新 DB,再删除缓存(Cache - Aside)**:这是最常用的方案,能实现最终一致,性能高且复杂度低。不过在并发、异常、网络延迟下,可能出现时序错乱,例如线程 A 更新数据库成功后,删除缓存失败(网络超时),此时线程 B 查询缓存会命中旧数据,导致数据库更新缓存仍是旧值的不一致情况[^3]。 2. **延迟双删(Delayed Double Delete)**:删除两次缓存,能防止并发问题,一致性较好,性能复杂度处于中等水平[^3]。 3. **先删缓存,再更新 DB(Write - Behind)**:此方案风险高,不推荐使用,一致性差,性能高但复杂度低[^3]。 4. **基于 Binlog 的异步更新(如 Canal)**:异步监听数据库变更,实现最终一致,性能复杂度都较高[^3]。 5. **分布式事务(如 Seata)**:能实现强一致,但性能差,复杂度高[^3]。 ### 针对不同并发情况的处理 对于并发几率很小的数据(如个人维度的订单数据、用户数据等),很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新。就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),给缓存加上过期时间依然可以解决大部分业务对于缓存的要求[^4]。 ### 推荐方案 推荐采用 Cache - Aside + 主动失效的方案,这是生产级常用方案[^3]。 以下是一个简单的 Java 示例代码,展示先更新 DB,再删除缓存的基本实现: ```java import java.util.HashMap; import java.util.Map; // 模拟数据库 class Database { private Map<String, String> data = new HashMap<>(); public void updateData(String key, String value) { data.put(key, value); } public String getData(String key) { return data.get(key); } } // 模拟缓存 class Cache { private Map<String, String> cacheData = new HashMap<>(); public void deleteCache(String key) { cacheData.remove(key); } public String getCache(String key) { return cacheData.get(key); } public void setCache(String key, String value) { cacheData.put(key, value); } } public class CacheDatabaseConsistency { private Database database = new Database(); private Cache cache = new Cache(); public void updateDataAndDeleteCache(String key, String value) { // 先更新数据库 database.updateData(key, value); // 再删除缓存 cache.deleteCache(key); } public String getData(String key) { String cacheValue = cache.getCache(key); if (cacheValue != null) { return cacheValue; } // 缓存未命中,从数据库获取 String dbValue = database.getData(key); if (dbValue != null) { // 将数据存入缓存 cache.setCache(key, dbValue); } return dbValue; } public static void main(String[] args) { CacheDatabaseConsistency consistency = new CacheDatabaseConsistency(); consistency.updateDataAndDeleteCache("product1", "New Product Name"); String data = consistency.getData("product1"); System.out.println(data); } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值