如何保证Redis与MySQL的数据同步?(如何保障缓存与DB的双写一致性?)

目录

方案一:延迟双删

步骤

“延迟” 的原因详解 

关键点

项目应用场景举例 (单体架构 - 博客系统)

方案二:读写锁 (精简版解释)

核心思想

步骤

为什么能保证一致性?(类比图书馆借书)

关键点

项目应用场景举例 (微服务项目 - 秒杀扣库存)

方案横向对比斟酌

Redis缓存同步方案对比表

选型决策树(根据业务场景快速判断)


方案一:延迟双删

  1. 步骤

    • 先删缓存: 在更新MySQL数据之前,先删除Redis中对应的缓存数据。
    • 写数据库: 执行MySQL的Update写操作。
    • (延迟后) 再删缓存: 在Step 2完成后,等待一段时间(比如500ms或1s)再次删除Redis中对应的缓存数据。
  2. “延迟” 的原因详解 

    • 问题在于 “主从复制延迟” 或 “数据库集群同步延迟”。一般情况下数据库是主从模式,读写分离,MySQL的主库更新成功,不代表从库(或其他集群节点)立刻就能读到最新的数据。
    • 想象一个场景,比如用户修改个人信息:
      • 用户A发起更新操作(比如修改用户名),执行了Step1(删缓存)和Step2(更新主库)。
      • 几乎在同时,用户B发起读取操作。
      • B发现缓存没有(Step1已删),于是去读从库
      • 但是! 此时主库的新数据可能还没同步到从库,B从从库读到了旧的用户名数据
      • B把读到的旧数据写回到了Redis缓存中。
      • 这时,用户A的Step3(延迟删缓存)还没执行。缓存里就被B填满了旧数据!这就产生了脏数据
    • “延迟”的作用: 这个等待时间,就是为了让主库的数据有足够的时间同步到从库。等过了这个延迟时间,再次删除缓存(Step3),就能确保:
      • 在延迟期间内,如果再有像B这样的请求读了从库旧数据并试图写入缓存,它写入的缓存会被Step3删除掉。
      • 延迟过后,再有新的读取请求,发现缓存没有,会去读(已经同步了最新数据的)从库,取到新数据并写入缓存。
  3. 关键点

    • 延迟时间估算: 延迟时间需要根据你的MySQL主从同步平均耗时来设定(比如观察监控,设置为平均耗时的2倍)。这不是一个精确值,但能大幅降低脏数据风险
    • 不保证强一致 在Step1删除缓存后到Step3执行前的这段时间,以及主从延迟期间,依然存在短暂的不一致窗口。这是为了性能和可用性做的妥协(最终一致性)。
    • 第二次删除失败怎么办? 这是个实际问题!面试官可能会追问。
      常见方案:将第二次删除操作放入消息队列进行重试,或记录日志进行补偿。可使用:RabbitMQ/Kafka等中间件实现!
  4. 项目应用场景举例 (单体架构 - 博客系统)

    • 用户编辑一篇已发布的博客文章(更新MySQL的articles表)。
    • 流程:
      • 先删Redis缓存:DEL article:123 (假设123是文章ID)。
      • 更新MySQL:UPDATE articles SET content = '新内容' WHERE id = 123
      • 等待500ms (预估数据库同步时间)。
      • 再次执行:DEL article:123
    • 这样就能最大程度保证别人刷新页面看到的是最新文章内容,即使存在主从延迟。

方案二:读写锁 (精简版解释)

  1. 核心思想

    利用锁来控制对“特定数据”的并发访问,保证在更新数据时(写操作),相关的读操作(查缓存+查DB+回填缓存) 能看到一致的结果。
  2. 步骤

    • 写操作 (更新数据):
      1. 获取该数据对应的写锁 (排他锁)。这个锁可以是Redis自己实现的分布式锁(如Redisson的RReadWriteLock),也可以是应用层的锁(需保证分布式环境有效)。关键:这个锁要能锁住“特定数据”(比如按ID锁)。
      2. 删除缓存。
      3. 更新数据库。
      4. 释放写锁。
    • 读操作 (获取数据):
      1. 尝试获取该数据对应的读锁 (共享锁)。
      2. 获取到读锁后,读缓存:
        • 缓存命中 -> 返回数据 -> 释放读锁。
        • 缓存未命中 -> 读数据库 -> 将数据库结果写入缓存 -> 返回数据 -> 释放读锁。
  3. 为什么能保证一致性?(类比图书馆借书)

    • 写锁 (排他锁): 就像图书管理员在更新某本书的信息(比如更正页码错误)。在管理员操作期间:
      • 他会把这本书从书架上先拿走(相当于删缓存)
      • 然后在后台修改记录(更新DB)
      • 操作期间,不允许任何人(读者)同时读这本书(阻塞读操作),也不允许其他管理员修改这本书(阻塞其他写操作)
    • 读锁 (共享锁): 就像读者想借阅这本书。
      • 如果管理员正在修改这本书(持有写锁),读者需要等待管理员完成(释放写锁)。
      • 如果管理员没在修改,读者可以同时和多个读者一起阅读这本书(多个读锁可以共存)
      • 读者读完后,会把书放回书架(相当于可能回填缓存)。
    • 锁导致的阻塞: 在写操作期间(持有写锁),读操作会被阻塞,直到写操作完成(释放写锁)。这就保证了在写操作进行中以及完成后,读操作要么读到新数据,要么需要重新从DB加载新数据(因为缓存被删了),不会读到写了一半的脏数据或旧数据。
  4. 关键点

    • 锁粒度: 锁的粒度要合适(比如按数据ID锁)。锁整个库或表性能太差。
    • 性能开销: 加锁解锁本身有开销,在高并发读场景下(多个读锁共存)性能尚可,但在高并发写场景下(写锁互斥)可能成为瓶颈。
    • 分布式锁: 在微服务环境下,必须使用分布式锁(如Redis实现的Redisson RLock)才能让锁在所有服务实例间生效。
    • 避免死锁: 确保锁最终一定会释放(设置超时时间)。
    • 保证最终一致: 主要解决了并发冲突下的脏数据问题。
  5. 项目应用场景举例 (微服务项目 - 秒杀扣库存)

    • 场景:多个用户同时抢购最后一件商品。
    • 读写锁应用:
      • 扣减库存 (写操作):
        • 获取商品ID=1001的写锁
        • 删Redis缓存 stock:1001
        • MySQL执行 UPDATE stock SET count = count - 1 WHERE id = 1001 AND count > 0
        • 释放写锁
      • 查询库存 (读操作):
        • 获取商品ID=1001的读锁
        • 查Redis缓存 stock:1001
        • 如果没查到,查MySQL SELECT count FROM stock WHERE id = 1001
        • 将结果写入Redis缓存 stock:1001
        • 返回库存数。
        • 释放读锁
    • 这样能有效防止在扣减库存的瞬间,其他请求读到错误(未扣减)的库存数量并回填到缓存。

