之前我们说过了缓存击穿,缓存穿透及缓存雪崩的区别 见
redis缓存雪崩,缓存穿透,缓存击穿场景及解决方案.
今天来谈下具体缓存击穿的解决方案
/**
* @author hm
* @date 2021/7/20
*/
@Service
public class RedisSnowSlideServiceImpl implements RedisSnowSlideService {
private static final Logger logger = LoggerFactory.getLogger(RedisSnowSlideServiceImpl.class);
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<Integer, String> redisTemplate;
@Override
public UserInfo getUser(Integer id) {
//1 先从redis里面查数据
String userInfoStr = redisTemplate.opsForValue().get(id);
logger.info("1---【开始】查询数据库--------------");
//2 如果redis中获取数据为空
if (isEmpty(userInfoStr)) {
//从数据库里面查数据
UserInfo userInfo = userMapper.findById(id);
//如果数据库为空直接返回
if (Objects.isNull(userInfo)) {
return null;
}
//不为空就将数据放redis
userInfoStr = JSON.toJSONString(userInfo);
logger.info("2---【结束】查询数据库--------------");
redisTemplate.opsForValue().set(id, userInfoStr);
}
return JSON.parseObject(userInfoStr, UserInfo.class);
}
private boolean isEmpty(String userInfoStr) {
return !StringUtils.hasText(userInfoStr);
}
}
如果,在//1到//2之间耗时1.5秒,那就代表着在这1.5秒时间内所有的查询都会走查询数据库。这也就是我们所说的缓存中的“缓存击穿”。
其实,你们项目如果并发量不是很高,也不用怕,一般这样写也没啥问题,但是一旦在多线程高并发量的情况下,这种写法就会有问题了。那么如何来解决这个问题呢?其实可以通过加锁来解决
下面提供方案二
@Override
public UserInfo getUser(Integer id) {
//1 先从redis里面查数据
String userInfoStr = redisTemplate.opsForValue().get(id);
logger.info("1---【开始】查询数据库--------------");
//2 如果redis中获取数据为空
if (isEmpty(userInfoStr)) {
synchronized (RedisSnowSlideServiceImpl.class){
//从数据库里面查数据
UserInfo userInfo = userMapper.findById(id);
//如果数据库为空直接返回
if (Objects.isNull(userInfo)) {
return null;
}
//不为空就将数据放redis
userInfoStr = JSON.toJSONString(userInfo);
logger.info("2---【结束】查询数据库--------------");
redisTemplate.opsForValue().set(id, userInfoStr);
}
}
return JSON.parseObject(userInfoStr, UserInfo.class);
}
仔细看这种方案确实是对线程争抢资源做了加锁操作,但是其实并没有解决缓存击穿问题,因为多个线程还是会排队获取到锁,然后执行同步代码块操作
所以需要在synchronized代码块中再次查询下redis,所以引入双重检查锁机制
@Override
public UserInfo getUser(Integer id) {
//1 先从redis里面查数据
String userInfoStr = redisTemplate.opsForValue().get(id);
logger.info("1---【开始】查询数据库--------------");
//2 如果redis中获取数据为空
if (isEmpty(userInfoStr)) {
synchronized (RedisSnowSlideServiceImpl.class){
userInfoStr = redisTemplate.opsForValue().get(id);
if (isEmpty(userInfoStr)){
//从数据库里面查数据
UserInfo userInfo = userMapper.findById(id);
//如果数据库为空直接返回
if (Objects.isNull(userInfo)) {
return null;
}
//不为空就将数据放redis
userInfoStr = JSON.toJSONString(userInfo);
logger.info("2---【结束】查询数据库--------------");
redisTemplate.opsForValue().set(id, userInfoStr);
}
}
}
return JSON.parseObject(userInfoStr, UserInfo.class);
}
这种方案其实在正常情况下就没有问题了,但是如果有人恶心攻击,不断的用一个数据库中不存在的数据,比如id为负数或者9999999999这种值,那么这样一来查缓存没有,查数据库也没有,直接返回,再次查缓存,再次查数据库…这样不断循环如果请求量很高也会对数据库造成压力。如何来解决呢?
@Override
public UserInfo getUser(Integer id) {
//1 先从redis里面查数据
String userInfoStr = redisTemplate.opsForValue().get(id);
logger.info("1---【开始】查询数据库--------------");
if (isEmpty(userInfoStr)) {
synchronized (RedisSnowSlideServiceImpl.class){
//查询下缓存
userInfoStr = redisTemplate.opsForValue().get(id);
if (isEmpty(userInfoStr)){
//2 查数据库
UserInfo userInfo = userMapper.findById(id);
if (Objects.isNull(userInfo)) {
//设置空对象
userInfo = new UserInfo();
}
userInfoStr = JSON.toJSONString(userInfo);
logger.info("2---【结束】查询数据库--------------");
redisTemplate.opsForValue().set(id, userInfoStr);
}
}
}
return JSON.parseObject(userInfoStr, UserInfo.class);
}
除了上述的设置空对象以为,其实还有种方案,就是通过布隆过滤器。
布隆过滤器(Bloom Filter):是一种空间效率极高的概率型算法和数据结构,用于判断一个元素是否在集合中(类似Hashset)。它的核心一个很长的二进制向量和一系列hash函数,数组长度以及hash函数的个数都是动态确定的。其内部维护一个全为0的bit数组
布隆过滤器三个使用场景:
网页爬虫对URL的去重,避免爬取相同的URL地址
反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(垃圾短信)
缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。
/**
* @author hm
* @date 2021/7/20
*/
@Service
public class RedisSnowSlideServiceImpl implements RedisSnowSlideService {
private static final Logger logger = LoggerFactory.getLogger(RedisSnowSlideServiceImpl.class);
@Autowired
private UserMapper userMapper;
@Autowired
private RedisTemplate<Integer, String> redisTemplate;
private static Integer size = 1000000000;
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), size);
/**
* 布隆过滤器
* @param id
* @return
*/
@Override
public UserInfo getUser(Integer id) {
//1 先从redis里面查数据
String userInfoStr = redisTemplate.opsForValue().get(id);
logger.info("1---【开始】查询数据库--------------");
if (isEmpty(userInfoStr)) {
//校验是否在布隆过滤器中
if (bloomFilter.mightContain(id)){
return null;
}
synchronized (RedisSnowSlideServiceImpl.class){
//查询下缓存
userInfoStr = redisTemplate.opsForValue().get(id);
if (isEmpty(userInfoStr)){
if (bloomFilter.mightContain(id)){
return null;
}
//2 查数据库
UserInfo userInfo = userMapper.findById(id);
if (Objects.isNull(userInfo)) {
//将id对应的空值放入布隆过滤器
bloomFilter.put(id);
}
userInfoStr = JSON.toJSONString(userInfo);
logger.info("2---【结束】查询数据库--------------");
redisTemplate.opsForValue().set(id, userInfoStr);
}
}
}
return JSON.parseObject(userInfoStr, UserInfo.class);
}
/**
* 分布式锁
* @param id
* @return
*/
@Override
public UserInfo getUser(Integer id) {
//1 先从redis里面查数据
String userInfoStr = redisTemplate.opsForValue().get(id);
logger.info("1---【开始】查询数据库--------------");
if (isEmpty(userInfoStr)) {
//缓存未命中 查询数据库
String lockKey = "lock" + id;
RLock lock = redisson.getLock(lockKey);
lock.lock();
if (lock.isLocked()) {
//2 查数据库
try {
UserInfo userInfo = userMapper.findById(id);
if (Objects.isNull(userInfo)){
return null;
}
userInfoStr = JSON.toJSONString(userInfo);
redisTemplate.opsForValue().set(id, userInfoStr);
} finally {
lock.unlock();
}
}else{
//查询下缓存
userInfoStr = redisTemplate.opsForValue().get(id);
if (isEmpty(userInfoStr)){
return null;
}
}
}
return JSON.parseObject(userInfoStr, UserInfo.class);
}