当前位置: 首页 > article >正文

MySQL 与 Redis 的数据一致性问题

      • 读数据的逻辑基本一致
      • 问题1: 一致性有哪些?
      • MySQL 与 Redis 的数据一致性方案有哪些?
          • 先写MySQL还是先写Redis?
          • 缓存数据是更新还是清除?
          • 强一致还是最终一致?
            • 问题: 如果mysql写成功了,但是Redis写(删除)失败了怎么办?
            • 重试机制的幂等问题如何解决?
        • 方案1: 先更新 MySQL 再清除 Redis
        • 方案2: 双删策略
        • 方案3: 监听MySQL的binlog日志删除
      • 问题: 热 key 失效 问题
          • 思路1: 让热key不丢
          • 思路2: 热key 失效的限流策略
            • **对于过期淘汰的解决方案**
            • **对于无法避免的热 key 失效**

读数据的逻辑基本一致

因为Redis有更好的性能(20w qps),通常做数据缓存使用,查询的时候

  • 先检查Redis,如果命中直接返回.
  • Redis未命中,再检查MySQL,
  • 将MySQL命中的数据同步到Redis,并且做返回

那么数据要更新,如何解决数据一致性问题?

问题1: 一致性有哪些?

  • 强一致: 更新数据同时生效(简单理解就是原子的,只要数据一改,无论怎么查询获取的都是最新的数据)
    • 强一致性实现的方案有哪些
      • 分布式事务
      • 共识算法(比如raft)
      • 但是因为强一致非常影响性能所以强一致方案很少使用
  • 弱一致: 提交更新之后允许一段时间内的数据不一致
    • 最终一致性:最终一致性是弱一致的一个,它允许更新后短时间内的数据不一致情况,但是最终会达成数据一致
    • 一般会使用重试或者补偿机制确保数据的最终一致性

MySQL 与 Redis 的数据一致性方案有哪些?

先写MySQL还是先写Redis?
  • 原则: 谁保存全量持久化数据先更新谁

为什么需要先写 MySQL?

  • 避免MySQL数据覆盖,丢失更新;(造成永久性错误)

如果先 操作缓存数据( Redis )有什么问题 ?

先更新/清除缓存(Redis)数据,再更新MySQL 的问题:

假设有两个连续的更改视频标题的请求,
请求 1改为 A; 请求 2改为 B

先写(更新/清除) Redis,两次清除 / 先改成 A,然后改成 B(Redis 是单线程的,请求将顺序执行)

这时候请求 1 的线程处理比较慢(或者阻塞了一下)

这时候请求 2 先更新了持久化全量数据(MySQL) 中记录:改为 B

然后请求 1 才开始更改MySQL 中的数据:改为 A

这时候 MySQL 的数据是错的,并且重启也无法恢复的错误
(因为 Redis 是缓存,如果数据不一致(Redis 数据不对)可以将 MySQL 数据刷到 Redis 也能达成一致,但是如果 MySQL 数据不对将无法修复)

这里只是改名字的例子,如果涉及到交易问题将更严重.

缓存数据是更新还是清除?

上面已经确定要先写 MySQL,再写 Redis

那么是更新还是清除呢?

  • 原则: 保证数据的一致性,一 般使用清除缓存的方式

还是上面的例子

如果更新缓存数据而不是删除存数据( Redis )有什么问题 ?

假设有两个连续的更改视频标题的请求,
请求 1改为 A; 请求 2改为 B

先更新 MySQL,先改成 A,然后改成 B

这时候请求 1 的线程处理比较慢(或者阻塞了一下)

这时候请求 2 先更新了 缓存数据( Redis) 中 的记录

然后请求 1 才开始更改 Redis 中的数据

这时候Redis 的数据是错误的,会导致后面查询的时候全部查询到错误的数据(只能重新加载 MySQL 数据到 Redis 才能恢复)

简单来讲,我们只能保证先到的请求的第一阶段写的执行顺序(MySQL 内部的事务),第二阶段写就无法保证执行顺序(除非使用强一致性方案),这时候如果使用更新 Redis 的方案就有数据错误的风险

