redisson-lock源码探析
分布式锁
一般情况下, 实现redis的分布式锁, 本质都是保证在一段时间内,当前线程对资源的独占.(这个一定时间, 是为了容错性)
http://redis.cn/topics/distlock.html
安全和活性失效保障
- 最简单的算法只需具备3个特性就可以实现一个最低保障的分布式锁。
- 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁。
- 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取。
- 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁.
单实例redis分布式锁的姿势
单Redis实例实现分布式锁的正确方法
在尝试克服上述单实例设置的限制之前,让我们先讨论一下在这种简单情况下实现分布式锁的正确做法,实际上这是一种可行的方案,尽管存在竞态,结果仍然是可接受的,另外,这里讨论的单实例加锁方法也是分布式加锁算法的基础。
获取锁使用命令:
SET resource_name my_random_value NX PX 30000
这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个30秒的自动失效时间(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。
value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。可以通过以下Lua脚本实现:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Redisson lock
redissonLock,实现了java的Lock接口, 可以像操作jdk中的lock接口一样操作分布式锁
RLock对象完全符合Java的Lock规范。也就是说只有拥有锁的进程才能解锁,其他进程解锁则会抛出IllegalMonitorStateException错误.(所以解锁时可能会抛出异常)
使用方法
- 最常见的方法, 和操作java的lock一样
RLock lock = redisson.getLock("anyLock");
// 最常见的使用方法
try{
lock.lock();
// 业务代码
}finally{
lock.unlock();
}
- 获取锁需要时间等待, 可以加上等待时间
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();
}
}
- 其他还有很多好玩的用法 , 比如公平锁 , redlock(分布式redis场景),读写锁,multilock等
详见https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8
redisson分布式锁, 原理
核心方法,就是getlock,lock, unlock, 重点关注RedissonLock 这个实现类.redisson的lock一般都是异步的 , 这个和他实现的connector有关,就和lettuce一样 .
加锁过程使用的是hash结构 , hash中的field是当前加锁的connectionid+线程id,支持可重入(reetrantlock).
类似这种
- anyLockname
01291287_业务线程1
01291287_业务线程2
另外使用了publish ssubscribe 模式监听锁释放. 因为只有抢不到锁的时候, 才去监听他什么时候释放, 方便再去抢.
以下按照顺序讲解
RLock lock = redisson.getLock("anyLock");
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
这个参数"anyLock" 就是作为后续hash结构的key.
internalLockLeaseTime 锁的释放时间(就是key的有效期)
pubSub 用于监听
lock.lock()--重头戏
执行流程
- 当前线程先去获取锁
- 获取锁成功, 直接返回
- 获取锁失败 进行如下处理
- 订阅当前key,并阻塞, 直到锁被释放
- while(true)循环, 再尝试获取锁, 如果获取成功, 跳出循环直接返回,。
- 如果获取失败, 那么继续阻塞, 等待锁释放。并重复上一步
-
- 跳出循环后,取消订阅。
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
//
Long ttl = tryAcquire(-1, leaseTime, unit, threadId); //获取锁. 如果获取不到 , ttl为空
// lock acquired
if (ttl == null) { //表示加锁成功
return;
}
//执行到这, 表示加锁失败
RFuture<RedissonLockEntry> future = subscribe(threadId); //异步订阅当前key, threadId只有公平锁时候才有用
if (interruptibly) { //是否支持中断 下面同步执行订阅(其实是有个默认的订阅时间, 超时就会报错, 防止异常或者太久卡死在这)
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
//到这里, 说明key被释放了 , 可以抢锁了
try {
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId); // 还是调用之前的方法, 抢锁
// lock acquired
if (ttl == null) { // 成功, 那就中断跳出去
break;
}
// waiting for message
if (ttl >= 0) { //被别人抢走了
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else { //<0 //在Redis 2.6和之前版本,如果key不存在或者key存在且无过期时间将返回-1。
// 从 Redis 2.8开始,错误返回值发送了如下变化:
// 如果key不存在返回-2
// 如果key存在且无过期时间返回-1
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally { //没抢到锁 ,就一直在while true里面转圈
unsubscribe(future, threadId); // 取消订阅
}
// get(lockAsync(leaseTime, unit));
}
unlock() 解锁
解锁相对简单很多 , 就是判断当前线程是不是持有锁, 如果持有, 那么就减一(可重入特性), 否则就删除锁
注意, 如果当前线程不持有锁(租期太短), 那么锁会被释放
下面贴一段解锁的代码
/**
* 解锁
* @param threadId
* @return
* null: 当前线程没有锁
* 0: 当前线程还持有重入锁
* 1: 释放完毕
*/
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " + // 如果当前线程没持有锁, 或者锁过期了,返回null
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " + //走到这,确定当前线程持有锁, 对锁减一, (可重入)
"if (counter > 0) then " + // 如果还持有锁, 没释放完
"redis.call('pexpire', KEYS[1], ARGV[2]); " + //续期, 延长锁的时间到internalLockLeaseTime
"return 0; " + //返回0
"else " +
"redis.call('del', KEYS[1]); " + //否则证明锁已经释放完毕, 删除锁
"redis.call('publish', KEYS[2], ARGV[1]); " + //推送消息 , 当前锁已经释放
"return 1; " + //释放成功,返回1
"end; " +
"return nil;",
Arrays.asList(getRawName(), getChannelName()), // KEYS[1] key channel name (redisson_lock__channel+key)
LockPubSub.UNLOCK_MESSAGE, //ARGV[1] 解锁消息 0
internalLockLeaseTime, //ARGV[2] 时间
getLockName(threadId)); // ARGV[3] connectionid+threadid , 对应的是field
}
后记
上面的lock()比较简单,自己也可以实现。上面贴出来的为了讲解清楚, 细节肯定少。 可以看加了注释的源码
更好玩的是加等待时间的 , 类似这种
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS)
玩future比较有意思 。