在网易的时候,用到了一种用Redis实现的分布式锁。
tryLock(){
SET Key 1 EX Seconds NX
}
release(){
DELETE Key
}
但是这种实现并不严谨,有可能多个客户端同时持有一把锁。
- C1成功获取到了锁,之后C1因为GC进入等待或者未知原因导致任务执行过长,最后在锁失效前C1没有主动释放锁。
- C2在C1的锁超时后获取到锁,并且开始执行,这个时候C1和C2都同时在执行,会因重复执行造成数据不一致等未知情况。
-
C1如果先执行完毕,则会释放C2的锁,此时可能导致另外一个C3进程获取到了锁。
另一个版本是:
tryLock(){
SET Key UniqId Seconds
}
release(){
EVAL(
//LuaScript
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
)
}
UniqId是自增id,优于时间戳(可能重复)。
这个版本避免了C1释放C2锁的情况,但依然有如下问题:
- 没有设置超时时间,那么持有锁的客户端crash,不能释放锁。
- Redis集群的数据同步为异步,假设master节点获取到了锁后,未完成数据同步的情况下crash,而此时其他客户端在新的master节点依然可以获得锁。
在Redis分布式锁的实现上还有很多问题等待解决,及时是Redlock也遭到了强烈质疑。
2019-04-17补充:
上面没有给出一个正确的完整版本。
- 获取锁(unique_value可以是UUID, requestId,自增id等)
SET resource_name unique_value NX PX 30000
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "PX";
/**
* 尝试获取分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expireTime 超期时间
* @return 是否获取成功
*/
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);
return LOCK_SUCCESS.equals(result);
}
}
- 释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
public class RedisTool {
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
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));
return RELEASE_SUCCESS.equals(result);
}
}
关于Lua,它是一个封装了多个命令的脚本,里面的参数需要传递。如果调用这个脚本,要使用eval命令。
事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
- 在Redis的master节点上拿到了锁;
- 但是这个加锁的key还没有同步到slave节点;
- master故障,发生故障转移,slave节点升级为master节点;
- 其他客户端,也可以获得锁。
网友评论