缓存双写策略

缓存双写策略(Cache Dual Write Strategy)是解决 缓存(如 Redis)与数据库(如 MySQL)数据一致性 的核心方案之一。它指的是 在数据更新时,同时(或按特定顺序)更新缓存和数据库 的操作策略。

📌 为什么需要双写?核心矛盾

  1. 性能需求: 读操作远多于写操作,缓存提供高性能读取。

  2. 数据持久性需求: 数据库保证数据的持久化和强一致性。

  3. 一致性需求: 用户期望读取缓存和数据库的结果是一致的(尤其是在更新后)。

🔧 常见的缓存双写策略及其特点

主要分为两大类:同步双写 和 异步双写(通过消息队列),各有优缺点和适用场景:

🧩 1. 同步双写 (Synchronous Dual Write)
  • 操作顺序:

    • 先更新数据库,再更新缓存。

    • 先删除缓存,再更新数据库。

  • 特点:

    • 强一致性要求高: 尽力保证在本次请求内缓存和数据库最终一致(但不绝对保证)。

    • 性能开销: 每次写操作需要同步操作 DB + Cache,增加延迟。

    • 复杂度: 需要处理失败场景(DB 成功 Cache 失败 / DB 失败 Cache 成功)。

策略操作步骤优点缺点 & 风险适用场景
Write-Through (穿透写)1. 更新数据库
2. 立即更新缓存
读性能好(缓存命中率高),逻辑简单并发问题:
- A 更新 DB -> B 更新 DB -> B 更新 Cache -> A 更新 Cache (缓存中是 A 的旧数据)
失败问题:
- DB 成功 Cache 失败:缓存是旧数据
- Cache 成功 DB 失败:缓存是脏数据
对一致性要求极高,且写操作不频繁
Cache Aside (旁路缓存)1. 删除缓存
2. 更新数据库
逻辑简单,避免并发写缓存问题缓存击穿: 删除后,大量读请求穿透到 DB
短暂不一致: 删缓存后更新 DB 前,读请求可能加载旧数据到缓存
失败问题:
- 删缓存成功,更新 DB 失败:缓存缺失(下次读加载)
- 更新 DB 成功,删缓存失败:缓存是旧数据
最常用,平衡一致性和性能
Write-Behind (后写/回写)1. 只更新缓存(标记为脏)
2. 异步批量将脏数据刷回 DB
写性能极高(延迟低),对 DB 压力小数据丢失风险: 缓存宕机导致未刷盘数据丢失
复杂性高: 需维护脏数据和刷盘逻辑
强一致性差
写密集、允许短暂丢失(如点赞、计数)
📥 2. 异步双写 (Asynchronous Dual Write via MQ)
  • 操作顺序:

    1. 应用成功更新数据库。

    2. 应用发送一条消息到消息队列(MQ),消息包含更新的数据。

    3. 独立的消费者服务监听 MQ,消费消息并更新缓存。

  • 特点:

    • 最终一致性: 保证 DB 和 Cache 最终会一致,但存在延迟。

    • 性能好: 主流程只操作 DB 和发 MQ,速度快。

    • 解耦: 缓存更新逻辑独立于主业务逻辑。

    • 可靠性依赖 MQ: 需要保证消息不丢失、至少消费一次。

    • 处理幂等性: 消费者更新缓存操作必须是幂等的(因为消息可能重复消费)。

适用场景:

  • 强一致性要求不高,接受秒级延迟。

  • 写操作非常频繁,同步更新缓存压力过大。

  • 系统架构追求解耦

🔄 缓存双写策略在秒杀系统中的典型应用

在之前设计的秒杀系统中,库存扣减 环节是双写策略的典型应用场景:

  1. 核心流程(Cache Aside + 异步最终一致):

    • Step 1 (写 Redis): 秒杀服务使用 DECR 或 Lua 脚本原子性地扣减 Redis 中的库存(防止超卖)。

    • Step 2 (写 MQ): 扣减成功后,发送“创建订单”消息到 MQ(不直接写 DB)。

    • Step 3 (写 DB - 订单服务): 订单服务消费 MQ 消息:

      • 写数据库 (强一致): 在 DB 中执行 UPDATE stock SET stock = stock - 1 WHERE id=? AND stock > 0最终防线,保证强一致不超卖)。

      • 创建订单记录。

    • Step 4 (异步更新/失效缓存 - 可选):

      • 方案 A (监听 DB Binlog): 通过 Canal/Debezium 监听 DB 的库存变更 Binlog,发送消息触发 Redis 库存同步(保持 Redis 与 DB 最终一致)。

      • 方案 B (订单服务直接失效): 如果 DB 扣减失败,订单服务发送消息回滚 Redis 库存INCR);成功则通常不需要主动更新 Redis(读请求主要依赖 Redis 预扣的标记,售罄后标记不再变化)。

  2. 为什么这样设计?

    • 性能: Redis 原子扣减处理超高并发。

    • 强一致(核心): DB 的 stock > 0 条件是防止超卖的终极保障。

    • 最终一致: Redis 库存与 DB 库存通过异步机制(Binlog 监听或补偿任务)达到最终一致。秒杀期间短暂的 Redis/DB 库存数值差异是可接受的(只要 DB 不超卖)。

    • 解耦与泄洪: MQ 将瞬时写 DB 的压力化解为异步处理。

