" />
文章

分布式锁加锁失败后的等待逻辑是如何实现的

分布式锁加锁失败后的等待逻辑是如何实现的

面试指北 · 蔚来

在 Redisson 的源码中,监听锁释放的逻辑主要由 LockPubSub 类负责,它利用了 Redis 的 Pub/Sub 机制 来监听锁的释放事件。


锁释放监听的实现过程

  1. 锁释放时,向 Redis 频道发布消息
  • 当某个线程释放锁时,Redisson 的 unlock 方法会向对应的 Redis 频道(redisson_lock__channel:<锁名称>)发布一条解锁消息。

  • 发布消息的逻辑在

    unlockInnerAsync

    方法中:

    java redis.call(ARGV[4], KEYS[2], ARGV[1]);

    • ARGV[4] 是 Redis 发布命令 PUBLISH。
    • KEYS[2] 是锁释放通知的频道名称,例如 redisson_lock__channel:<锁名称>。
    • ARGV[1] 是解锁消息的内容。
  1. 订阅锁释放事件
  • 当某线程尝试获取锁但失败时,会通过 subscribe 方法订阅 Redis 的解锁频道。

  • subscribe

    方法的实现:

    java protected CompletableFuture<RedissonLockEntry> subscribe(long threadId) { return pubSub.subscribe(getEntryName(), getChannelName()); }

    • getChannelName():生成锁释放的频道名称,类似于 redisson_lock__channel:<锁名称>。
    • pubSub.subscribe(...):订阅指定频道,并返回一个 CompletableFuture,表示订阅完成的异步状态。
  1. 监听消息并唤醒等待线程
  • 当锁释放时,Redis 会向频道发布消息,订阅的线程会收到该消息。

  • LockPubSub

    会调用注册的回调函数,通知所有等待的线程:

    java @Override protected void onMessage(RedissonLockEntry value) { // 唤醒所有等待线程 value.getLatch().release(); value.notifyListeners(); }

    • value.getLatch().release():释放线程等待的 Latch。
    • value.notifyListeners():通知线程重新尝试获取锁。
  1. 线程唤醒后重新尝试获取锁
  • 被唤醒的线程会调用

    tryAcquire

    方法,重新尝试获取锁:

    java while (true) { Long ttl = tryAcquire(...); if (ttl == null) { // 锁成功获取 break; } }


锁释放监听的关键点

1. 频道名称

频道名称是 redisson_lock__channel:<锁名称>,由以下方法生成:

String getChannelName() {
    return prefixName("redisson_lock__channel", getRawName());
}

2. 发布消息

当锁被释放时,unlock 方法会通过 Redis 的 PUBLISH 命令向频道发布消息:

redis.call('PUBLISH', KEYS[2], ARGV[1]);

3. 订阅消息

锁的订阅通过 LockPubSub 类完成,订阅的逻辑如下:

pubSub.subscribe(getEntryName(), getChannelName());
  • getChannelName() 是要订阅的频道名称。
  • subscribe 方法会注册一个监听器,当消息到达时会触发回调。

4. 消息到达时唤醒等待线程

  • 当消息到达时,

onMessage

方法被调用:

java protected void onMessage(RedissonLockEntry value) { value.getLatch().release(); // 唤醒等待线程 value.notifyListeners(); // 通知监听器 }


完整流程

  1. 加锁失败

:

  • 线程订阅锁的释放频道并进入等待状态(通过 Latch 阻塞)。
  1. 锁释放

:

  • 持有锁的线程调用 unlock 方法,Redis 发布解锁消息。
  1. 收到消息

:

  • 订阅频道的线程通过 LockPubSub 接收到解锁通知。
  1. 唤醒线程

:

  • onMessage 方法释放 Latch,唤醒等待的线程。
  1. 重新尝试加锁

:

  • 被唤醒的线程调用 tryAcquire 方法重新尝试获取锁。

代码示例:关键类的简化逻辑

发布消息

-- Redis 脚本解锁时发布消息
if (redis.call('del', KEYS[1]) == 1) then
    redis.call('PUBLISH', KEYS[2], ARGV[1]); -- 发布锁释放消息
    return 1;
end

订阅逻辑

protected CompletableFuture<RedissonLockEntry> subscribe(long threadId) {
    return pubSub.subscribe(getEntryName(), getChannelName());
}

接收消息并唤醒

@Override
protected void onMessage(RedissonLockEntry value) {
    value.getLatch().release(); // 唤醒等待线程
    value.notifyListeners();    // 通知监听器
}

核心代码

lock

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }

        CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
        pubSub.timeout(future);
        RedissonLockEntry entry;
        if (interruptibly) {
            entry = commandExecutor.getInterrupted(future);
        } else {
            entry = commandExecutor.get(future);
        }

        try {
            while (true) {
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    break;
                }

                // waiting for message
                if (ttl >= 0) {
                    try {
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        entry.getLatch().acquire();
                    } else {
                        entry.getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            unsubscribe(entry, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

解锁

protected RFuture<Boolean> unlockInnerAsync(long threadId, String requestId, int timeout) {
    return evalWriteSyncedNoRetryAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        "local val = redis.call('get', KEYS[3]); " +
            "if val ~= false then " +
                "return tonumber(val);" +
            "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]); " +
                "redis.call('set', KEYS[3], 0, 'px', ARGV[5]); " +
                "return 0; " +
            "else " +
                "redis.call('del', KEYS[1]); " +
                "redis.call(ARGV[4], KEYS[2], ARGV[1]); " +
                "redis.call('set', KEYS[3], 1, 'px', ARGV[5]); " +
                "return 1; " +
            "end; ",
        Arrays.asList(getRawName(), getChannelName(), getUnlockLatchName(requestId)),
        LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime,
        getLockName(threadId), getSubscribeService().getPublishCommand(), timeout);
}

总结

Redisson 的锁释放监听机制完全依赖于 Redis 的 Pub/Sub 功能:

  1. 释放锁时:通过 PUBLISH 向频道发布解锁消息。
  2. 订阅锁释放:加锁失败的线程订阅频道并进入等待。
  3. 消息通知:当锁释放时,订阅者通过 Latch 被唤醒,重新尝试获取锁。

这种机制保证了加锁失败的线程不会频繁轮询 Redis,极大地优化了资源消耗,同时提高了锁的公平性。

参考资料

1. Redis分布式锁实现Redisson 15问

License:  CC BY 4.0