前言

当不同的进程,必须以独占资源的方式实现资源共享,就需要用到分布式锁。

安全和稳定性

分布式锁的实现,必须满足以下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存在以下问题:

  1. 客户端A获取到锁资源,同时设置超时时间10s,紧接着A被其他操作堵塞了进程
  2. 10s过后,由于A的业务尚未执行完,锁过期自动失效了
  3. 客户端B成功获取锁资源
  4. 此时A的业务逻辑执行完毕,做了释放锁操作,此时删除的KEY是客户端B加锁的KEY
  5. 客户端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函数:

  1. 先从Redis取出KEY
  2. 如果KEY不存在,说明已经过期了,这里直接return true,当做释放锁资源成功
  3. 如果KEY存在,把VALUE和加锁时用的UUID做比较,相等就说明这是自己占用的锁资源,直接DELETE;如果不相等,就不要做DELETE操作,说明这是其他客户端加的锁

扩展锁

如果你的业务逻辑可以拆解成多个小步骤,可以将锁的有效时间设置短一些。在业务处理的过程中,当发现KEY的有效期很短时,再次延长其有效期(前提还是key存在并且value是之前设置的value)

posted on 2020-09-16 14:18  风停了,雨来了  阅读(1109)  评论(0编辑  收藏  举报