基于互斥锁方式解决缓存击穿问题

大体思路

image-20250307192018454

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public Result queryById(Long id) {

//互斥锁解决缓存击穿,缓存空对象解决缓存穿透
Shop shop = queryWithMutex(id);

if(shop==null){
return Result.fail("店铺信息不存在");
}

return Result.ok(shop);
}

关键代码

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
76
77
78
79
80
//互斥锁解决缓存击穿问题
public Shop queryWithMutex(Long id){

//1,从redis中查询商铺缓存

String shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);

//2,判断是否存在
//2.1 存在直接返回
//在这里判断查到了直接返回,没查到那不是null就是空字符串
if(StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}

//由于我们设置了空对象是空字符串,所以,不是null就说明命中了我们缓存的空对象,可以直接返回错误信息
//等于null的情况下,我们才会到数据库查数据
if(shopJson!=null){
return null;
}

//2。2 不存在,查询数据库
Shop shop = null;

try {
//缓存重建
//1,1获取互斥锁
//1,2判断是否获取成功
//1,3失败,则休眠并重试

if (!tryLock(RedisConstants.LOCK_SHOP_KEY+id)) {
//休眠50毫秒
Thread.sleep(50);
//递归重试获取锁
return queryWithMutex(id);
}


//1,4成功后

//再次检测redis缓存是否存在

shopJson = stringRedisTemplate.opsForValue().get(RedisConstants.CACHE_SHOP_KEY + id);

//2,判断是否存在
//2.1 存在直接返回
//在这里判断查到了直接返回,没查到那不是null就是空字符串
if(StrUtil.isNotBlank(shopJson)){
return JSONUtil.toBean(shopJson, Shop.class);
}

//由于我们设置了空对象是空字符串,所以,不是null就说明命中了我们缓存的空对象,可以直接返回错误信息
//等于null的情况下,我们才会到数据库查数据
if(shopJson!=null){
return null;
}


//缓存不存在,就根据id查询数据库
shop = getById(id);

//数据库不存在直接返回
if(shop==null){
stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY + id,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return null;
}


//存在,写入缓存并返回

stringRedisTemplate.opsForValue().set(RedisConstants.CACHE_SHOP_KEY+ id,JSONUtil.toJsonStr(shop),RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}finally {
//释放锁

unLock(RedisConstants.LOCK_SHOP_KEY+id);
}

return shop;
}

获取锁的代码:

1
2
3
4
private boolean tryLock(String key){
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}

这段代码,原理是基于Redis中的setnx操作,先设置一个key值,后面再次设置同样的key时,会设置失败,达成一个锁的效果,其中一个线程抢先取设这个值,后面的线程就无法再设了,场景是多个线程请求一个热点数据,所以他们要设的key是相同的,所以是互斥的。

释放锁的代码:

1
2
3
private void unLock(String key){
stringRedisTemplate.delete(key);
}

它的逻辑就是,删除之前设置的key value,让后面的线程,可以去设置这个key