问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

如何保障 MySQL 数据库和 Redis 缓存数据一致性

创作时间:
作者:
@小白创作中心

如何保障 MySQL 数据库和 Redis 缓存数据一致性

引用
CSDN
1.
https://blog.csdn.net/m0_74109105/article/details/146461773

在高并发业务场景中,数据库通常是系统架构中最容易成为性能瓶颈的环节。为了解决这一问题,我们常常引入 Redis 作为缓存层,在访问链路中充当缓冲区角色,让用户请求优先访问 Redis 而非直接查询 MySQL 等关系型数据库,从而显著降低数据库的访问压力。本文将聚焦于 MySQL 数据库与 Redis 缓存操作的先后顺序问题,而非深入探讨各种缓存策略(如旁路缓存、读写穿透策略或写回策略等)的实现细节。通过分析不同的操作顺序,我们将探讨如何有效地保障缓存与数据库之间的数据一致性。

方案分析

注意:每种方案都存在特定条件下的缓存一致性问题,选择方案时需要根据业务特性权衡利弊。

先删除 Redis,再写 MySQL,再删除 Redis

原理分析:

  • 首先删除缓存,避免其他请求读取到旧数据
  • 然后更新数据库中的数据
  • 最后再次删除缓存(双删策略)

优点:

  • 通过二次删除解决并发问题,防止其他线程在第一次删除和数据库更新之间读取旧数据并写入缓存
  • 相比直接更新缓存,删除操作更简单且不易出错
  • 下次读取时会自动加载最新数据到缓存

缺点:

  • 操作步骤多,增加了复杂度
  • 如果最后一次删除失败,仍可能导致数据不一致
  • 增加了系统开销,需要执行两次删除操作

适用场景:

  • 写操作较少、读操作较多的系统
  • 对数据一致性要求较高的业务场景

具体问题场景:

时间点1: 线程 A 删除商品 X 的 Redis 缓存(X=100)
时间点2: 线程 B 读取商品 X,发现缓存不存在,从 MySQL 读取(X=100)并重建缓存
时间点3: 线程 A 更新 MySQL 中商品X的值(X=50)
时间点4: 线程 A 执行第二次删除 Redis 缓存,但由于网络问题失败
结果: Redis 中缓存值仍为 X=100,而 MySQL 中 X=50,数据不一致

问题本质:即使采用双删策略,如果第二次删除失败,或者两次删除间隔时间过长,仍可能导致数据不一致。

先写 MySQL,再删除 Redis

原理分析:

  • 先保证数据库数据更新成功
  • 然后删除缓存,下次读取时会从数据库加载最新数据

优点:

  • 操作简单,仅有两个步骤
  • 保证了数据库中始终是最新数据
  • 实现简单,不需要复杂的补偿机制

缺点:

  • 在删除缓存前,如果有读请求,会读到旧数据
  • 如果删除缓存失败,会导致缓存数据长时间不一致
  • 高并发场景下可能出现"缓存击穿"问题

适用场景:

  • 读写比例相对平衡的系统
  • 数据更新频率不是特别高的场景

具体问题场景:

时间点1: 线程 A 更新 MySQL 中用户余额 Y=1000
时间点2: 线程 B 读取用户余额,从 Redis 获取旧值 Y=800
时间点3: 线程 A 删除 Redis 缓存
时间点4: 线程 B 基于旧值 Y=800 进行业务处理
结果: 线程 B 使用了旧数据进行业务处理,可能导致业务错误

问题本质:在更新数据库到删除缓存的时间窗口内,其他线程可能读取到旧的缓存数据。

先写 MySQL,通过 Binlog,异步更新 Redis

原理分析:

  • 先更新数据库
  • 通过订阅 MySQL 的 Binlog 变化,异步更新或删除对应的缓存

优点:

  • 完全解耦了缓存更新逻辑,不影响主业务流程
  • 基于事件驱动,可以保证数据最终一致性
  • 不会增加业务代码的复杂度
  • 可以捕获所有的数据变更,包括其他系统导致的变更

缺点:

  • 实现较为复杂,需要额外的组件(如 Canal)来解析 Binlog
  • 存在一定的时间延迟,只能保证最终一致性
  • 对数据库主从架构有一定要求

适用场景:

  • 大型系统,数据变更来源多样
  • 可以接受短暂的数据不一致
  • 希望减少业务代码的复杂度

具体问题场景:

时间点1: 系统执行大批量数据更新,更新订单状态
时间点2: Binlog 解析组件出现延迟,队列堆积
时间点3: 用户查询订单状态,从 Redis 获取到旧状态
时间点4: 10 分钟后 Binlog 才处理完成,Redis 缓存更新
结果: 在这 10 分钟窗口期内,用户看到的订单状态是错误的

