前言
对分布式锁不太了解的小伙伴,可以先看一下这篇文章 https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA
Redis分布式锁加锁
最开始的分布式锁是使用setnx+expire命令来实现的。setnx设置成功返回1,表示获取到锁,返回0,表示没有获取到锁,同时为了避免显示释放锁失败,导致资源永远也不释放,获取到锁后还会用expire命令设置锁超时的时间。
但有个问题就是setnx+expire不是原子性的,有可能获取到锁后,还没执行expire命令,也没执行释放锁的操作,服务就挂了,这样这个资源就永远也不会访问到了。
为了解决这个问题,Redis 2.6.12版本以后,为set命令增加了一系列的参数,我们此时用NX和PX参数就可以解决这个问题。
所以现在Redis分布式锁的加锁命令如下
SET resource_name random_value NX PX 30000
NX只会在key不存在的时候给key赋值,PX通知Redis保存这个key 30000ms,当资源被锁定超过这个时间时,锁将自动释放
random_value最好是全局唯一的值,保证释放锁的安全性
127.0.0.1:6379> SET lock1 100 NX PX 30000
OK
127.0.0.1:6379> SET lock1 100 NX PX 30000
(nil)
当某个key不存在时才能设置成功。这就可以让多个并发线程同时去设置同一个key,只有一个能设置成功。而其他线程设置失败,也就是获得锁失败
Redis分布式锁解锁
解锁不能简单的使用如下命令
del resource_name
因为有可能节点A加锁后执行超时,锁被释放了。节点B又重新加锁,A正常执行到del命令的话就把节点B的锁给释放了。所以在解锁之前先判断一下是不是自己加的锁,是自己加的锁再释放,不是就不释放。所以伪代码如下
if (random_value.equals(redisClient.get(resource_name))) {
del(key)
}
因为判断和解锁是2个独立的操作,不具有原子性,还是有可能会出问题。所以解锁的过程要执行如下的Lua脚本 ,通过Lua脚本来保证判断和解锁具有原子性。
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
如果key对应的value一致,则删除这个key,通过这个方式释放锁是为了避免Client释放了其他Client申请的锁
到此你已经彻底理解了该如何实现一个分布式锁了,以及为什么要这样做的原因
加锁执行命令
SET resource_name random_value NX PX 30000
解锁执行脚本
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
一个分布式锁的工具类写法如下
public class LockUtil {
private static final String OK = "OK";
private static final Long LONG_ONE = 1L;
private static final String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
public static boolean lock(String lockKey, String requestId, long expire) {
Jedis jedis = RedisPool.getJedis();
SetParams setParams = new SetParams();
setParams.nx().px(expire);
return OK.equals(jedis.set(lockKey, requestId, setParams));
}
public static boolean unlock(String lockKey, String requestId) {
Jedis jedis = RedisPool.getJedis();
return LONG_ONE.equals(jedis.eval(script, 1, lockKey, requestId));
}
}
如何正确的加解锁?
错误的加解锁逻辑
public void workV1() {
String lockKey = "testKey";
String requestId = UUID.randomUUID().toString();
if (LockUtil.lock(lockKey, requestId, 2000)) {
try {
LockUtil.unlock(lockKey, requestId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这个例子的加解锁都有问题
解锁:当发生异常的时候,解锁逻辑并不会执行,所以需要将其放在finally语句中 加锁:将加锁的命令成功发到服务端并成功执行,但是获取响应超时,就会执行解锁的逻辑。因此需要将加锁的逻辑放在try语句中
正确的加解锁逻辑
public void workV2() {
String lockKey = "testKey";
String requestId = UUID.randomUUID().toString();
try {
if (LockUtil.lock(lockKey, requestId, 2000)) {
}
} catch (Exception e) {
e.printStackTrace();
} finally {
LockUtil.unlock(lockKey, requestId);
}
}
参考博客
[1]https://mp.weixin.qq.com/s/8fdBKAyHZrfHmSajXT_dnA [2]https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/ 红锁 [3]https://mp.weixin.qq.com/s/5E9_CSpnvf_KnDnKO0XjQg [4]https://segmentfault.com/a/1190000039362581
|