Redis 的雪崩、穿透和击穿

Redis 雪崩

  雪崩就是指缓存中大批量热点数据过期后系统涌入大量查询请求,因为大部分数据在Redis层已经失效,请求渗透到数据库层,大批量请求犹如洪水一般涌入,引起数据库压力造成查询堵塞甚至宕机。

解决办法:

  1. 将缓存失效时间分散开,比如每个key的过期时间是随机,防止同一时间大量数据过期现象发生,这样不会出现同一时间全部请求都落在数据库层,如果缓存数据库是分布式部署,将热点数据均匀分布在不同Redis和数据库中,有效分担压力,别一个人扛。
  2. 简单粗暴,让Redis数据永不过期(如果业务准许,比如不用更新的名单类)。当然,如果业务数据准许的情况下可以,比如中奖名单用户,每期用户开奖后,名单不可能会变了,无需更新。

缓存穿透

黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。

缓存击穿

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

解决方式也很简单,

  • 可以将热点数据设置为永远不过期;
  • 基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;


@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory redisConnectionFactory){
//创建RedisTemplate对象
RedisTemplate<String, String> template = new RedisTemplate<>();
//设置连接工厂
template.setConnectionFactory(redisConnectionFactory);
//创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
//设置KEY的序列化
template.setKeySerializer(RedisSerializer.string());
template.setHashKeySerializer(RedisSerializer.string());
//设置VALUE的序列化,不用jsonRedisSerializer
// template.setValueSerializer(jsonRedisSerializer);
// template.setHashValueSerializer(jsonRedisSerializer);
template.setValueSerializer(RedisSerializer.string());
template.setHashValueSerializer(RedisSerializer.string());
//返回
return template;
}
}

添加锁方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 获取互斥锁
* @return
*/
private Boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtils.isTrue(flag);
}

/**
* 释放锁
* @param key
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}

获取互斥锁和释放锁的传参都应传城市redis互斥锁key

然后编写通过互斥锁机制查询城市信息的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 通过互斥锁机制查询城市信息
* @param key
*/
private City queryCityWithMutex(String key, String cityCode) {

City city = null;
// 1.查询缓存
String cityJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断缓存是否有数据
if (StringUtils.isNotBlank(cityJson)) {
// 3.有,则返回
city = JSONObject.parseObject(cityJson, City.class);
return city;
}
// 4.无,则获取互斥锁
String lockKey = RedisConstants.LOCK_CITY_KEY + cityCode;
Boolean isLock = tryLock(lockKey);
// 5.判断获取锁是否成功
try {
if (!isLock) {
// 6.获取失败, 休眠并重试
Thread.sleep(100);
return queryCityWithMutex(key, cityCode);
}
// 7.获取成功, 查询数据库
city = baseMapper.getByCode(cityCode);
// 8.判断数据库是否有数据
if (city == null) {
// 9.无,则将空数据写入redis
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 10.有,则将数据写入redis
stringRedisTemplate.opsForValue()
.set(key, JSONObject.toJSONString(city), RedisConstants.CACHE_CITY_TTL, TimeUnit.MINUTES);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 11.释放锁
unLock(lockKey);
}
// 12.返回数据
return city;
}

其中常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* redis常量
* @author wl
* @date 2022/3/17 16:09
*/
public interface RedisConstants {
/**
* 空值缓存过期时间(分钟)
*/
Long CACHE_NULL_TTL = 2L;

/**
* 城市redis缓存key
*/
String CACHE_CITY_KEY = "cache:city:";
/**
* 城市redis缓存过期时间(分钟)
*/
Long CACHE_CITY_TTL = 30L;

/**
* 城市redis互斥锁key
*/
String LOCK_CITY_KEY = "lock:city:";
}