问题本质:异步更新存在天然的延迟,在高峰期或系统异常时,延迟可能变大,影响业务体验。

先写 MySQL,再写 Redis

原理分析:

  • 先更新数据库
  • 然后直接更新缓存(而非删除)

优点:

  • 读取缓存时总能拿到最新数据,无需回源到数据库
  • 减少了缓存缺失情况,提高了读性能
  • 避免了缓存雪崩问题

缺点:

  • 如果缓存更新失败,会导致数据不一致
  • 更新缓存比删除缓存更复杂,需要完整构建缓存数据
  • 两个写操作必须都成功,否则需要额外的补偿机制

适用场景:

  • 读多写少且读性能要求高的系统
  • 缓存重建成本较高的场景

具体问题场景:

时间点1: 线程 A 更新 MySQL 商品库存 Z=30
时间点2: 线程 B 也更新同一商品库存 Z=25
时间点3: 线程 B 更新 Redis 缓存 Z=25
时间点4: 线程 A 因为网络延迟现在才更新 Redis 缓存 Z=30
结果: MySQL 中库存为 25,Redis 中为 30,数据不一致

问题本质:并发更新时,由于执行顺序不同或者网络延迟问题,可能导致缓存中存在非最新值,如果这期间有业务读取并处理,会有问题。

先写 Redis,再写 MySQL

原理分析:

  • 先更新缓存
  • 然后再更新数据库

优点:

  • 用户可以立即看到操作结果,提升用户体验
  • 适合对实时性要求极高的场景

缺点:

  • 违反了"数据库是真实数据源"的设计原则
  • 如果数据库更新失败,缓存和数据库会长期不一致
  • 数据库故障可能导致数据永久丢失
  • 难以处理分布式事务问题

适用场景:

  • 实时性要求极高的特殊业务场景
  • 可以容忍数据丢失风险的非关键数据
  • 临时性数据或统计数据

具体问题场景:

时间点1: 线程 A 更新 Redis 中秒杀商品数量 W=0(表示已售罄)
时间点2: 用户看到商品已售罄,无法下单
时间点3: 线程 A 准备更新 MySQL,但 MySQL 宕机
时间点4: 系统重启,Redis 数据丢失,重建缓存显示 W>0
结果: 缓存数据丢失,且无法从数据库恢复正确状态,导致超卖

问题本质:如果数据库更新失败,缓存中的数据没有持久化保障,可能导致数据永久丢失或不一致。

先删除 Redis,再写 MySQL

原理分析:

  • 先删除缓存
  • 然后更新数据库

优点:

  • 操作步骤简单,只需两步
  • 避免了缓存更新的复杂性

缺点:

  • 删除缓存后、更新数据库前,如果有读请求,可能会将旧数据重新写入缓存
  • 在高并发场景下容易出现数据不一致问题
  • 可能导致频繁的缓存缺失,增加数据库压力

适用场景:

  • 写操作较少的系统
  • 对缓存一致性要求不是特别高的场景
  • 缓存构建逻辑复杂的情况

具体问题场景:

时间点1: 线程 A 删除 Redis 中用户积分缓存
时间点2: 线程 B 读取用户积分,发现缓存不存在,从 MySQL 读取(积分=100)并重建缓存
时间点3: 线程 A 更新 MySQL 中用户积分为150
结果: Redis 中积分=100,MySQL 中积分=150,数据不一致

问题本质:删除缓存后、更新数据库前,如果有读请求介入,会导致旧数据被重新写入缓存,造成长期不一致。

结论

  • 实时一致性方案:推荐采用"先写 MySQL,再删除 Redis"的策略。虽然在理论上仍存在数据短暂不一致的可能,但这种不一致出现的条件较为苛刻,需要多个特定因素同时满足。因此,在需要保证实时性的场景下,该方案是能够在最大程度上保障数据一致性的最优解决方案。
  • 最终一致性方案:推荐采用"先写 MySQL,通过 Binlog 异步更新 Redis"的策略。通过订阅 MySQL 的 Binlog 变更,结合消息队列机制异步更新 Redis 缓存,完全解耦了缓存更新逻辑,不会影响主业务流程。该方案虽然存在一定延迟,但能够可靠地保证数据的最终一致性,特别适合大型分布式系统和可接受短暂不一致的业务场景。

两种方案各有优势,具体选择应根据业务对实时性与一致性的需求权衡决定。在实践中,可以针对不同业务模块采用不同的缓存一致性策略,以达到最佳的系统性能和数据可靠性平衡。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号