redis分布式锁的实现,本质是使用setnx 【set if not exits】
命令设置key,设置成功则加锁成功,未设置成功,说明已被其他线程设置,加锁失败。setnx
命令需要与 expire
命令一起使用,给锁加一个过期时间,这样锁就可以释放,不会因为服务器宕机而造成死锁。
这里出现以下几个问题:
-
setnx
和expire
两个操作中间的时间,redis服务器挂了,造成死锁 - 如何保证客户端获取锁后,不被其他客户端解锁。
- 如何保证锁的过期时间不被其他客户端修改。
加锁代码
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);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
可以看到,我们加锁就一行代码:jedis.set(String key, String value, String nxxx, String expx, int time)
,这个set()方法一共有五个形参:
- 第一个为key,我们使用key来当锁,因为key是唯一的。
- 第二个为value,我们传的是
requestId
,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value
赋值为requestId
,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId
可以使用UUID.randomUUID().toString()
方法生成。 - 第三个为
nxxx
,这个参数我们填的是NX
,意思是SET IF NOT EXIST
,即当key
不存在时,我们进行set操作;若key
已经存在,则不做任何操作; - 第四个为
expx
,这个参数我们传的是PX
,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。 - 第五个为
time
,与第四个参数相呼应,代表key
的过期时间。
解锁代码
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));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
- 写一个
lua
脚本代码,redis
需要执行的命令 - 使用
jedis.eval()
方法,将lua
脚本命令交给redis
服务端执行
以上代码,只能保证单机的情况下,锁是OK的。如果是分布式,那么就要用到封装好的redisson
框架。
redisson
使用了Lua命令。
锁的内容: hash的名称为锁名,hash里面内容仅包含一条键值对,键为redisson客户端唯一标识+持有锁线程id,值为锁重入计数;给hash设置的过期时间就是锁的过期时间。
发布订阅模式。发布订阅即为,如果AB同时去争抢一把锁,A成功B失败,那么B可以订阅这把锁的信息。当A解锁之后,对锁的信息进行发布,B得到通知后继续去争抢这把锁。
使用了watchDog
看门狗,这个watchDog
实际是一个回调,会每隔十秒去执行一次,如果锁仍存在,那么增加锁的过期时间。
redis红锁
需要特别注意的是,RedissonLock 同样没有解决 节点挂掉的时候,存在丢失锁的风险的问题。而现实情况是有一些场景无法容忍的,所以 Redisson 提供了实现了redlock算法的 RedissonRedLock,RedissonRedLock 真正解决了单点失败的问题,代价是需要额外的为 RedissonRedLock 搭建Redis环境。