什么是分布式锁已经在之前的文章介绍过了,不明白的同学请回头阅读。这篇文章开始介绍分布式锁的具体实现。实现方式有很多,基于 redis 第一可以使用 redis 的 setnx 命令去手动封装,第二种方式可以通过开源的 redisson。
今天介绍最基本的通过 setnx命令来实现。首先了解下 setnx 命令:
SETNXset if not exist,如果 key 不存在,设置指定的值并返回 1,否则不做任何操作返回 0,整个过程是原子操作。
通过该命令来实现锁时需要考虑多线程并发带来的异常情况,比如上一次程序中断导致锁没有释放、死锁问题。
思路锁未释放问题,可以通过过期时间来解决,执行 setnx 命令时将 key 设置为 timestamp:
SETNX lock.foo
获取锁失败时,通过 GET 命令获取 timestamp 如果小于等于当前时间,则释放锁。这里注意,不能 仅仅调用 DEL 命令释放,然后再调用 SETNX 获取,因为 DEL 和 SETNX 是两个命令,非原子操作,会存在多线程并发问题,比如:
假设客户端 c1 获取了锁后,执行了一系列业务操作后没来得及释放锁就宕机了,c2、c3 接下来获取锁:
c2 执行 SETNX 失败,开始调用 GET 发现已经过期,刚好 c3 也执行到此,c2、c3 接下来开始分别执行 DEL 和 SETNX 命令,最后同时都成功获取到了锁!这个问题的解决也很简单,就是使用一个 GETSET 命令 替换 DEL 和 SETNX 两个命令,从而保证无并发问题。
代码实现基于以上讨论,获取锁的代码整理如下:
public boolean lock(String lockKey,String timeStamp,long time){
// setnx 成功
if(stringRedisTemplate.opsForValue().setIfAbsent(lockKey, timeStamp)){
// 给锁加个过期时间,如果被执行了,锁到期后自动释放
stringRedisTemplate.expire (lockKey,time,TimeUnit.MILLISECONDS);
return true;
}
//setnx 失败,获取锁失败
// 判断锁超时
String currentLock = (String) stringRedisTemplate.opsForValue().get(lockKey);
// 如果锁过期
if(!StringUtils.isEmpty(currentLock) && Long.parseLong(currentLock) < System.currentTimeMillis()){
// GETSET 上新锁,并返回旧的锁
String preLock = (String) stringRedisTemplate.opsForValue().getAndSet(lockKey, timeStamp);
// 二次检查,如果刚好两个线程都执行了 GETSET , 只有 GETSET 前后是同一把锁的才算获取锁成功,类似于 CAS 的思想!!!
if(!StringUtils.isEmpty(preLock) && preLock.equals(currentLock)){
stringRedisTemplate.expire (lockKey,time,TimeUnit.MILLISECONDS);
return true;
}
}
return false;
}
释放锁的时候,也要检查时间戳:
public boolean release(String lockKey,String timeStamp){
try {
String currentValue = (String) stringRedisTemplate.opsForValue().get(lockKey);
if(!StringUtils.isEmpty(currentValue) && currentValue.equals(timeStamp) ){
// 删除锁状态
return stringRedisTemplate.opsForValue().getOperations().delete(lockKey);
}
} catch (Exception e) {
log.error ( "锁释放异常,{}",e );
return false;
}
}
需要注意的事,锁的时间应该设置足够长,因为业务代码可能会比较耗时,尤其是涉及到了数据库或者 IO 操作时。
如果觉得还不错的话,关注、分享、在看, 原创不易,且看且珍惜~



