Redisson——浅析分布式锁之加锁

Redisson——浅析分布式锁之加锁

1. 分享目的

分布式锁往往有很多的情况需要考虑,比如锁住的任务还没执行完就超过锁过期时间,可重入等。这里通过分析Redisson加锁来对这些问题提供解决方案及完善需要考虑的情况。Redisson有几个加锁的方法,这里只分析经常使用的tryLock().

2. 源码分析

加锁tryLock()

@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();
    // 尝试获取锁,ttl为空表示成功获取锁,否则返回当前锁的剩余时间
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    if (ttl == null) {
        return true;
    }

    // 计算剩余等待时间
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        // 获取锁失败
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    current = System.currentTimeMillis();
    // 订阅解锁消息
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
    // 剩余等待时间内 阻塞线程
    // 如果返回false则表示等待时间内没有解锁,则获取锁失败
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        if (!subscribeFuture.cancel(false)) {
            subscribeFuture.onComplete((res, e) -> {
                if (e == null) {
                    // 取消订阅
                    unsubscribe(subscribeFuture, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    // 收到解锁消息,则多个线程竞争锁
    try {
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }

        // 间隔时间自旋
        while (true) {
            long currentTime = System.currentTimeMillis();
            // 尝试获取锁
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
            if (ttl == null) {
                return true;
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

            currentTime = System.currentTimeMillis();
            // 锁剩余时间和剩余等待时间取最小值t,在t时间内,使用Semaphore阻塞线程
            // 如在t时间内Semaphore.release(),表示已解锁,继续循环竞争锁
            if (ttl >= 0 && ttl < time) {
                subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }

            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId);
    }
}

加锁主流程

  1. tryAcquire()中通过netty跟redis进行通讯执行加锁的lua脚本,获取锁成功则返回,失败则获取锁剩余时间ttl;

  2. 订阅解锁消息,避免无效的自旋竞争锁,阻塞等待解锁消息;

  3. 收到解锁消息后,此时会进入间隔时间自旋,即多个线程竞争锁,当获取锁失败后,根据剩余等待时间和锁剩余时间的最小值t阻塞线程,阻塞后再次进入循环竞争锁;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gxejEWMI-1663686590547)(D:\rts\online-course\redisson\分布式锁——加锁\加锁主流程.png)]

竞争锁tryAcquire()

/**
 * 竞争锁
 */
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }

    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                                         commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
                                                         TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    // 使用默认leaseTime的话这里会增加watchDog机制
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e != null) {
            return;
        }

        // 表示获取到锁
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    // 获取锁是否成功
    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                          "if (redis.call('exists', KEYS[1]) == 0) then " +    // 不存在key,则设置key的值value+1,并设置过期时间
                          "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +  // 返回null
                          "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                          "return nil; " +                                   
                          "end; " +
                          "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +  // 存在key则重入锁value+1,重置过期时间
                          "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +             // 返回null
                          "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                          "return nil; " +
                          "end; " +
                          "return redis.call('pttl', KEYS[1]);",               // 返回剩余时间
                          Collections.singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}
  • 这里通过netty发送lua脚本保证原子性,这条lua脚本的逻辑:

    1. 是否存在等于锁名字的key,如果不存在,则设置数据结构为Hash,并将线程ID做为其中的属性,属性值+1用来做可重入锁,设置过期时间,返回null;这里表示获取到了锁;
    2. 当key存在,线程ID也存在其中的属性时,则将属性值+1,表示可重入,重置过期时间,返回null;表示获取到了锁;
    3. 当key存在,线程ID不等于其属性时,表示没获取到锁,返回锁的剩余时间。
  • 当使用默认的锁释放时间时,会添加watchdog机制

    private 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);
            // 定时监听线程
            renewExpiration();
        }
    }
    
    private void renewExpiration() {
        ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ee == null) {
            return;
        }
    
        // 使用HashedWheelTimer定时执行任务
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            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;
                }
    
                // 调用redis对锁续约
                RFuture<Boolean> future = renewExpirationAsync(threadId);
                future.onComplete((res, e) -> {
                    if (e != null) {
                        log.error("Can't update lock " + getName() + " expiration", e);
                        return;
                    }
    
                    // redis续约成功递归
                    if (res) {
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
        ee.setTimeout(task);
    }
    
    protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        // 锁存在且对应的属性包含threadId则重置锁的过期时间
        return evalWriteAsync(getName(), 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(getName()),
                              internalLockLeaseTime, getLockName(threadId));
    }
    

    使用HashedWheelTimer在默认过期时间的三分之一(10s)后执行任务,任务内容是调用redis对锁续约,续约成功再次递归调用。

订阅解锁消息subscribe()

主要逻辑代码在PublishSubscribe.subscribe()

public RFuture<E> subscribe(String entryName, String channelName) {
    AtomicReference<Runnable> listenerHolder = new AtomicReference<Runnable>();
    AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));
    RPromise<E> newPromise = new RedissonPromise<E>() {
        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            return semaphore.remove(listenerHolder.get());
        }
    };

    // redis发布订阅监听器,当收到redis消息时执行
    Runnable listener = new Runnable() {

        @Override
        public void run() {
            E entry = entries.get(entryName);
            if (entry != null) {
                entry.acquire();
                // 释放加锁时subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS)
                semaphore.release();
                entry.getPromise().onComplete(new TransferListener<E>(newPromise));
                return;
            }

            E value = createEntry(newPromise);
            value.acquire();

            E oldValue = entries.putIfAbsent(entryName, value);
            if (oldValue != null) {
                oldValue.acquire();
                semaphore.release();
                oldValue.getPromise().onComplete(new TransferListener<E>(newPromise));
                return;
            }

            RedisPubSubListener<Object> listener = createListener(channelName, value);
            service.subscribe(LongCodec.INSTANCE, channelName, semaphore, listener);
        }
    };
    semaphore.acquire(listener);
    listenerHolder.set(listener);

    return newPromise;
}

该方法的主要逻辑是组装redis的订阅发布,并且定义监听器,当收到解锁消息时执行监听器内容。

间隔自旋

当收到解锁消息后,此时可能会有多个线程竞争锁,这里会进入间隔自旋中。

  1. 竞争锁,即调用tryAcquire()
  2. 没获取到锁,则根据锁的剩余时间和剩余等待时间之间的最小值t,在t时间内使用Semaphore进行线程阻塞,这里可以联系上述订阅的代码,收到解锁消息后的监听任务里,调用了Semaphore.release()
  3. 阻塞后再次竞争锁。

3. 总结

讲了源码的主要流程,忽略了其中RFutureHashedWheelTimer等异步多线程设计,为了对redisson分布式加锁有个整体的认识,后面会对其中用到的设计进行详细分析。



世界那么大,感谢遇见,未来可期…

欢迎同频共振的那一部分人

作者公众号:Tarzan写bug

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值