前言
当不同的进程,必须以独占资源的方式实现资源共享,就需要用到分布式锁。
安全和稳定性
分布式锁的实现,必须满足以下2个特性
- 独享互斥:在任意一个时刻,只能有一个客户端持有锁
- 无死锁:既然有加锁,则必须存在解锁。即使持有锁的客户端崩溃宕机,锁仍然允许被其他客户端获取,不能造成无限期的等待
例子1
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/lock")
public void lock1() throws InterruptedException {
String lockKey = "lockKey", lockValue = "lockValue";
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (success){
try{
System.out.println(String.format("Thread-%d:Success", Thread.currentThread().getId()));
}
finally {
stringRedisTemplate.delete(lockKey);
}
}
}
- 独享互斥:setIfAbsent等同于Redis的SETNX命令,当key不存在才生效,返回的是true;当key存在时返回false。
- 无死锁:setIfAbsent第3个参数是key超时时间,就算中途应用服务挂了,等时间到了key自动失效。另外,finally处理删除key操作,及时释放锁。
用JMeter发送1000个并发请求,结果如下:
Thread-227:Success
Thread-88:Success
Thread-116:Success
Thread-202:Success
Thread-185:Success
Thread-97:Success
但是,上述Demo存在以下问题:
- 客户端A获取到锁资源,同时设置超时时间10s,紧接着A被其他操作堵塞了进程
- 10s过后,由于A的业务尚未执行完,锁过期自动失效了
- 客户端B成功获取锁资源
- 此时A的业务逻辑执行完毕,做了释放锁操作,此时删除的KEY是客户端B加锁的KEY
- 客户端C尝试获取锁资源,由于KEY已经被A删掉了,所以C也加锁成功,和客户端B存在了并发的问题
例子2
public void lock3() throws InterruptedException {
String lockKey = "lockKey";
//lock value改成唯一性的UUID
String lockValue = UUID.randomUUID().toString();
Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10));
if (success){
try{
System.out.println(String.format("Thread-%d:Success", Thread.currentThread().getId()));
}
finally {
//释放锁不再是简单的DELETE KEY
releaseLock(lockKey, lockValue);
}
}
}
private boolean releaseLock(String key, String value){
String srcValue = stringRedisTemplate.opsForValue().get(key);
if (StrUtil.isEmpty(srcValue)){
return true;
}
else if (srcValue.equals(value)){
stringRedisTemplate.delete(key);
return true;
}
return false;
}
上面增加了releaseLock函数:
- 先从Redis取出KEY
- 如果KEY不存在,说明已经过期了,这里直接return true,当做释放锁资源成功
- 如果KEY存在,把VALUE和加锁时用的UUID做比较,相等就说明这是自己占用的锁资源,直接DELETE;如果不相等,就不要做DELETE操作,说明这是其他客户端加的锁
扩展锁
如果你的业务逻辑可以拆解成多个小步骤,可以将锁的有效时间设置短一些。在业务处理的过程中,当发现KEY的有效期很短时,再次延长其有效期(前提还是key存在并且value是之前设置的value)