Redisson 介绍
对于分布式或者多节点应用,一个分布式锁对于多并发场景显得尤为重要。一般分布式锁要支持和满足以下特性:
1、互斥:同一时刻只能有一个线程获得锁。
2、防止死锁:分布式锁非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
3、性能:需要考虑减少锁等待的时间,避免导致大量线程阻塞。在锁的设计时,需要考虑两点。1、锁的颗粒度要尽量小 2、锁的范围尽量要小
4、重入:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用
Redisson 作为 java 的 Redis 客户端之一,是 Redis 官网推荐的 java 语言实现分布式锁的项目。 Redisson 就是提供了一堆锁... 也是目前大部分公司使用 Redis 分布式锁最常用的一种方式。
可重入锁(Reentrant Lock)/公平锁(Fair Lock)/联锁(MultiLock)/红锁(RedLock)/读写锁(ReadWriteLock)/信号量(Semaphore) 等等
项目结构
Redisson有两个主要的接口类,RedissonClient和RLock。其中RedissonClient定义了客户端的相关方法,如创建锁。RLock则定义了锁的具体操作,若加锁,释放锁等。其中Redisson是RedissonClient的实现类,RedissonLock是RLock的实现类。当然这里只展示了几个重要的的接口和实现类。
RLock
RLock则定义了锁的具体操作,若加锁,释放锁等,详情如下
public interface RLock extends Lock, RExpirable, RLockAsync {
/**
* 中断锁 和上面中断锁差不多,只是这里如果获得锁成功,添加锁的有效时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lockInterruptibly(long var1, TimeUnit var3) throws InterruptedException;
/**
* 这里比上面多一个参数,多添加一个锁的有效时间
* 尝试使用定义的等待时间获取锁。如有必要会等待定义的最长等待时间,直到锁可用。
* 锁定将在定义的等待时间间隔后自动释放。会出 InterruptedException 异常
* @param waitTime 等待时间
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
boolean tryLock(long var1, long var3, TimeUnit var5) throws InterruptedException;
/**
* 加锁 上面是默认30秒这里可以手动设置锁的有效时间
*
* @param leaseTime 锁有效时间
* @param unit 时间单位 小时、分、秒、毫秒等
*/
void lock(long var1, TimeUnit var3);
/**
* 检验该锁是否被线程使用,如果被使用返回True
*/
boolean isLocked();
/**
* 检查当前线程是否获得此锁(这个和上面的区别就是该方法可以判断是否当前线程获得此锁,而不是此锁是否被线程占有)
* 这个比上面那个实用
*/
boolean isHeldByCurrentThread();
/**
* 如果锁存在并且现在已解锁,则返回 true,否则返回 false
*/
boolean forceUnlock();
/**
* 获取当前线程持有此锁的次数,如果当前线程未持有此锁,则为 0
*/
int getHoldCount();
}
RedissonClient
RedissonClient定义了客户端的相关方法,如创建锁
public class RedissonLock extends RedissonExpirable implements RLock {
public RLock getLock(String name) {
return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
}
public RLock getFairLock(String name) {
return new RedissonFairLock(this.connectionManager.getCommandExecutor(), name);
}
public RReadWriteLock getReadWriteLock(String name) {
return new RedissonReadWriteLock(this.connectionManager.getCommandExecutor(), name);
}
}
加锁过程
加锁和解锁过程都是RedissonLock为例。
lock方法调用了lockInterruptibly方法,该方法翻译过来为可中断锁,若没有获取锁,则会进行阻塞等待锁。
该方法最重要的一步是调用tryAcquire来获取ttl(Time To Live,存活时间)。若为null,说明加锁成功。若ttl不为null,说明加锁失败,则会阻塞进行等到锁。具体有以下几步操作,
首先使用subscribe()给当前加锁失败的线程去订阅一个channel(当该线程调用unlock()或者interrupt()时,redis会发送事件通知,让该线程不再阻塞等待锁)。
然后进行下面的while循环,尝试加锁刷新这个ttl的时间, 分析ttl >= 0的逻辑,等待ttl秒获取许可, 假设现在ttl是10秒,这个方法就会阻塞在这里等待10s之后去循环这个while继续尝试加锁。
public void lock(long leaseTime, TimeUnit unit) {
try {
this.lockInterruptibly(leaseTime, unit);
} catch (InterruptedException var5) {
Thread.currentThread().interrupt();
}
}
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
this.commandExecutor.syncSubscription(future);
try {
while(true) {
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
if (ttl >= 0L) {
// .getLatch()是一个Semaphore 信号量
// 这个方法就会阻塞在这里等待ttl之后去循环这个while继续尝试加锁
this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
this.getEntry(threadId).getLatch().acquire();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
lock方法方法中最重要的还是调用了tryAcquire()方法来获取ttl。其中tryAcquire()分两步。调用tryAcquireAsync()方法,由于 leaseTime == -1,于是又调用 tryLockInnerAsync()方法。若leaseTime==-1,则加锁时间默认为是30s。
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.addListener(new FutureListener<Long>() {
public void operationComplete(Future<Long> future) throws Exception {
if (future.isSuccess()) {
Long ttlRemaining = (Long)future.getNow();
if (ttlRemaining == null) {
RedissonLock.this.scheduleExpirationRenewal(threadId);
}
}
}
});
return ttlRemainingFuture;
}
}
真正加锁的操作还是用的lua语法实现的,如下所示。
1. 根据key查询是否存在,若不存在。则是在一个 getLockName(threadId),值为1的键值对,并设置过期时间,返回null。
2. 如果key查询存在,则根据getLockName(threadId)判断是否存在,若存在,则在其值上+1,并重新设置过期时间,返回null。(可重入锁)
3. 否则(即key存在,且被其他线程占有),则返回ttl。
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.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(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
KEYS[1] 为 getName(),ARGV[2] 为 getLockName(threadId)。ARGV[1] internalLockLeaseTime,锁的有效时间。
释放锁过程
public void unlock() {
this.get(this.unlockAsync(Thread.currentThread().getId()));
}
public RFuture<Void> unlockAsync(final long threadId) {
final RPromise<Void> result = new RedissonPromise();
RFuture<Boolean> future = this.unlockInnerAsync(threadId);
future.addListener(new FutureListener<Boolean>() {
public void operationComplete(Future<Boolean> future) throws Exception {
if (!future.isSuccess()) {
RedissonLock.this.cancelExpirationRenewal(threadId);
result.tryFailure(future.cause());
} else {
Boolean opStatus = (Boolean)future.getNow();
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + RedissonLock.this.id + " thread-id: " + threadId);
result.tryFailure(cause);
} else {
if (opStatus) {
RedissonLock.this.cancelExpirationRenewal((Long)null);
}
result.trySuccess((Object)null);
}
}
}
});
return result;
}
解锁过程最终还是执行的以下redis命令,其中涉及到了四个参数,分别为:
KEYS[1]:key
KEYS[2]:ChannelName,加锁的时候会生成一个channel
ARGV[1]:LockPubSub.unlockMessage,存的是0
ARGV[2]:生存时间
ARGV[3]:getLockName(threadId),这个线程对应的锁名称
释放锁的逻辑为:
广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了。
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return this.commandExecutor.evalWriteAsync(this.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(this.getName(), this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId)});
}