1、面试题:
- 1.只要使用缓存,就可能会涉及到redis缓存与数据库双存储双写,只要是双写,就存在数据一致性问题,那么是如何解决数据一致性问题的
- 2.双写一致性,你先动缓存redis还是数据库MySQL,哪一个?why
- 3.延时双删做过吗?会有哪些问题
- 4.有这么一种情况,微服务查询redis无,mysql有,为了保证数据的一致性回写redis需要注意什么?双检加锁你了解吗?如何尽量避免缓存击穿?
- 5.redis和MySQL双写100%会出现披露,做不到强一致性,应该如何保证最终一致性
2、缓存双写一致性的理解:
2.1.理解:
2.2.编码实现:
a.需求介绍:
b.采用双边加锁:
多个线程同时去查询数据库的这条数据
,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。
c.代码:
package com.atguigu.redis.service;
import com.atguigu.redis.entities.User;
import com.atguigu.redis.mapper.UserMapper;
import io.swagger.models.auth.In;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class UserService {
public static final String CACHE_KEY_USER = "user:";
@Resource
private UserMapper userMapper;
@Resource
private RedisTemplate redisTemplate;
/**
* 业务逻辑没有写错,对于小厂中厂(QPS《=1000)可以使用,但是大厂不行
* @param id
* @return
*/
public User findUserById(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql
user = (User) redisTemplate.opsForValue().get(key);
if(user == null)
{
//2 redis里面无,继续查询mysql
user = userMapper.selectByPrimaryKey(id);
if(user == null)
{
//3.1 redis+mysql 都无数据
//你具体细化,防止多次穿透,我们业务规定,记录下导致穿透的这个key回写redis
return user;
}else{
//3.2 mysql有,需要将数据写回redis,保证下一次的缓存命中率
redisTemplate.opsForValue().set(key,user);
}
}
return user;
}
/**
* 加强补充,避免突然key失效了,打爆mysql,做一下预防,尽量不出现击穿的情况。
* @param id
* @return
*/
public User findUserById2(Integer id)
{
User user = null;
String key = CACHE_KEY_USER+id;
//1 先从redis里面查询,如果有直接返回结果,如果没有再去查询mysql,
// 第1次查询redis,加锁前
user = (User) redisTemplate.opsForValue().get(key);
if(user == null) {
//2 大厂用,对于高QPS的优化,进来就先加锁,保证一个请求操作,让外面的redis等待一下,避免击穿mysql
synchronized (UserService.class){
//第2次查询redis,加锁后
user = (User) redisTemplate.opsForValue().get(key);
//3 二次查redis还是null,可以去查mysql了(mysql默认有数据)
if (user == null) {
//4 查询mysql拿数据(mysql默认有数据)
user = userMapper.selectByPrimaryKey(id);
if (user == null) {
return null;
}else{
//5 mysql里面有数据的,需要回写redis,完成数据一致性的同步工作
redisTemplate.opsForValue().setIfAbsent(key,user,7L,TimeUnit.DAYS);
}
}
}
}
return user;
}
}
3、数据库与缓存一致性的更新策略:
无论身操作,我们的目的就是保证最终一致性
- 一般我们都给缓存设置过期时间,定期清理缓存并回写,是保证最终一致性的解决方案
- 2.我们可以对存入缓存的数据设置过期时间,
所有的写操作以数据库为准
,对缓存操作只是尽最大努力即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存,达到一致性,切记要以mysql的数据库写入库为准
3.2.可停机的情况更新策略:
- 1.挂牌报错、凌晨升级、服务降级、温馨提示
- 2.最好单线程操作(
对于重量级的数据操作
)
3.3.不停机情况4种更新策略:
四种更新策略(推荐最后一种,看场景)
a.先更新数据库,在更新缓存
- 1.异常情况1:
- 2.异常情况2:
b.先更新缓存,再更新数据库
- 1.这种方式不太推荐,一般业务会将mysq作为底单数据库,有最终解释权
- 2.异常情况:
c.先删除缓存,再更新数据库
c1.异常问题分析:
上述三个步骤的总结:
c2.解决方案:
1.采用延时双删策略解决上面的异常
2.双删方案面试:
- 1.
这个删除应该休眠多久呢
?
- 2上述同步淘汰策略的改善,防止系统吞吐量降低:
- 3.后续看门狗WatchDog源码分析:
d.先更新数据库,再删除缓存
d1.异常问题:
d2.解决方案:
4.3.总结:
4.3.编码实现:Redis与MySQL数据库案例一致性的
编码实现需要使用到Canal,在Cana篇章再做具体的整理