因为Redis有更好的性能(20w qps),通常做数据缓存使用,查询的时候
那么数据要更新,如何解决数据一致性问题?
为什么需要先写 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) 的更新通过异步任务去实现的
首先明确这是一个低概率事件,清除数据,没有任何复杂的逻辑,仅仅是清除,很少出现失败的问题
一般会选择重试来解决偶然性(偶尔因为网络问题)的失败
如果重试一直失败怎么办?
所以设置一个最大重试次数,超过应该立即告警
幂等问题一般要通过唯一键验证来解决,比如点赞,那么就记录一下谁给谁点了赞,如果记录已经存在就不在增加点赞数量.
但是这边是重试 Redis 写(清除缓存的任务),重试不会产生幂等问题
好处实现简单
清除 Redis->更新mysql->再清除 Redis
单独开一个线程监听 mysql 的 binlog 日志,如果有更新,我们就对应的删除 Redis 对应的 key
我们的业务层只需要关系 mysql 的更新就可以了
思考: 监听日志更新缓存数据行不行?
我们使用的是清除 Redis 的策略,那么如果数据是一个热点数据,有频繁的更新与查询会发生什么?
这种清除 Redis 的策略如果有频繁的更新对导致缓存层(Redis) 会失效, 大量的请求会打到 mysql 上面,mysql 可能直接被打爆,造成严重的事故.
(热 key 失效,缓存击穿问题)
场景:
假设现在是一个短视频的功能,有一个爆火的视频,用户疯狂的点赞,评论,收藏;
每一个操作都会更新视频的数据(点赞数,评论数,收藏数);
如果我们清除Redis 的缓存数据,所有的获取视频数据的请求都全部打到mysql,mysql 必被打爆.这时候怎么办?
两个思路:
首先明确频繁更新的数据到底是什么?
一般情况下频繁更新的数据都是计数类数据(观看量,点赞数,评论数,收藏数)这一类是频繁更新的数据;像什么内容,名称,简介,详情一般是不会做频繁更新的(谁家好人疯狂改自己的名字);
针对计数类数据的方案就是,增量缓存,定时更新到 mysql 策略,避免数据频繁更新行为导致 Redis 缓存长期失效造成击穿
具体做法
如果在更新期间有查询怎么办?
逻辑是一样的(原始数据+增量)
因为我们更新完成(mysql 更新成功)同时清除 Redis 的缓存记录 并将数据的增量设置为 0(lua 脚本实现两个操作的原子性)
上面的方法可以减少热 key 失效的概率,但是这样是无法避免热 key 失效的.
还有两个热 key 失效的情景
具体做法
我们可以维护一个热 key 的数据有哪些
lfu : 一般使用一段时间 (1s 或者 1 分钟)key的访问次数 如果达到某个阀值(比如每秒访问超过 100 次的就算热 key)
对于这一类 key 不设置过期时间,等到热 key 不再热(低于 100 次时)就再次加上过期时间(避免不设置过期时间的 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