1、RedissonLock获取锁重试机制
在上篇文章 : Redisson 源码解析系列一:分布式锁RedissonLock可重入机制+锁续约解析 已经介绍了RedissonLock加锁、释放锁和锁续约机制,在此篇文章介绍获取锁时的重试机制。
2、重试获取锁机制
在redisson中使用重试获取锁,需要指定waitTime,如下:
public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
return tryLock(waitTime, -1, unit);
}
主要在ryLock(long waitTime, long leaseTime, TimeUnit unit)
方法中实现锁重试机制。
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();
// 1、尝试获取锁,这里返回ttl,一定是获取锁失败,且单位是毫秒
Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// 获取锁成功,直接返回true
if (ttl == null) { return true;}
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
current = System.currentTimeMillis();
// 2、订阅channel,即执行redis publish/subscribe 发布/订阅指令
// channel 名称格式:redisson_lock__channel:{lockName},lockName 对应 Redis hash Key=lock:order:userId
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
// 3、同步等待监听器注册完成,订阅等待time,若等待time时间达到,未能收到消息,则解除订阅,代表锁获取失败,存在其他线程持有锁
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
//解除订阅channel
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(waitTime, unit, threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 4、正式进入Loop,执行锁重试机制
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) { return true; }
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
// 6、使用信号量机制尝试在ttl时间段内获取锁
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
// 7、判断当前剩余时间,若不充足,则返回false,获取锁失败
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(waitTime, unit, threadId);
return false;
}
// 8、若上述条件不成立,则代表尚有充足时间,继续Loop执行锁重试
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
}
可以看见,获取锁重试的逻辑比较之前不重试逻辑多了很多代码。其主要流程:
- 尝试获取锁,获取成功返回;
- 判断剩余时间是否充足,不充足则返回;
- 采用redis publish/subscribe机制,订阅channel消息,若其他线程释放锁,则publish消息进入管道;
- 执行subscribeFuture.await(time),阻塞等待剩余时间获取消息,若获取失败则返回;
- 进入Loop循环,尝试获取锁,失败则采用信号量机制,等待剩余时间ttl / time;
- 最后再判断剩余时间,若不充足,则返回false,获取锁失败,若成功则代表尚有充足时间,继续Loop执行锁重试
在解析锁重试机制过程中需要了解三个知识点,所在笔者在此单独写了一篇文章记录。
2.1 Redis 发布订阅机制 (publish/subscribe)
什么是发布订阅?
Redis 发布订阅(pub/sub)是一种消息通信模式——生产者(publish)发送消息,订阅者(subscribe)接收消息。
- 一个频道可以有任意多个订阅者
- 而一个订阅者也可以同时订阅任意多个频道
其命令如下:
127.0.0.1:6379> help PUBLISH --发布
PUBLISH channel message
summary: Post a message to a channel
since: 2.0.0
group: pubsub
127.0.0.1:6379> help SUBSCRIBE --订阅
SUBSCRIBE channel [channel ...]
summary: Listen for messages published to the given channels
since: 2.0.0
group: pubsub
127.0.0.1:6379> help PSUBSCRIBE --模式匹配订阅
PSUBSCRIBE pattern [pattern ...]
summary: Listen for messages published to channels matching the given patterns
since: 2.0.0
group: pubsub
在此仅以redissonLock 重试阶段逻辑展开叙述,更多的发布订阅机制内容放在另一篇文章。
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
主要是subscribe(threadId)方法,进入subscribe查看:
// entryName 格式:“UUID:name”;
// channelName 格式:“redisson_lock__channel:{name}”;
protected RFuture<RedissonLockEntry> subscribe(long threadId) {
return pubSub.subscribe(getEntryName(), getChannelName());
}
public RFuture<E> subscribe(String entryName, String channelName) {
// 1.获取当前锁的AsyncSemaphore 信号量对象——对于同一个锁,semaphore为单例
AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
RPromise<E> newPromise = new RedissonPromise<>();
// 2.semaphore.acquire() 返回一个 CompletableFuture 实例
semaphore.acquire(() -> {
if (!newPromise.setUncancellable()) {
semaphore.release();
return;
}
// 3.从entries 缓存对象中获取当前锁的RedissonLockEntry,即E是RedissonLockEntry类型
//ConcurrentMap<String, E> entries = new ConcurrentHashMap<>();
E entry = entries.get(entryName);
if (entry != null) {
entry.acquire();
semaphore.release();
entry.getPromise().onComplete(new TransferListener<E>(newPromise));
return;
}
// 4.若entries 缓存对象中不存在,则新建RedissonLockEntry对象,
// RedissonLockEntry 对应订阅任务,构建一个 RedissonLockEntry 实例
E value = createEntry(newPromise);
// 由于新创建的RedissonLockEntry中的线程许可数量为0,在此将其 counter++;是默认值为1
value.acquire();
E oldValue = entries.putIfAbsent(entryName, value);
if (oldValue != null) {
oldValue.acquire();
semaphore.release();
oldValue.getPromise().onComplete(new TransferListener<E>(newPromise));
return;
}
// 5.创建相关通道监听器
RedisPubSubListener<Object> listener = createListener(channelName, value);
// 6. 发起订阅操作
service.subscribe(LongCodec.INSTANCE, channelName, semaphore, listener);
});
return newPromise;
}
首先跟进第一个步骤,获取当前锁的信号量,且该属性如何初始化的。
/** PublishSubscribeService.java */
private final AsyncSemaphore[] locks = new AsyncSemaphore[50];
public PublishSubscribeService(ConnectionManager connectionManager, MasterSlaveServersConfig config) {
super();
this.connectionManager = connectionManager;
this.config = config;
for (int i = 0; i < locks.length; i++) {
locks[i] = new AsyncSemaphore(1);
}
}
public AsyncSemaphore getSemaphore(ChannelName channelName) {
return locks[Math.abs(channelName.hashCode() % locks.length)];
}
我们会发现其在初始化PublishSubscribeService时,会初始化一组信号量,获取信号量时使用channelName.hashCode()获取当前通过对应的信号量。改语法旨在:对于同一个锁,semaphore为单例。
紧接着执行semaphore.acquire() 返回一个 CompletableFuture 实例。
/** AsyncSemaphore.java */
private final AtomicInteger counter;
private final Queue<Entry> listeners = new ConcurrentLinkedQueue<>();
public void acquire(Runnable listener) {
acquire(listener, 1);
}
public void acquire(Runnable listener, int permits) {
if (permits <= 0) {
throw new IllegalArgumentException("permits should be non-zero");
}
// 当前线程runnable task 入队
listeners.add(new Entry(listener, permits));
tryRun(1);
}
private void tryRun(int permits) {
if (counter.get() == 0 || listeners.peek() == null) {
return;
}
// 当前线程许可>=0时,执行listener queue中的task
if (counter.addAndGet(-permits) >= 0) {
Entry e = listeners.peek();
if (e == null) {
counter.addAndGet(permits);
return;
}
if (e.getPermits() != permits) {
counter.addAndGet(permits);
tryRun(e.getPermits());
return;
}
// task 出队
Entry entry = listeners.poll();
if (entry == null) {
//若为空,则重置线程许可
counter.addAndGet(permits);
return;
}
if (removedListeners.remove(entry.getRunnable())) {
counter.addAndGet(entry.getPermits());
tryRun(1);
} else {
// 执行runnable task
entry.runnable.run();
}
} else {
counter.addAndGet(permits);
}
}
AsyncSemaphore#counter
代表当前信号量允许的请求数,初始值为1。
每次执行AsyncSemaphore#acquire()
都会将当前runnable task入AsyncSemaphore#listeners队列,即对于A\B\C\D等多个线程共同抢同一把锁时,会将各个线程的runnable task入队。
再执行AsyncSemaphore#tryRun(1)
,若线程许可>=0,则执行runnable task(首次获取锁拜的线程符合条件,会进入entry.runnable.run();执行)。也就是执行到了task内部。
task内部大体步骤:
1、从entries 缓存对象中获取当前锁的Entry,发现{@linkPublishSubscribe#entries}中并没有当前锁对应的记录
2、会创建一个{@link RedissonLockEntry}并添加到{@link PublishSubscribe#entries},key为"uuid:name"。
3、同时会注册监听器redisPubSubListener参考{@link PublishSubscribe#createListener}
4、监听通道事件并发起订阅操作,参考{@link PublishSubscribeService#subscribe}
2.2 信号量机制 (AsyncSemaphore)
什么是信号量?
信号量JUC下对基础锁的子类,信号量可以使得多个线程,同时访问一个资源.
RedissonLock获取锁和释放锁逻辑何总穿插着使用大量的信号量机制,在此需要先熟悉AsyncSemaphore机制。
AsyncSemaphore是不是和JDK中JUC包下Semaphore相似,主要用于实现 JVM 进程级的多线程限流,比如:限制某一业务只允许最多 50个线程并发访问。
首先先介绍下Semaphore。
-
Semaphore(信号量)可以用来限制能同时访问共享资源的线程上限,它内部维护了一个permits变量,就是线程许可的数量
-
Semaphore的许可数量如果小于0个,就会阻塞获取,直到有线程释放许可
-
Semaphore是一个非重入锁
示例如下:final Semaphore semp = new Semaphore(5); @Override public void run() { try { // acquire():尝试获得准入的许可,若无法获得,会持续等待,直到线程释放一个许可,或者线程中断。 semp.acquire(); // 尝试获取信号量,同一时刻最多允许5个线程访问 Thread.sleep(2000); System.out.println(Thread.currentThread().getId() + ":任务完成"); semp.release(); } catch (InterruptedException e) { e.printStackTrace(); } }
同样,AsyncSemaphore 也是这里原理,其 Async 前缀意味着其acquire()方法是非阻塞的并可以返回一个CompletableFuture实例。
在2.1 章节介绍了发布订阅逻辑后,当线程同步等待线程订阅频道超时时,将会使用信号量机制等待。也就是RedissonLock#tryLock(long waitTime, long leaseTime, TimeUnit unit)
方法中的逻辑
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
if (ttl >= 0 && ttl < time) {
// 6、使用信号量机制尝试在ttl时间段内获取锁
subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
在此的subscribeFuture就是上述所说的订阅频道所创建的结果。而其中的getLatch()
就是获取RedissonLockEntry中的信号量对象,如下:
public class RedissonLockEntry implements PubSubEntry<RedissonLockEntry> {
private int counter;
private final Semaphore latch; // juc下的信号量对象
private final RPromise<RedissonLockEntry> promise;
private final ConcurrentLinkedQueue<Runnable> listeners = new ConcurrentLinkedQueue<Runnable>();
public RedissonLockEntry(RPromise<RedissonLockEntry> promise) {
super();
this.latch = new Semaphore(0);
this.promise = promise;
}
}
所以,在上述逻辑中,使用subscribeFuture中的JUC下的Semaphore 对象,阻塞ttl/time时间尝试获取线程可执行信号,也就是Semaphore 对象的permits>0,,来达到阻塞一段时间的效果,而不是立马进行再次重试获取锁。巧妙的避开了不间断重复尝试获取锁而造成的cpu消耗问题。
那么这个Semaphore 对象的信号是什么时候才能获取的呢?
类比解锁阶段执行的lua脚本,解锁成功后,lua脚本中还执行了一条publish指令,用来向channel发送已经释放锁的消息。而其他获取锁失败的线程都向该channel绑定了listener,所以,我们进入subscribe阶段创建的listener内逻辑:
private RedisPubSubListener<Object> createListener(String channelName, E value) {
RedisPubSubListener<Object> listener = new BaseRedisPubSubListener() {
@Override
public void onMessage(CharSequence channel, Object message) {
if (!channelName.equals(channel.toString())) {
return;
}
PublishSubscribe.this.onMessage(value, (Long) message);
}
@Override
public boolean onStatus(PubSubType type, CharSequence channel) {
if (!channelName.equals(channel.toString())) {
return false;
}
if (type == PubSubType.SUBSCRIBE) {
value.getPromise().trySuccess(value);
return true;
}
return false;
}
};
return listener;
}
在此方法内,当channel收到消息,回调onMessage方法,首先进行channel名称是否匹配判断,若不是,则直接return。若channel匹配,再进入PublishSubscribe.this.onMessage(value, (Long) message);方法,实际上调用的是org.redisson.pubsub.LockPubSub#onMessage
方法。
@Override
protected void onMessage(RedissonLockEntry value, Long message) {
//解锁消息
if (message.equals(UNLOCK_MESSAGE)) {
Runnable runnableToExecute = value.getListeners().poll();
if (runnableToExecute != null) {
runnableToExecute.run();
}
// 释放信号量
value.getLatch().release();
} else if (message.equals(READ_UNLOCK_MESSAGE)) { //读锁 解锁消息
while (true) {
Runnable runnableToExecute = value.getListeners().poll();
if (runnableToExecute == null) {
break;
}
runnableToExecute.run();
}
value.getLatch().release(value.getLatch().getQueueLength());
}
}
到此,我们已经可以看见,当channel内监听到消息时,会调用RedissonLockEntry# getLatch()
获取JUC包下的Semaphore 对象,再接着调用Semaphore#release();
释放信号量,结束重试逻辑的等待ttl/time的阻塞等待时间。
到这里,已经将RedissonLock的锁重试机制叙述完成,可能内容有点混乱,笔者精力和能力有限,暂时只能解读到此,望读者谅解。