高并发、缓存穿透、缓存击穿、缓存雪崩、互斥锁、逻辑过期
缓存穿透
- 客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,那么所有请求都会直接打到数据库上,增大数据库压力
- 解决:
缓存空对象、布隆过滤、增加 id 复杂度,避免被猜出规律、做好基础数据格式校验、加强用户权限校验、做热点参数限流
缓存击穿(热点 key)
- 一个被高并发访问并且缓存重建业务复杂的 key 突然失效,大量请求瞬间打到数据库
- 解决:
互斥锁、逻辑过期
缓存雪崩
- 同一时段的大量 key 同时失效或 redis 宕机,导致大量请求直接打到数据库
- 解决:
给不同的 key 的 TTL 添加随机值、提高 redis 集群的高可用、缓存业务添加限流策略、添加多级缓存
缓存穿透

解决案例—缓存空对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//缓存穿透
public Shop queryWithPassThrough(Long id){
//1.从redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
//2.判断shopJson是否有数据,有直接返回,没有判空
if (StrUtil.isNotBlank(shopJson)){
//判断某字符串是否不为空且长度不为0且不由空白符构成
//判断参数:是否不为空,长度是否不为0,值是否不包含空白字符
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//3.是否命中缓存的空对象
//shopJson不
|
缓存击穿

解决方法


互斥锁案例

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
|
public Shop queryWithMutex(Long id){
//1.从redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get("cache:shop:"+id);
if (StrUtil.isNotBlank(shopJson)){
//判断某字符串是否不为空且长度不为0且不由空白符构成
//判断参数:是否不为空,长度是否不为0,值是否不包含空白字符
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
//shopJson不为空说明长度为0或者由空白符构成
//没有命中空值
if (shopJson!=null){
return null;
}
//shopJson为空则查数据库加缓存
//缓存重建
//1.获取互斥锁
String lockKey="lock:shop:"+id;
Shop shop = null;
try {
boolean triedLock = tryLock(lockKey);
//2.判断是否获取锁成功
if (!triedLock){
//3.获取锁失败则休眠50ms,醒来重试
Thread.sleep(50);
return queryWithMutex(id);
}
//4.获取锁成功则查询数据库,存在则返回并缓存重建,不存在则缓存空值并返回
shop = getById(id);
//模拟重建延迟
//Thread.sleep(200);
if (shop==null){
//5.防止redis穿透,查询数据库为空,就缓存一个空值,并设置过期时间
stringRedisTemplate.opsForValue().set("cache:shop:","",30L,TimeUnit.MINUTES);
//数据库没有,返回
return null;
}
//6.数据库查询到信息,添加缓存,设置超时剔除
stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(shop),30L, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.释放互斥锁
unLock(lockKey);
}
return shop;
}
//缓存击穿问题,获取互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放互斥锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
|
这里模拟高并发,使用 apifox

逻辑过期案例
逻辑过期并不是设置 TTL,而是为缓存添加一个逻辑上的时间字段,判断该字段是否过期,过期则缓存重建

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
|
//缓存重建,也可以用于缓存预热,提前把热点数据缓存到redis
private void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
//1.查询店铺数据
Shop shop = getById(id);
//模拟缓存重建延迟
Thread.sleep(200);
//2.封装逻辑过期时间
RedisData data = new RedisData();
data.setData(shop);
//设置逻辑过期时间
data.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//3.写入redis
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(data));
}
//线程池,10个线程
private static final ExecutorService CACHE_REBUILD= Executors.newFixedThreadPool(10);
//逻辑过期解决缓存击穿
//热点key一般提前添加到缓存,也就是缓存预热。
//如果没有命中,说明业务上要求查询的该数据不展示,也就不需要再去查数据库了,这点看实际的业务需求
//命中但是过期了就需要缓存重建,此时要去查数据库
public Shop queryWithLogicExpire(Long id){
//1.从redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY+id);
if (StrUtil.isBlank(shopJson)){
//2.没有命中直接返回null
return null;
}
//3.命中redis,把json反序列化成对象
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
JSONObject data = (JSONObject)redisData.getData();
Shop shop = JSONUtil.toBean(data, Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
//4.判断逻辑时间是否过期
if (expireTime.isAfter(LocalDateTime.now())){
//5.未过期,直接返回
return shop;
}
//6.过期,缓存重建,获取互斥锁,判断是否获取锁成功
String key="lock:shop:"+id;
boolean triedLock = tryLock(key);
if (triedLock){
// 7.获取锁成功,使用线程池开启独立线程缓存重建
CACHE_REBUILD.submit(
()->{
try {
this.saveShop2Redis(id,1800L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
unLock(key);
}
}
)
}
//8.获取锁失败,先返回过期的商铺信息
return shop;
}
//获取互斥锁
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放互斥锁
private void unLock(String key){
stringRedisTemplate.delete(key);
}
|
缓存雪崩