强一致还是最终一致?

强一致

一般强一致实现是通过事务实现的

  • 开启一个mysql事务(start)

  • 操作mysql,更新数据(这里在事务提交之前都是会持续占有资源,其他请求要更改就会阻塞,直到事务提交)

  • 操作 Redis 删除(强一致也可以更新)数据

  • 提交事务(释放事务过程中的锁,让其他请求可以执行)

  • 基本原理就是借助mysql 事务的原子性来实现 mysql 与 Redis 数据的强一致性)

一般的场景:
银行,金融这种安全性特别高的场景会使用强一致性

最终一致

一般是异步任务,加上重试机制与补偿机制确保最终一致性

核心原理就是只要 mysql 更新成功了,就认为数据更新成功了,而缓存(Redis) 的更新通过异步任务去实现的

问题: 如果mysql写成功了,但是Redis写(删除)失败了怎么办?
  • 首先明确这是一个低概率事件,清除数据,没有任何复杂的逻辑,仅仅是清除,很少出现失败的问题

  • 一般会选择重试来解决偶然性(偶尔因为网络问题)的失败

如果重试一直失败怎么办?

  • 如果重试一直失败一般是 Redis 不可用了,或者服务端与Redis 的网络不可用了;
  • 这时候已经不是偶尔失败了,而是所有的(Redis)请求 将全部失败,这时候应该立即告警,并且限流,降级,熔断保护服务(mysql 等)不被打爆;
  • 然后立即抢修

所以设置一个最大重试次数,超过应该立即告警

重试机制的幂等问题如何解决?

幂等问题一般要通过唯一键验证来解决,比如点赞,那么就记录一下谁给谁点了赞,如果记录已经存在就不在增加点赞数量.

但是这边是重试 Redis 写(清除缓存的任务),重试不会产生幂等问题

  • Redis 不要更新缓存数据,而是清除缓存
方案1: 先更新 MySQL 再清除 Redis
  • 收到更新的请求,先更新(update) mysql 数据,
  • 如果更新完成就开一个线程做Redis 清除,
  • 同时做返回

好处实现简单

方案2: 双删策略

清除 Redis->更新mysql->再清除 Redis

  • 个人感觉很鸡肋(与第一种方案的效果相似,但性能更差)
方案3: 监听MySQL的binlog日志删除

单独开一个线程监听 mysql 的 binlog 日志,如果有更新,我们就对应的删除 Redis 对应的 key

我们的业务层只需要关系 mysql 的更新就可以了

  • 优势:业务逻辑更简单,同时避免反复创建与销毁线程带来的性能损耗
  • 但是需要 mysql 开启 binlog 日志(如果服务本身没有 binlog 的需求的话单独开会增加额外的消耗)

思考: 监听日志更新缓存数据行不行?

问题: 热 key 失效 问题

我们使用的是清除 Redis 的策略,那么如果数据是一个热点数据,有频繁的更新与查询会发生什么?

这种清除 Redis 的策略如果有频繁的更新对导致缓存层(Redis) 会失效, 大量的请求会打到 mysql 上面,mysql 可能直接被打爆,造成严重的事故.

(热 key 失效,缓存击穿问题)

场景:
假设现在是一个短视频的功能,有一个爆火的视频,用户疯狂的点赞,评论,收藏;
每一个操作都会更新视频的数据(点赞数,评论数,收藏数);
如果我们清除Redis 的缓存数据,所有的获取视频数据的请求都全部打到mysql,mysql 必被打爆.这时候怎么办?

两个思路:

思路1: 让热key不丢
  • 首先明确频繁更新的数据到底是什么?

    • 一般情况下频繁更新的数据都是计数类数据(观看量,点赞数,评论数,收藏数)这一类是频繁更新的数据;像什么内容,名称,简介,详情一般是不会做频繁更新的(谁家好人疯狂改自己的名字);

    • 针对计数类数据的方案就是,增量缓存,定时更新到 mysql 策略,避免数据频繁更新行为导致 Redis 缓存长期失效造成击穿

  • 具体做法

    • 计数类数据单独(Redis)缓存增量,然后定时刷到 mysql 中,而不是每次都更新数据
    • 查询的时候先查询 Redis 的原始数据(旧记录)与最近时间数据的增量(点赞,评论,收藏的增量),在服务层做计算统计,再返回给客户端,这样就可以避免频繁更新问题

