Redis做分布式锁的问题

参考

谷粒商城p158:https://www.bilibili.com/video/BV1np4y1C7Yf?p=158

redis官网:http://www.redis.cn/commands/set.html

提要

分布式场景下将原有的本地锁换为,基于redis的setnx命令的分布式锁

  • getCatalogJsonFromDb:从数据库查数据
  • getCatalogJsonFromDbWithLocalLock:利用本地锁查数据
  • getCatalogJsonFromDbWithRedisLock:利用redis的setnx命令的分布式锁查数据,现需要完成的

分布式锁演进-阶段一

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

// 1、占分布式锁,setnx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
// 加锁成功。。。执行任务
Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}

}

问题

  • setnx占好了位置,业务代码异常或程序宕机,没有执行删锁逻辑,死锁!!!

解决

  • 设置锁自动过期,即使没有删除,到期也会消失

分布式锁演进-阶段二

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

// 1、占分布式锁,setnx
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){
// 加锁成功。。。执行任务

// 2、设置过期时间
redisTemplate.expire("lock", 30, TimeUnit.SECONDS);

Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}

}

问题

  • 加锁与设置过期时间非原子操作,所以仍会出现死锁问题

解决

  • 利用redis提供的setexnx,做原子操作

分布式锁演进-阶段三

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

// 1、占分布式锁,setnx
// 同时设置时间
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if(lock){
// 加锁成功。。。执行任务

// 2、设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);

Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
redisTemplate.delete("lock");
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}

}

问题

  • 如果业务时间过长,我们的锁过期自动删除,这时直接删锁,有可能把别人正在持有的锁删除了

解决

  • 占锁时指定uuid保证唯一性,删锁需要验证是否是自己的锁

分布式锁演进-阶段四

代码

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
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

// 1、占分布式锁,setnx
// 同时设置时间
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if(lock){
// 加锁成功。。。执行任务

// 2、设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);

Map<String, List<Catalog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
// redisTemplate.delete("lock");
String lockValue = redisTemplate.opsForValue().get("lock");
// 验证是否是自己的锁
if(uuid.equals(lockValue)){
// 是,删锁
redisTemplate.delete("lock");
}
return catalogJsonFromDb;
}else{
// 加锁失败。。。重试
// 休眠100ms重试
// 自旋方式
return getCatalogJsonFromDbWithRedisLock();
}

}

问题

  • 注意在redisget锁的值时,即String lockValue = redisTemplate.opsForValue().get("lock");,这时可能redis还存在我们的锁,这时返回的正是我们uuid,但是因为网络传输的延时,我们要执行delete操作时,我们的锁已经因为过期策略删除了,所以虽然这是的锁不是我们的,但程序代码仍然会执行删除锁(并非我们的),本质仍是非原子操作问题

解决

  • lua脚本,实现原子操作

分布式锁演进-阶段五

代码

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
public Map<String, List<Catalog2Vo>> getCatalogJsonFromDbWithRedisLock() {

// 1、占分布式锁,setnx
// 同时设置时间
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
Map<String, List<Catalog2Vo>> catalogJsonFromDb = null;
if (lock) {
// 加锁成功

// 2、设置过期时间
// redisTemplate.expire("lock", 30, TimeUnit.SECONDS);

try {
catalogJsonFromDb = getCatalogJsonFromDb();
} finally {
// 获取值对比+对比成功删除=原子操作
// String lockValue = redisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// redisTemplate.delete("lock");
// }

// 脚本解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Long unlock = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return catalogJsonFromDb;

} else {
// 加锁失败。。。重试
// 休眠100ms重试
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getCatalogJsonFromDbWithRedisLock();
}

}

总结

使用redis做分布式锁

  • 加锁和设置过期时间的原子性问题
  • 解锁与验证锁的归属的原子性问题
  • 还有业务时间与过期时间的设置,有时需要延长过期时间

可以考虑使用Redisson