📌 总结:缓存双写策略的核心要点

  1. 目标: 在保证系统性能(利用缓存)的同时,尽力维持缓存与数据库的数据一致性。

  2. 没有银弹: 每种策略都有其优缺点和适用场景,Cache Aside(删缓存+更新DB) 是实践中最常用、相对平衡的方案。

  3. 秒杀场景特殊: 秒杀的核心是 Redis 预扣库存(写缓存) + DB 最终强一致扣减(写数据库) + MQ 异步化,是一种特定优化的双写模式。

  4. 一致性级别: 明确业务对一致性的要求(强一致?最终一致?能容忍多久不一致?)。

  5. 失败处理: 必须考虑每一步操作失败后的补偿或重试机制(如删缓存失败后重试、通过监听 Binlog 补偿缓存)。

  6. 监控: 监控缓存与 DB 的一致性延迟、双写失败率等指标至关重要。

选择哪种双写策略,需要根据具体的业务场景、性能要求、数据一致性要求以及团队的技术栈来综合权衡。在秒杀这种极端场景下,通常会采用 Cache Aside 的变种 + 强一致 DB 操作 + 异步最终一致 的组合方案。

### 缓存更新策略一致性解决方案 #### Cache Aside Pattern 一种常见的缓存更新策略是 **Cache Aside Pattern**,即缓存在更新时由调用方负责,在完成数据库更新之后再同步更新缓存[^1]。这种方式被称为方案,因为每次数据变更都需要同时修改数据库和缓存。 #### 数据一致性的本质问题 当涉及机制时,不可避免会出现数据不一致的情况。这是因为缓存和数据库之间的操作并非原子化执行,可能存在时间窗口使得两者状态不同步[^3]。 #### 设置过期时间和最终一致性 为了缓解这一问题,可以为缓存中的数据设定合理的过期时间。即使缓存更新失败,一旦到达预设的过期期限,后续读取请求将自动从数据库获取最新的值并重新填充到缓存中,从而逐步达成最终的一致性[^5]。 #### 主流解决方案及其适用场景 针对一致性问题,目前有四种主要的解决方案: 1. **强一致性锁机制** 使用分布式锁来确保同一时刻只有一个线程能够对某个键进行操作,无论是更新数据库还是刷新缓存都严格串行处理,以此保障实时一致性[^4]。 2. **异步消息队列驱动** 借助消息中间件(如Kafka、RabbitMQ),在数据库记录发生变化后发送事件通知给消费者服务,后者负责及时更新对应的缓存条目。 3. **基于版本号控制** 在每一条存储于数据库内的实体对象上增加一个`version`字段,每当发生改动便递增其数值;而关联至该资源的缓存则需携带相同的版本标识符,只有匹配成功才允许返回命中结果。 4. **TTL (Time-To-Live)** 方法 如前所述,利用短暂的有效期让陈旧副本自然淘汰掉,并依赖下一次查询触发加载新内容的过程重建临时高速缓冲层。 #### 生产环境下的最佳实践建议 综合考虑以上方法以及具体应用场景的要求,推荐采取如下措施以优化整体架构设计: - 对热数据采用较短生命周期管理; - 关键交易环节引入显式的事务边界界定; - 结合业务逻辑特性灵活选用适合的技术手段组合应对挑战。 ```python import redis from sqlalchemy import create_engine, update def db_update_and_cache_refresh(key, value): engine = create_engine('mysql+pymysql://user:password@host/dbname') with engine.connect() as conn: stmt = update(Table).where(Table.id == key).values(data=value) result = conn.execute(stmt) r = redis.Redis(host='localhost', port=6379, decode_responses=True) try: r.setex(name=key, time=300, value=value) # Set cache with TTL of 5 minutes except Exception as e: print(f"Failed to refresh cache due to {e}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值