MySQL 与 Redis 的数据一致性问题

      • 读数据的逻辑基本一致
      • [问题1: 一致性有哪些?](#问题1: 一致性有哪些?)
      • [MySQL 与 Redis 的数据一致性方案有哪些?](#MySQL 与 Redis 的数据一致性方案有哪些?)
      • [问题: 热 key 失效 问题](#问题: 热 key 失效 问题)
          • [思路1: 让热key不丢](#思路1: 让热key不丢)
          • [思路2: 热key 失效的限流策略](#思路2: 热key 失效的限流策略)

读数据的逻辑基本一致

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

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

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

问题1: 一致性有哪些?

  • 强一致: 更新数据同时生效(简单理解就是原子的,只要数据一改,无论怎么查询获取的都是最新的数据)
    • 一般会使用事务保证MySQL与Redis的数据强一致
  • 弱一致: 提交更新之后允许一段时间内的数据不一致
    • 最终一致性:最终一致性是弱一致的一个,它允许更新后短时间内的数据不一致情况,但是最终会达成数据一致
    • 一般会使用重试或者补偿机制确保数据的最终一致性

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 失效

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

标记限流策略:

具体做法

  • 如果查询 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

相关推荐
松涛和鸣1 小时前
72、IMX6ULL驱动实战:设备树(DTS/DTB)+ GPIO子系统+Platform总线
linux·服务器·arm开发·数据库·单片机
likangbinlxa1 小时前
【Oracle11g SQL详解】UPDATE 和 DELETE 操作的正确使用
数据库·sql
r i c k2 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦2 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL3 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·3 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德3 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫4 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i4 小时前
完全卸载MariaDB
数据库·mariadb
期待のcode4 小时前
Redis的主从复制与集群
运维·服务器·redis