Redisson 源码解析系列二:分布式锁RedissonLock获取锁重试机制

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的锁重试机制叙述完成,可能内容有点混乱,笔者精力和能力有限,暂时只能解读到此,望读者谅解。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redisson是一个基于Redis的Java驻留库,提供了分布式和线程安全的Java数据结构。Redisson分布式锁实现是基于Redis的setnx命令和Lua脚本实现的。下面是Redisson分布式锁源码分析: 1.获取 Redisson分布式锁获取方法是tryAcquire方法,该方法首先会尝试使用setnx命令在Redis中创建一个key,如果创建成功则表示获取成功,否则会进入自旋等待。在自旋等待期间,Redisson会使用watchDog机制来监控的状态,如果被其他线程释放,则会重新尝试获取。 2.释放 Redisson分布式锁释放方法是release方法,该方法会使用Lua脚本来判断当前线程是否持有,如果持有则会使用del命令删除的key。 3.watchDog机制 Redisson的watchDog机制是用来监控的状态的,该机制会在获取时启动一个定时任务,定时任务会检查的状态,如果被其他线程释放,则会重新尝试获取。 ```java // 获取 public boolean tryAcquire(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException { long time = unit.toMillis(waitTime); long current = System.currentTimeMillis(); final long threadId = Thread.currentThread().getId(); final long leaseTimeInMillis = unit.toMillis(leaseTime); while (true) { if (tryAcquire()) { scheduleExpirationRenewal(threadId, leaseTimeInMillis); return true; } time -= (System.currentTimeMillis() - current); if (time <= 0) { return false; } current = System.currentTimeMillis(); if (Thread.interrupted()) { throw new InterruptedException(); } // watchDog机制 RFuture<RedissonLockEntry> future = subscribe(threadId); if (!future.await(time, TimeUnit.MILLISECONDS)) { return false; } } } // 释放 public void unlock() { if (isHeldByCurrentThread()) { unlockInner(); } } private void unlockInner() { Long ttl = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_LONG, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then return nil end; " + "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " + "if (counter > 0) then return 0 end; " + "redis.call('del', KEYS[1]); " + "redis.call('publish', KEYS[2], ARGV[1]); " + "return 1;", Arrays.<Object>asList(getName(), getChannelName()), encode(new UnlockMessage(getName(), getLockName())), id); cancelExpirationRenewal(); if (ttl == null) { throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + id + " thread-id: " + Thread.currentThread().getId()); } if (ttl == -1) { get(lockName).deleteAsync(); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值