方案横向对比斟酌

Redis缓存同步方案对比表

对比维度延迟双删读写锁
核心目标降低主从延迟导致的脏数据风险,追求最终一致性控制并发读写时序,在锁生效期内接近强一致
实现复杂度★★☆☆☆ (简单)
• 只需控制两次删除动作 + 延时队列
★★★★☆ (复杂)
• 需引入分布式锁机制
• 要管理读写锁的生命周期
性能影响• 写操作:轻微延迟 (等主从同步)
• 读操作:无阻塞
• 写操作阻塞读操作
• 高并发写场景可能成瓶颈
• 读多写少时性能较好
一致性强度最终一致 (存在短暂不一致窗口)准强一致 (写锁期间绝对一致,整体最终一致)
可靠性关键点• 二次删除失败需补偿 (如MQ重试)
• 依赖主从同步延迟时间的估算
• 锁超时需合理设置 (防死锁)
• 锁粒度要细 (如按数据ID锁)
适用场景• 容忍短暂不一致
• 读多写少
• 主从延迟可控
• 案例:博客文章更新、商品信息修改
• 对一致性要求高
• 写操作耗时短
• 并发冲突严重场景
• 案例:秒杀库存扣减、账户余额更新
不适用场景• 强一致性要求场景
• 主从延迟波动大
• 二次删除补偿机制缺失
• 写操作耗时过长
• 超高并发写
• 无法承受读阻塞
项目落地案例单体项目(苍穹外卖)
• 更新菜品价格时,延迟双删防止用户看到旧价格
微服务项目
• 秒杀扣库存时用读写锁,避免超卖和脏读
面试追问防御点Q:第二次删除失败怎么办?
• 答:MQ重试补偿 + 日志告警兜底
Q:写锁阻塞导致系统卡顿怎么办?
• 答:最小化锁内操作 + 设置锁超时 + 降级熔断

选型决策树(根据业务场景快速判断)

### 实现 MySQL 数据库 Redis 缓存一致性的策略 #### 同步直策略 为了保持缓存数据库一致性,在执行操作时,可以采取同步直的办法。这意味着每次向MySQL数据库提交更改的同时也会立即更新Redis缓存的内容[^1]。 ```python def update_data_sync(key, value): try: mysql_db.execute(f"UPDATE table SET column='{value}' WHERE id={key}") redis_client.set(key, value) except Exception as e: handle_exception(e) ``` 这种方法简单直接,但是存在一些缺点:增加了系统的复杂度以及潜在的失败风险;另外由于两次独立的操作可能会因为网络延迟等原因造成短暂的不同步现象。 #### 异步缓策略 另一种方式是通过消息队列机制来间接完成数据的一致性维护工作。每当有新的记录被加入到MySQL中后,不是立刻刷新整个缓存而是发送一条通知给专门负责处理此类事件的服务组件,由其按照一定的时间间隔批量地清理过期条目并重新加载最新的状态信息至Redis内[^2]。 ```python import pika connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() def callback(ch, method, properties, body): key, value = parse_body(body) redis_client.set(key, value) channel.basic_consume(queue='data_updates', on_message_callback=callback, auto_ack=True) channel.start_consuming() ``` 这种方式能够有效降低即时响应时间内的负载压力,并且提高了整体架构灵活性可扩展能力。不过需要注意的是,这可能导致短期内某些查询返回的结果并不是最新版本的数据副本。 #### 基于 Binlog 的最终一致性保障 利用MySQL自身的二进制日志功能(Binlog),可以在事务完成后自动触发相应的变更传播动作直至目标存储节点——在这里指的就是关联着特定键空间范围内的Redis实例群集。此过程通常借助额外的消息中间件如Kafka或者RabbitMQ来进行协调管理,确保即使在网络分区期间也能维持全局视角下相对稳定的状态转换路径[^3]。 ```sql -- 配置 MySQL 主服务器开启 binlog 功能 server-id = 1 log-bin = /var/log/mysql/mysql-bin.log expire_logs_days = 7 max_binlog_size = 100M ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值