闲聊
缓存通用查询3部曲
- redis 中数据,返回redis 中的数据
- redis 中没有,查询数据库并返回
- 完成第二部的同时,将数据库查询结果写到redis,redis和数据库数据一致.
谈谈双写一致性的理解
1.如果redis 中有数据:需要和数据库中的相同
2.如果redis 中无数据: 数据库中的值如果是最新的,则要写入到redis
redis 缓存种类
- 只读(通过命令的方式写入,不是由我们的java程序,不常用)
- 读写(常用)
redis 读写缓存的策略
- 同步直写策略
写数据后,也同步写到redis,缓存和数据库中的数据一致。
对于读写缓存来说,要想保证缓存和数据库中的数据一致,就要采用同步直写策略 - 异步缓存策略
正常业务运行中,一定时间后才将数据写到redis(比如下单后的快递信息)
异常情况出现后,需要将失败的动作进行修补,有可能需要借助mq等消息队列。
问题
1. 如果使用redis,那就回涉及到redis 缓存与数据库双写问题。是先动redis呢,还是先动mysql?
首先我们的目的是明确的:就是保证redis和mysql的一致性。
给缓存设置过期时间,定期清理缓存并会写,是最终保证一致性的解决方案。
我们可以对缓存数据设置过期时间,所有的写以数据库为准,对缓存操作尽最大
努力即可。也就是说数据库写入成功,而缓存写入失败,那么只要达到过期时间,
则后面的请求会从数据库中读取新值回写缓存。从而达到最终一致性。
在了解一下4中更新策略
-
先更新数据库,在更新缓存
如果更新mqsql 成功,redis 失败,则数据不一致
缓存中使错误的数据,数据不一致,且高并发下问题更是严重
-
线程缓存,在更新数据库
如果redis 成功,mqsql失败,则数据不一致
3. 先删除缓存,在更新数据库
如果redis 删除成功, 数据库删除失败,则数据不一致
下面有问题的代码
A线程执行下面的代码,删除缓存后,正在更新数据库,
线程B要获取这个缓存 ,没有查到,线程B就把数据回写到缓存,
然后线程A更新完数据库后,发现缓存还在
导致缓存中一直都是错的数据。
处理办法1 延迟双删除
这个方案在第一次删除之后,延迟一段时间再次进行删除,所以我们把它叫做“延迟双栓”
- 先更新数据库,在删除缓存 (可用,也有问题,但是比上面的3种更好)
如果mqsql成功,redis 失败,只要是mysql 是正确的,下次通过回写到reids,保证一致性
ali的cannel也是这个思想, myssql有个中间件canel,可以完成biglog日志订阅功能。 先更新数据库,在更新缓存。
如果上述的问题也不能容忍(在通过在get一次,以取得正确的数据),那我们只能借助mq与mysql的binlog日志了
![在这里插入图片描述](https://img-blog.csdnimg.cn/355cb7ab3eab4bc6a9122eea84ab8d82.png)
1.我们可以监听 binlog 日志, 但我们知道那些key 和那些表的关系的时。我们便可以筛选出我们要监听的sql语句了。
2.当监听到 执行删除缓存关联的sql语句时,便把这个key放到消息队列。
3.通过mq 中消息执行删除操作。删除成功则已出,删除失败重试。如果重试次数较多,加入死信队列,后续处理,排查故障。
但是这样做有一定的延迟性。比如充值话费,高峰期;1分钟后到账。
2.了解延迟双删吗
延迟双删,
是指在第一次删除之后,延迟一段时间再次进行删除,所以我们把它叫做“延迟双栓”。 目的是 在第一次删除和第二次删除之前,防止有别的线程将值回写到reids.
线程A sleep的时间,需要大于线程B读取数据在写入缓存的时间。
这个时间该怎么确定呢?
第一种方法:
在业务运行的时候统计下线程读取数据与写回数据的时间 T1。结合自己业务,只要保证双删之间的时间 大于T1百毫秒即可。
第二种方法:
新启动后台监控程序,后面将会提到 watchDog监控程序。
这种同步删除降低吞吐量怎么办?
因为要删除2次,所以吞吐量会降低,另起一个线程,异步删除。
3. 微服务查询redis无,mysql有。为了保证双写一致性,会写redis 时需要注意什么?,了解双检加锁吗,如何避免缓存击穿?
一般情况下,使用上面的缓存三部曲即可,但如果是高并发就不行了。想想看,
统一时刻有1000个请求过来,代码都执行 判断redis中有无数据,也就是要
1000次,接着因为redis中没有,又是1000次访问数据库,再接着又是1000次
的会写redis. 这个在高并发下,明显是不可取的。
所有出现 双检加锁。
那什么是双检加锁呢?
public String doubleCheckCache() {
// 先从redis 中查询
String key = "user:1000";
String s = stringRedisTemplate.opsForValue().get(key);
if (!StringUtils.hasLength(s)) {
// 加锁,防止qps高时,同时查到redis中无数据的情况下,继续访问数据库。防止缓存击穿。
synchronized (this) {
// 再次检查redis,第一个进入同步的线程,已经将数据写回了redis
s = stringRedisTemplate.opsForValue().get(key);
if (!StringUtils.hasLength(s)) {
//模拟查询数据库
s = "我是小明";
// 会写redis
stringRedisTemplate.opsForValue().setIfAbsent(key,s);
// 这里要考虑数据库如果为空的时候,要怀疑这ke能时恶意的,续作我们将其加入黑明单。
}
}
}
return s;
}
5. redis 和mysql 100%会出纰漏,做不到强一致性,如何保证最终一致性?
消息队列,有延迟,
延迟双删除,线程A 时间不好估算
等待删除后,执行查询命令,不支持高并发