Redis的定位是缓存,缓存的主要目的是为了减轻MySQL(DB层)的请求压力,并且快速响应
所以缓存一定要保持高性能,但是强一致性会严重破坏高性能的特性,所以一般是采用最终一致性的方案.
你能找到的市面上大部分的解决方案都是最终一致性的
先写 MySQL
为什么需要先写 MySQL?
避免MySQL数据覆盖,丢失更新;(造成永久性错误)
因为并发情况下先写 Redis,无法保证 MySQL 写的时候是顺序的
假设有两个连续的更改视频标题的请求,
先写 Redis先改成 A,然后改成 B(Redis 是单线程的,请求将顺序执行)
这时候请求 1 的线程处理比较慢(或者阻塞了一下)
这时候请求 2 先更新了持久化全量数据(MySQL) 中记录:改为 B
然后请求 1 才开始更改MySQL 中的数据:改为 A
这时候 MySQL 的数据是错的,并且重启也无法恢复的错误
如果先更新 Redis 会造成无法修复的数据不一致(MySQL 数据是错误的)
结论: 先做 MySQL 的更新,再更新 Redis
原则: 必须避免缓存击穿(大量访问打到MySQL 将 MySQL 打爆)
假设有两个连续的更新视频标题的请求
先更新 MySQL,先改成 A,然后改成 B
这时候请求 1 的线程处理比较慢(或者阻塞了一下)
这时候请求 2 先更新了 缓存数据( Redis) 中 的记录
然后请求 1 才开始更改 Redis 中的数据
这时候Redis 的数据是错误的,会导致后面查询的时候全部查询到错误的数据(只能重新加载 MySQL 数据到 Redis 才能恢复)
概率低
影响有限
单独开一个服务监听数据库 binlog 日志更新缓存可以有效避免数据不一致的问题
首先产生不一致的原因是更新缓存的顺序无法保证,因为有的请求执行快,有的请求执行慢,这个无法保证,所以有可能造成覆盖的问题
但是 binlog 是有序的,按照事务提交的顺序进行追加的,所以使用 binlog 更新缓存(Redis)就是按照事务提交的顺序进行更新,就不会出现数据不一致的问题
不行
还是原来的逻辑后到的可能先入队,先到的可能后入队(先到的线程阻塞了一下)
分布式锁解决数据不一致问题,但是一般不会使用,因为破坏了Redis 的高性能
…
清除策略有非常大的缓存击穿问题,可能造成 MySQL 被打爆,这是不能接受的
当未命中缓存就加分布式锁(使用 lua 脚本)避免大量请求打到 MySQL 导致服务崩溃
伪代码:
// 更新数据库
err := db.Update(data).Error()
if err != nil{
return
}
// MySQL更新成功再更新 Redis
redis.Update(key,data)//或者使用删除redis.Delete(key)
伪代码
// 更新数据库
err := db.Update(data).Error()
if err != nil{
return
}
// 异步更新 Redis
go redis.Update(key,data)//或者使用删除redis.Delete(key)
更新接口
// 只做更新数据库操作
err := db.Update(data).Error()
if err != nil{
return
}
Redis 更新服务
for{
//监听 binlog 日志
binlog<-binlogChan
//写 Redis
redis.Update(binlog["key"],binlog["data"])//或者使用删除redis.Delete(binlog["key"])
}
对于可预见性的热数据,并且并发读写高的数据一般使用监听 binlog +更新缓存 Redis 数据的方式
对于不可预见热key 并且更新频率低,容忍延迟刷新数据生效的业务使用异步协程+删除 Redis 缓存数据的方案
重试与告警: 当写 Redis 失效的时候,再保证幂等(做计算类更新一定要保证幂等再重试,使用 lua)的情况下进行重试
击穿预防: 使用分布式锁限制热key 打到 MySQL 的 请求的数量,避免缓存击穿.