如果在更新期间有查询怎么办?
逻辑是一样的(原始数据+增量)
因为我们更新完成(mysql 更新成功)同时清除 Redis 的缓存记录 并将数据的增量设置为 0(lua 脚本实现两个操作的原子性)

思路2: 热key 失效的限流策略

上面的方法可以减少热 key 失效的概率,但是这样是无法避免热 key 失效的.

还有两个热 key 失效的情景

  • 过期淘汰
  • 数据更新(比如定时刷新增量数据/作者更改了视频的详情(名字/简介/详情等))
对于过期淘汰的解决方案
  • 热key 不淘汰

具体做法
我们可以维护一个热 key 的数据有哪些

lfu : 一般使用一段时间 (1s 或者 1 分钟)key的访问次数 如果达到某个阀值(比如每秒访问超过 100 次的就算热 key)

对于这一类 key 不设置过期时间,等到热 key 不再热(低于 100 次时)就再次加上过期时间(避免不设置过期时间的 key 越来越多),这样避免热 key 失效问题

对于无法避免的热 key 失效

数据更新的清除缓存行为(定时的增量数据刷新/用户更改)

  1. 对用户进行限流: 比如用户每分钟只能改一次数据

  2. 标记限流策略:

具体做法

  • 如果查询 key 未命中 Redis,那么对改数据 key进行标记(使用 lua 脚本 对 key 储存一个状态-更新中…),后面的请求(在这个请求将数据同步到Redis 前)全部拒绝,

  • 然后这个请求去查询 mysql 并同步到Redis(覆盖 key 刚刚设置的状态)

  • 为了避免永久"更新中"问题,设置更新中状态的时候需要携带过期时间,避免查询途中服务器宕机导致数据状态一直处于更新中

参考:
tps://www.cnblogs.com/coderacademy/p/18137480
https://juejin.cn/post/6964531365643550751
https://www.cnblogs.com/huang580256/p/17299585.html
https://blog.csdn.net/weixin_45433817/article/details/130814075


http://www.kler.cn/a/502556.html

相关文章:

  • 八股学习 Redis
  • 【某大型互联网企业】软件测试面试经验分享(1 ~ 3年)
  • Java Web开发进阶——错误处理与日志管理
  • 多模态论文笔记——CLIP
  • python 寻找数据拐点
  • 设计一篇利用python爬虫获取1688详情API接口的长篇软文
  • Linux自定义分隔符
  • 【14】模型训练自制数据集前的一些数据处理操作
  • 基于springboot果蔬供应链信息管理平台
  • Linux 下 Vim 环境安装踩坑问题汇总及解决方法(重置版)
  • AI学习路线图-邱锡鹏-神经网络与深度学习
  • 双线性插值算法:原理、实现、优化及在图像处理和多领域中的广泛应用与发展趋势(二)
  • 【数据库】Mysql精简回顾复习
  • 【人工智能】自然语言生成的前沿探索:利用GPT-2和BERT实现自动文本生成与完形填空
  • python-leetcode-长度最小的子数组
  • C#版 软件开发6大原则与23种设计模式
  • 【理论】测试框架体系TDD、BDD、ATDD、MBT、DDT介绍
  • 2025年华为OD上机考试真题(Java)——整数对最小和
  • vulnhub靶场【IA系列】之Keyring
  • 关于Java状态模式的面试题及其答案
  • 全网首发:编译libssh,产生类似undefined reference to `EVP_aes_256_ctr@OPENSSL_1_1_0‘的大量错误
  • 基于深度学习的滑块验证破解方法及模型训练过程
  • 算法日记2:洛谷p3853路标设置(二分答案)
  • 女性机器人有市场吗
  • Scaling Laws:通往更大模型的路径
  • Mysql常见知识点