Redisson的使用,pom文件引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.0</version>
</dependency>
Redisson源码地址
Redisson的配置
public class RedissonConfig {
("${spring.redis.host}")
private String host;
("${spring.redis.port}")
private String port;
// 初始化RedissonClient
public RedissonClient getRedisson() {
// Redisson 配置类
Config config = new Config();
// Redis连接地址
String address = new StringBuilder("redis://").append(host).append(":").append(port).toString();
// 单机模式
config.useSingleServer().setAddress(address).setDatabase(0);
// 根据配置创建RedissonClient并返回
return Redisson.create(config);
}
}
【RLock.lock】
【RLock.lock】的一个简单的例子
public String distributedLock(){
long decrResult = 0;
String uId = UUID.randomUUID().toString();
RLock lock = redissonClient.getLock("lockLock");
lock.lock();
try{
String stock = stringRedisTemplate.opsForValue().get("key");
if(stock != null){
if (Integer.parseInt(stock) > 0){
decrResult = stringRedisTemplate.opsForValue().decrement("key");
System.out.println(decrResult);
}
}
}finally {
lock.unlock();
}
return "购买成功,剩余库存:" + decrResult;
}
锁名称的赋值
通过【RedissonClient.getLock】的方式设置锁,传入的参数为锁的名称
RLock lock = redissonClient.getLock("lockLock");
public RLock getLock(String name) {
return new RedissonLock(commandExecutor, name);
}
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
public RedissonBaseLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.id = commandExecutor.getConnectionManager().getId();
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = id + ":" + name;
}
RedissonExpirable(CommandAsyncExecutor connectionManager, String name) {
super(connectionManager, name);
}
public RedissonObject(CommandAsyncExecutor commandExecutor, String name) {
this(commandExecutor.getConnectionManager().getCodec(), commandExecutor, name);
}
到了这里通过【setName】方法进行设置锁名称
public RedissonObject(Codec codec, CommandAsyncExecutor commandExecutor, String name) {
this.codec = codec;
this.commandExecutor = commandExecutor;
if (name == null) {
throw new NullPointerException("name can't be null");
}
setName(name);
}
将锁名称赋值给了一个全局变量name
protected String name;
protected final void setName(String name) {
this.name = commandExecutor.getConnectionManager().getConfig().getNameMapper().map(name);
}
public String map(String name) {
return name;
}
lock
在外调用lock方法时,其实内部只是屏蔽了对参数的赋值,在你不知道的地方总有人负重前行。
public void lock() {
try {
lock(-1, null, false);
} catch (InterruptedException e) {
throw new IllegalStateException();
}
}
方法调用到这里时参数已经设置完毕,没有过期时间,没有时间单位,不可中断。
这里的逻辑如下:
- 获取当前线程并以为参数,进行尝试加锁,加锁成功直接return。
- 加锁失败开启异步任务,去订阅加锁成功的Key
- 在循环中不断的尝试加锁,加锁成功则跳出循环,取消订阅
加锁的方法是【tryAcquire】
加锁的核心代码
【tryAcquire】方法传入的参数【waitTime ==-1】和【leaseTime== -1】
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
// 获取当前线程的ID
long threadId = Thread.currentThread().getId();
// 尝试加锁,加锁成功返回的时null,加锁失败返回的是加锁线程剩余的时间
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired,加锁成功
if (ttl == null) {
return;
}
// 异步订阅一个Channel
CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
pubSub.timeout(future);
RedissonLockEntry entry;
// 是否需要中断-false
// 防止在非公平的情况下,某些线程因为饥饿等待过长时间不释放不取消消息订阅
// 对于lock方法interruptibly==false,所以不会进行中断
if (interruptibly) {
// 异步中断执行结果
entry = commandExecutor.getInterrupted(future);
} else {
// 异步执行结果
entry = commandExecutor.get(future);
}
// try以下的逻辑都是没有加到锁的尝试
// 没有加到锁的小倒霉蛋会在这里再次进行加锁
try {
// 注意这里是不断的循环尝试
while (true) {
// 执行加锁逻辑
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired,加锁成功,跳出循环
if (ttl == null) {
break;
}
// waiting for message,加锁失败,ttl锁剩余的时间
if (ttl >= 0) {
try {
// entry.getLatch()返回的是Semaphore
// Semaphore.tryAcquire是一个超时等待的方法,阻塞等待ttl秒,让出CPU
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
// 是否需要中断-false
if (interruptibly) {
throw e;
}
entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
// 负数情况
// 如果key不存在返回-2,如果key存在且无过期时间返回-1
if (interruptibly) {
entry.getLatch().acquire();
} else {
entry.getLatch().acquireUninterruptibly();
}
}
// 没有加到锁,也没有中断,就在while里一致循环并不断尝试
}
} finally {
// 加锁成功,取消订阅
unsubscribe(entry, threadId);
}
}
subscribe/订阅Channel
protected CompletableFuture<RedissonLockEntry> subscribe(long threadId) {
return pubSub.subscribe(getEntryName(), getChannelName());
}
Channel的名称就是[redisson_lock__channel]加上Key的name
String getChannelName() {
return prefixName("redisson_lock__channel", getRawName());
}
public static String prefixName(String prefix, String name) {
if (name.contains("{")) {
return prefix + ":" + name;
}
return prefix + ":{" + name + "}";
}
LockPubSub类
LockPubSub extends PublishSubscribe
,上面的【subscribe】订阅方法其实是【PublishSubscribe】类中的,而【pubSub】就是【LockPubSub】类。在【LockPubSub】类中有【onMessage】方法用于监听Channel的事件回调,是要监听的事件获取授权,并发去加锁。
protected void onMessage(RedissonLockEntry value, Long message) {
// 消息是锁释放的消息
if (message.equals(UNLOCK_MESSAGE)) {
Runnable runnableToExecute = value.getListeners().poll();
if (runnableToExecute != null) {
runnableToExecute.run();
}
// 获得Semaphore的release,并发去执行加锁
value.getLatch().release();
// 写锁释放的消息
} else if (message.equals(READ_UNLOCK_MESSAGE)) {
while (true) {
Runnable runnableToExecute = value.getListeners().poll();
if (runnableToExecute == null) {
break;
}
runnableToExecute.run();
}
// 获得Semaphore的release,并发去执行加锁
value.getLatch().release(value.getLatch().getQueueLength());
}
}
tryAcquire
在方法内又调用了【tryAcquireAsync(waitTime, leaseTime, unit, threadId)】方法,并返回一个【RFuture】对象,并通过RedissonObject.get获取到CommandAsyncExecutor,CommandAsyncExecutor.get获取到RFuture。
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
这里的【tryLockInnerAsync】方法是没有传入等待时间时,会采用默认的【leaseTime == 30S】
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
// 判断超时时间是否大于0
if (leaseTime > 0) {
// 大于0直接将超时时间直接传入
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// 超时时间小于0,设置默认的超时时间30S
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
// lock acquired,加锁成功,加锁成功时返回的就是null,这个判断分支一定会走
if (ttlRemaining == null) {
if (leaseTime > 0) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
// 开启定时任务,锁续命的刷新
scheduleExpirationRenewal(threadId);
}
}
return ttlRemaining;
});
return new CompletableFutureWrapper<>(f);
}
scheduleExpirationRenewal/支线
锁续命的逻辑如下
protected void scheduleExpirationRenewal(long threadId) {
ExpirationEntry entry = new ExpirationEntry();
ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
try {
// 递归调用进行锁续命
renewExpiration();
} finally {
if (Thread.currentThread().isInterrupted()) {
cancelExpirationRenewal(threadId);
}
}
}
}
private void renewExpiration() {
ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ee == null) {
return;
}
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
// 要具体执行的任务
CompletionStage<Boolean> future = renewExpirationAsync(threadId);
future.whenComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getRawName() + " expiration", e);
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
return;
}
// 执行的Lua脚本返回true,锁还在
if (res) {
// reschedule itself
// 如果线程还存在,就递归调用并续命
renewExpiration();
} else {
// 如果线程不存在,就关闭销毁线程
cancelExpirationRenewal(null);
}
});
}
// 每过设置时间的 1/3 时,就会再次执行任务
// 主线程执行10S秒后
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
具体任务就是执行一个Lua脚本,Key是否存在,存在重新设置过期时间30S,成功返回1对应Java的true,失败返回false。
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getRawName()),
internalLockLeaseTime, getLockName(threadId));
}
tryLockInnerAsync
在这里会向Redis发送Lua脚本,Redis执行Lua脚本进行加锁。使用eval
将命令写到Redis中去,一次网络调用,节约网络开销,且原子性的执行。
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', 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(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
锁的名称通过【getRawName】获取全局变量name,这个name就是一开始getLock时设置的锁名称。
public final String getRawName() {
return name;
}
【leaseTime】就在上一个方法中传入的【internalLockLeaseTime】,而这个全局变量又是通过【getLockWatchdogTimeout】方法获取的,俗称看门狗。
protected long internalLockLeaseTime;
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
this.commandExecutor = commandExecutor;
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
看门狗时时间在Config类里进行了初始化,默认30S
package org.redisson.config.Config.java
private long lockWatchdogTimeout = 30 * 1000;
public long getLockWatchdogTimeout() {
return lockWatchdogTimeout;
}
加锁的Lua脚本
参数说明
KEYS[1]-->就是Key,一开始getLock传入的参数,在这里是通过getRawName()获取
ARGV[1]-->锁的过期时间,对应unit.toMillis(leaseTime)
ARGV[2]-->Hash设置的field,对应getLockName(threadId)
脚本内容说明
// 当前Key不存在,可以加锁
if (redis.call('exists', KEYS[1]) == 0) then
// 设置Key,并进行+1操作
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 设置锁的过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
// 加锁成功返回nil,对应Java的null
return nil;
end;
// 锁已经存在,并且加锁成功
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
// 对field再次+1
redis.call('hincrby', KEYS[1], ARGV[2], 1);
// 刷新锁的过期时间,就是重新设置
redis.call('pexpire', KEYS[1], ARGV[1]);
// 获取锁成功返回nil,对应Java的null
return nil;
end;
// 加锁失败返回当前锁的剩余时间
return redis.call('pttl', KEYS[1]);
lock的加锁流程
Redisson加锁成功后饭返回一个Future异步任务,ttlRemainingFuture.thenApply;
,在匿名内部类中调用【scheduleExpirationRenewal】方法,在方法中就会去执行锁续命的逻辑,循环调用。
加锁失败的线程会在while中自旋尝试进行加锁,获取锁失败,则使用Semaphore信号量进行阻塞。根据订阅的Channel,当锁被释放时,会收到事件回调通知,获取到Semaphore的授权,唤醒阻塞,再去加锁。
unlock
加锁使用Lua脚本进行加锁,那解锁一定使用Lua脚本进行解锁,最终一定会又Lua脚本的调用,小样。
这里又是通过RedissonObject.get获取到CommandAsyncExecutor,CommandAsyncExecutor.get获取到RFuture。
unlockAsync
public void unlock() {
try {
// 释放锁
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
public RFuture<Void> unlockAsync(long threadId) {
// 释放锁
RFuture<Boolean> future = unlockInnerAsync(threadId);
CompletionStage<Void> f = future.handle((opStatus, e) -> {
// 取消锁续命的异步任务
cancelExpirationRenewal(threadId);
if (e != null) {
throw new CompletionException(e);
}
if (opStatus == null) {
IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
+ id + " thread-id: " + threadId);
throw new CompletionException(cause);
}
return null;
});
return new CompletableFutureWrapper<>(f);
}
unlockInnerAsync
果然呀,解锁方法【unlockInnerAsync】还是调用到了Lua脚本。
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"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(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}
cancelExpirationRenewal
当锁释放后,就不再需要对当前Key进行续命的异步任务了,所以应该在之后将任务取消
protected void cancelExpirationRenewal(Long threadId) {
// 根据名称获取到任务
ExpirationEntry task = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (task == null) {
// 说明已经移除,直接返回
return;
}
if (threadId != null) {
// 线程不为空,从任务中移除
task.removeThreadId(threadId);
}
if (threadId == null || task.hasNoThreads()) {
Timeout timeout = task.getTimeout();
if (timeout != null) {
timeout.cancel();
}
// 任务移除后,将Map的中的相关信息也进行移除
EXPIRATION_RENEWAL_MAP.remove(getEntryName());
}
}
解锁的Lua脚本
参数说明
KEYS[1]--> 就是Key,一开始getLock传入的参数,这里是通过getRawName()获取
KEYS[2]--> 发布消息时Channel的名称,这里是通过getRawName()获取
ARGV[1]--> 解锁时发布的消息内容,UNLOCK_MESSAGE = 0L;
ARGV[2]--> 锁续命的时间30S
ARGV[3]--> Redis的Hash操作的field值
脚本内容说明
// Key不存在直接返回null
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
return nil;
end;
// 对Key进行-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
// 如果value>0,说明存在重入的情况
if (counter > 0) then
// 锁续命
redis.call('pexpire', KEYS[1], ARGV[2]);
// 返回失败
return 0;
else
// 删除锁
redis.call('del', KEYS[1]);
向Channel发布消息
redis.call('publish', KEYS[2], ARGV[1]);
// 返回成功
return 1;
end;
return nil;
unlock的解锁流程
【RLock.tryLock】
在【Redisson】中对于特【tryLock】提供了三个不同的实现
在有参方法的使用时,参数少的方法最终会调用到参数多的方法,参数少的方法设置了默认的超时时间【leaseTime == -1】,但最终都会调用到【tryLockInnerAsync】方法,不带参数的【tryLock】方法基本与【RLock.lock】方法实现方式一致,所以这里看带有参数的【tryLock】方法
tryLock
相较于【lock】方法【tryLock】方法在执行过程中,不断的去判断等待的时间是否超过了【waitTime】,如果超出了,那就获取锁失败直接返回false。
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();
// 尝试加锁 leaseTime == -1
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired,ttl为null,锁获取成功
if (ttl == null) {
return true;
}
// 算出时间差
time -= System.currentTimeMillis() - current;
// 在指定时间内没有获取到锁,加锁失败,返回false
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 获取当前时间
current = System.currentTimeMillis();
// 订阅Channel
CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
try {
// 获取异步任务执行结果
subscribeFuture.get(time, TimeUnit.MILLISECONDS);
} catch (ExecutionException | TimeoutException e) {
// 任务取消了
if (!subscribeFuture.cancel(false)) {
subscribeFuture.whenComplete((res, ex) -> {
if (ex == null) {
// 取消订阅
unsubscribe(res, threadId);
}
});
}
// 执行器异常,超时异常,加锁失败,返回false
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
// 算出时间差
time -= System.currentTimeMillis() - current;
// 在指定时间内没有获取到锁,加锁失败,返回false
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 在指定时间内
while (true) {
// 算出时间差
long currentTime = System.currentTimeMillis();
// 尝试加锁
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired,加锁成功
if (ttl == null) {
// 返回true
return true;
}
// 算出时间差,在指定时间内没有获取到锁,加锁失败,返回false
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
// ttl 大于0,且ttl小于指定时间,那么尝试去获取锁
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
// 此时time小于ttl,只能寄希望与在time时间内,锁释放并尝试加锁
commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
// 算出时间差,在指定时间内没有获取到锁,加锁失败,返回false
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
}
} finally {
// 取消订阅
unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
}
}
tryLock的加锁流程
其实在加锁的主要流程上与【lock】方法相比多出来的部分一直是在不断的进行,等待时间的判断。同样也会调用Lua脚本进行加锁,只要在规定时间内,同样去订阅Channel进行异步通知回调,超时获取不到锁就加锁失败返回false
unlock
【tryLock】的【unlock】流程与【lock】的流程一样