Redisson实现分散式锁(2)—RedissonLock

有关Redisson实现分散式锁上一篇文章讲了分散式的锁原理:Redisson实现分散式锁---原理
这篇主要讲RedissonLock和RLock。Redisson分散式锁的实现是基于RLock界面,RedissonLock实现RLock界面。
一、RLock界面
1、概念
public interface RLock extends Lock, RExpirable, RLockAsync
很明显RLock是继承Lock锁,所以他有Lock锁的所有特性,比如lock、unlock、trylock等特性,同时它还有很多新特性:强制锁释放,带有效期的锁,。
2、RLock锁API
这里针对上面做个整理,这里列举几个常用的界面说明
public interface RRLock {
//----------------------Lock界面方法-----------------------
/**
* 加锁 锁的有效期预设30秒
*/
void lock();
/**
* tryLock()方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他执行绪获取),则返回false .
*/
boolean tryLock();
/**
* tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,
* 在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
*
* @param time 等待时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
/**
* 解锁
*/
void unlock();
/**
* 中断锁 表示该锁可以被中断 假如A和B同时调这个方法,A获取锁,B为获取锁,那么B执行绪可以通过
* Thread.currentThread().interrupt(); 方法真正中断该执行绪
*/
void lockInterruptibly();
//----------------------RLock界面方法-----------------------
/**
* 加锁 上面是预设30秒这里可以手动设定锁的有效时间
*
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lock(long leaseTime, TimeUnit unit);
/**
* 这里比上面多一个引数,多新增一个锁的有效时间
*
* @param waitTime 等待时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
/**
* 检验该锁是否被执行绪使用,如果被使用返回True
*/
boolean isLocked();
/**
* 检查当前执行绪是否获得此锁(这个和上面的区别就是该方法可以判断是否当前执行绪获得此锁,而不是此锁是否被执行绪占有)
* 这个比上面那个实用
*/
boolean isHeldByCurrentThread();
/**
* 中断锁 和上面中断锁差不多,只是这里如果获得锁成功,新增锁的有效时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lockInterruptibly(long leaseTime, TimeUnit unit);
}
RLock相关界面,主要是新添加了 leaseTime 属性字段,主要是用来设定锁的过期时间,避免死锁。
二、RedissonLock实现类
public class RedissonLock extends RedissonExpirable implements RLock
RedissonLock实现了RLock界面,所以实现了界面的具体方法。这里我列举几个方法说明下
1、void lock()方法
@Override
public void lock() {
try {
lockInterruptibly();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
发现lock锁里面进去其实用的是lockInterruptibly(中断锁,表示可以被中断),而且捕获异常后用 Thread.currentThread().interrupt()来真正中断当前执行绪,其实它们是搭配一起使用的。
具体有关lockInterruptibly()方法讲解推荐一个部落格。部落格:Lock的lockInterruptibly()
接下来执行流程,这里理下关键几步
/**
* 1、带上预设值调另一个中断锁方法
*/
@Override
public void lockInterruptibly() throws InterruptedException {
lockInterruptibly(-1, null);
}
/**
* 2、另一个中断锁的方法
*/
void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException
/**
* 3、这里已经设定了锁的有效时间预设为30秒 (commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()=30)
*/
RFuture ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
/**
* 4、最后通过lua指令码访问Redis,保证操作的原子性
*/
RFuture tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand command) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
那么void lock(long leaseTime, TimeUnit unit)方法其实和上面很相似了,就是从上面第二步开始的。
2、tryLock(long waitTime, long leaseTime, TimeUnit unit)
界面的引数和含义上面已经说过了,现在我们开看下源代码,这里只显示一些重要逻辑。
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
//1、 获取锁同时获取成功的情况下,和lock(...)方法是一样的 直接返回True,获取锁False再往下走
if (ttl == null) {
return true;
}
//2、如果超过了尝试获取锁的等待时间,当然返回false 了。
time -= System.currentTimeMillis() - current;
if (time acquireFailed(threadId);
return false;
}
// 3、订阅监听redis讯息,并且建立RedissonLockEntry,其中RedissonLockEntry中比较关键的是一个 Semaphore属性物件,用来控制本地的锁请求的讯号量同步,返回的是netty框架的Future实现。
final RFuture subscribeFuture = subscribe(threadId);
// 阻塞等待subscribe的future的结果物件,如果subscribe方法呼叫超过了time,说明已经超过了客户端设定的最大wait time,则直接返回false,取消订阅,不再继续申请锁了。
// 只有await返回true,才进入循环尝试获取锁
if (!await(subscribeFuture, time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.addListener(new FutureListener() {
@Override
public void operationComplete(Future future) throws Exception {
if (subscribeFuture.isSuccess()) {
unsubscribe(subscribeFuture, threadId);
}
}
});
}
acquireFailed(threadId);
return false;
}
//4、如果没有超过尝试获取锁的等待时间,那么通过While一直获取锁。最终只会有两种结果
//1)、在等待时间内获取锁成功 返回true。2)等待时间结束了还没有获取到锁那么返回false。
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// 获取锁成功
if (ttl == null) {
return true;
}
// 获取锁失败
time -= System.currentTimeMillis() - currentTime;
if (time acquireFailed(threadId);
return false;
}
}
}
重点 tryLock一般用于特定满足需求的场合,但不建议作为一般需求的分散式锁,一般分散式锁建议用void lock(long leaseTime, TimeUnit unit)。因为从效能上考虑,在高并发情况下后者效率是前者的好几倍
3、unlock()
解锁的逻辑很简单。
@Override
public void unlock() {
// 1.通过 Lua 指令码执行 Redis 命令释放锁
Boolean opStatus = commandExecutor.evalWrite(getName(), LongCodec.INSTANCE,
RedisCommands.EVAL_BOOLEAN,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.asList(getName(), getChannelName()),
LockPubSub.unlockMessage, internalLockLeaseTime,
getLockName(Thread.currentThread().getId()));
// 2.非锁的持有者释放锁时丢掷异常
if (opStatus == null) {
throw new IllegalMonitorStateException(
"attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + Thread.currentThread().getId());
}
// 3.释放锁后取消重新整理锁失效时间的排程任务
if (opStatus) {
cancelExpirationRenewal();
}
}
使用 EVAL 命令执行 Lua 指令码来释放锁:
key 不存在,说明锁已释放,直接执行 publish 命令释出释放锁讯息并返回 1。key 存在,但是 field 在 Hash 中不存在,说明自己不是锁持有者,无权释放锁,返回 nil。因为锁可重入,所以释放锁时不能把所有已获取的锁全都释放掉,一次只能释放一把锁,因此执行 hincrby 对锁的值减一。释放一把锁后,如果还有剩余的锁,则重新整理锁的失效时间并返回 0;如果刚才释放的已经是最后一把锁,则执行 del 命令删除锁的 key,并发布锁释放讯息,返回 1。注意这里有个实际开发过程中,容易出现很容易出现上面第二步异常,非锁的持有者释放锁时丢掷异常。比如下面这种情况
//设定锁1秒过去
redissonLock.lock("redisson", 1);
/**
* 业务逻辑需要咨询2秒
*/
redissonLock.release("redisson");
/**
* 执行绪1 进来获得锁后,执行绪一切正常并没有宕机,但它的业务逻辑需要执行2秒,这就会有个问题,在 执行绪1 执行1秒后,这个锁就自动过期了,
* 那么这个时候 执行绪2 进来了。线上程1去解锁就会抛上面这个异常(因为解锁和当前锁已经不是同一执行绪了)
*/
end:如果你觉得本文对你有帮助的话,记得点赞转发,你的支援就是我更新动力。