上次我们介绍了SETNX + GETSET的方式实现分布式锁,这是老版本Redis中最常用的实现分布式锁的方法,但该方法存在以下问题:
对于锁拥有者标识的问题,我们首先想到可以使用ID来标识线程,在传入参数中携带ID。
首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。但是,上述操作无法保证原子性!也就是无法做到线程安全的特性。
那么,如何传入线程ID同时又保证操作的原子性呢?答案是使用Lua脚本对传入参数进行处理。
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。
对于上述锁强制要求分布式下每个客户端的时间必须同步的问题,Redis在其2.6.12版本中更新了SET命令,新版的SET命令可以传入多种参数,其中就包括锁过期时间!
SET key value [EX seconds] [PX milliseconds] [NX|XX]
参数解释:
EX seconds – 设置键 key 的过期时间,单位时秒
PX milliseconds – 设置键 key 的过期时间,单位时毫秒
NX – 只有键 key 不存在的时候才会设置 key 的值
XX – 只有键 key 存在的时候才会设置 key 的值
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
这样,在新版本的Redis中,使用SET + LUA脚本的方式,就能完美解决分布式锁问题了!
后来我还了解到Redisson这个开源项目。如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件。
使用:直接操作Lock接口就可以了,没有学习成本!!
RLock lock = redisson.getLock("anyLock");
// 加锁以后10秒钟自动解锁
// 无需调用unlock方法手动解锁
lock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口
用他提供的锁接口即可实现分布式锁。Redisson采用了基于NIO的Netty框架,对Redis的常用功能进行了Java封装,实现各类Java常用的数据结构set,list,queue,map、多线程组件(各种锁),消息队列等。