缓存双写策略

缓存双写策略(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 操作 + 异步最终一致 的组合方案。

### 三级标题:Redis 缓存与数据库一致性解决方案 在高并发系统中,为了提高性能,通常会使用 Redis 作为缓存来减少对数据库的直接访问。然而,引入缓存后,如何保证缓存和数据库之间的数据一致性成为了一个关键问题。以下是几种常见的解决方案,用于解决 Redis 缓存与数据库一致性问题。 #### 三级标题:更新数据库 + 更新缓存 在更新数据库的同时更新缓存,这种方法看似简单直接,但在并发情况下无法保证缓存和数据库的一致性。此外,如果缓存更新失败,会导致缓存中的数据与数据库中的数据不一致。因此,这种方法并不推荐在高并发场景下使用。 #### 三级标题:更新数据库 + 删除缓存 更新数据库后删除缓存,这种方法可以确保在下次读取时从数据库中获取最新的数据并重新填充缓存。然而,在并发情况下,如果在删除缓存和更新数据库之间有其他请求读取缓存,可能会导致读取到旧数据。为了解决这个问题,可以采用延迟策略,即在更新数据库后立即删除缓存,并在一段时间后再删除一次,以确保所有可能的旧数据都被清除。 #### 三级标题:先更新数据库,再删除缓存 为了保证两步都成功执行,可以配合消息队列或订阅变更日志的方案来做。通过消息队列将更新数据库和删除缓存的操作异步化,并通过重试机制保证数据一致性。这种方法的本质是通过重试的方式保证数据一致性。 #### 三级标题:使用读锁 对于并发几率很小的数据,可以通过加读锁来保证并发读的时候按顺序排好队。读读的时候相当于无锁。这种方法可以有效避免数据不一致问题,但会降低系统性能。 #### 三级标题:监听数据库变更日志 通过监听数据库的 binlog 日志及时去修改缓存,例如使用阿里开源的 Cana,这种方法可以实时更新缓存,但引入了新的中间件,增加了系统的复杂度。 ### 三级标题:代码示例 以下是一个简单的 Python 示例,展示如何使用 Redis 和数据库进行操作: ```python import redis import mysql.connector # 连接数据库 db = mysql.connector.connect( host="localhost", user="yourusername", password="yourpassword", database="yourdatabase" ) # 连接 Redis r = redis.Redis(host='localhost', port=6379, db=0) def update_data(data_id, new_value): cursor = db.cursor() try: # 更新数据库 cursor.execute("UPDATE yourtable SET value = %s WHERE id = %s", (new_value, data_id)) db.commit() # 删除缓存 r.delete(f"data:{data_id}") except Exception as e: db.rollback() print(f"Error: {e}") finally: cursor.close() def get_data(data_id): # 从缓存中获取数据 cached_data = r.get(f"data:{data_id}") if cached_data: return cached_data else: # 从数据库中获取数据 cursor = db.cursor() cursor.execute("SELECT value FROM yourtable WHERE id = %s", (data_id,)) result = cursor.fetchone() cursor.close() if result: # 将数据缓存 r.setex(f"data:{data_id}", 3600, result[0]) return result[0] else: return None ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值