MySQL 与 Redis 的数据一致性问题

embedded/2025/1/15 1:10:28/

      • 读数据的逻辑基本一致
      • 问题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) 的更新通过异步任务去实现的

mysqlRedis_99">问题: 如果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.ppmy.cn/embedded/153982.html

相关文章

2025年01月11日Github流行趋势

项目名称:xiaozhi-esp32 项目地址url:https://github.com/78/xiaozhi-esp32项目语言:C历史star数:2433今日star数:321项目维护者:78, MakerM0, whble, nooodles2023, Kevincoooool项目简介:构建…

Leetcode - 147双周赛

目录 一、3407. 子字符串匹配模式二、3408. 设计任务管理器三、3409. 最长相邻绝对差递减子序列四、3410. 删除所有值为某个元素后的最大子数组和 一、3407. 子字符串匹配模式 题目链接 字符串匹配问题,把字符串 p 分成两段 、,i 是 ’ * ’ 的下标&am…

React 进阶之路:深入详解事件绑定的多样方式与区别,促使更加容易理解

React 中的事件绑定是处理用户交互的一个重要方面。React 的事件系统与传统的 DOM 事件系统有所不同,它在设计时考虑了性能、可维护性和易用性,因此 React 提供了多种方式来绑定事件处理程序。理解这些绑定方式及其区别,有助于在实际项目中做…

open3d+opencv实现矩形框裁剪点云操作(C++)

👑主页:吾名招财 👓简介:工科学硕,研究方向机器视觉,爱好较广泛… ​💫签名:面朝大海,春暖花开! open3dopencv实现矩形框裁剪点云操作(C&#xff…

第三章、python中的对象、变量及地址的概念(3.1-3.4)------对象、变量、内存地址及可迭代对象

目录 3.1内存地址(或逻辑地址)、id()、is、in 3.2创建对象及对象的划分问题 3.3变量 3.3.1变量被赋值(=) 3.3.2变量无需声明数据类型 3.3.3变量的作用域(scope)及种类 3.4可迭代对象(Iterable) 第三章、python中的对象、变量及地址的概念 本章讲述编程中对象、变量、地址的基本…

JavaScript Chrome 中的运行

我们在 Chrome 浏览器中可以通过按下 F12 按钮或者右击页面,选择"检查"来开启开发者工具。 也可以在右上角菜单栏选择 "更多工具"》"开发者工具" 来开启: 1、Console 窗口调试 JavaScript 代码 清空 Console 窗口到内容可…

【HM-React】08. Layout模块

基本结构和样式reset 结构创建 实现步骤 打开 antd/Layout 布局组件文档,找到示例:顶部-侧边布局-通栏拷贝示例代码到我们的 Layout 页面中分析并调整页面布局 代码实现 pages/Layout/index.js import { Layout, Menu, Popconfirm } from antd impor…

第二章:HTML的常用标签

目录 一、标签 二、常用标签 1.排版标签 2.文本标签 3.图片标签img 4.列表 5.表格 6.表单 7.框架标签iframe 三、总结 一、标签 HTML是一种标记性语言,主要通过各种标签来呈现页面,不同标签有不同的语义和效果。注意:效果并不重要…