基于Redis的分布式锁RedissonLock原理剖析

本文深入探讨RedissonLock的订阅机制,如何通过信号量和Redis的channel解决锁申请资源浪费问题,详细分析线程间如何同步等待锁释放,及无效锁申请的处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

RedissonLock#subscribe

订阅锁释放事件,并阻塞等待锁释放,有效的解决了无效的锁申请浪费资源的问题:
基于信号量,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争.

1、当 this.await 返回 false,说明等待时间已经超出获取锁最大等待时间,取消订阅并返回获取锁失败.
2、当 this.await 返回 true,进入循环尝试获取锁.

protected final LockPubSub pubSub;

...

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

entryName 格式:“id:name”;
channelName 格式:“redisson_lock__channel:{name}”;

RedissonLock#pubSub 是在RedissonLock构造函数中通过如下方式初始化的:

this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();

subscribeServiceMasterSlaveConnectionManager的实现中又是通过如下方式构造的,其中this就是MasterSlaveConnectionManager实例,config则为MasterSlaveServersConfig实例:

subscribeService = new PublishSubscribeService(this, config);

//PublishSubscribeService.java
private final LockPubSub lockPubSub = new LockPubSub(this);
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);
    }
}

我们会发现其在初始化时,会初始化一组信号量,至于用途是什么,我们会在后面揭晓。现在我们知道RedissonLock#pubSub是怎么初始化的了,让我们回到订阅流程。

/*
* @param entryName     格式:“id:name”
* @param channelName   格式:“redisson_lock__channel:{name}”
* @return
*/
//PublishSubscribe.java
public RFuture<E> subscribe(String entryName, String channelName) {
    //代码@1 对于同一个锁,semaphore为单例
    AsyncSemaphore semaphore = service.getSemaphore(new ChannelName(channelName));

    //每一个尝试获取锁失败的线程都会创建一个listenerHolder和一个newPromise,所以这里的listenerHolder、newPromise是与当前获取锁的线程绑定的
    AtomicReference<Runnable> listenerHolder = new AtomicReference<>();
    RPromise<E> newPromise = new RedissonPromise<E>() {
        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            return semaphore.remove(listenerHolder.get());
        }
    };

    //每一个尝试获取锁失败的线程都会创建一个listener
    Runnable listener = () -> {
        // entry's type is RedissonLockEntry
        E entry = entries.get(entryName);
        if (entry != null) {
            entry.acquire();
            semaphore.release();
            entry.getPromise().onComplete(new TransferListener<E>(newPromise));
            return;
        }

        // new RedissonLockEntry
        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> redisPubSubListener = createListener(channelName, value);
        service.subscribe(LongCodec.INSTANCE, channelName, semaphore, redisPubSubListener);
    };

    //首个尝试获取锁失败的线程acquire操作不会阻塞,会直接触发执行listener.run方法
    semaphore.acquire(listener);
    listenerHolder.set(listener);
    
    return newPromise;
}

代码@1

public AsyncSemaphore getSemaphore(ChannelName channelName) {
    return locks[Math.abs(channelName.hashCode() % locks.length)];
}

通过入参channelName的格式redisson_lock__channel:{name}我们知道,对于同一个锁,这里获取的信号量是同一个。

代码@2

public void acquire(Runnable listener) {
    acquire(listener, 1);
}

public void acquire(Runnable listener, int permits) {

    synchronized (this) {
        if (counter < permits) {
            listeners.add(new Entry(listener, permits));
            return;
        } else {
            counter -= permits;
        }
    }

    listener.run();
}

AsyncSemaphore#counter代表当前信号量允许的请求数,初始值为1,所以对于首个尝试获取该锁失败的线程会直接触发执行listener.run方法。而对于后续尝试获取该锁失败的线程则会创建Entry对象(保存listenerpermits的映射关系)并保存到AsyncSemaphore#listeners

尝试获取锁失败的线程会走到此流程订阅redis通知。

  1. 假设有A、B、C、etc多个线程顺序调用{@link #subscribe}方法,因为semaphore初始信号量为1(参见{@link PublishSubscribeService#PublishSubscribeService}方法),所以线程A可以获得信号量(参见{@link AsyncSemaphore#acquire}方法),并执行{@code listener.run}方法,而对于之后的线程B、C、etc,其listener会被封装成{@link AsyncSemaphore.Entry}保存在semaphore的{@link AsyncSemaphore#listeners}中。

  2. 首个尝试获取锁失败的线程{@code semaphore.acquire(listener);}操作不会阻塞,会直接触发执行{@code listener.run}方法。这里线程A作为首个尝试获取锁失败的线程,会执行{@code listener.run}方法,其发现{@link PublishSubscribe#entries}中并没有当前锁对应的记录,会创建一个{@link RedissonLockEntry}(参见{@link #createEntry}方法)并添加到{@link PublishSubscribe#entries}(姑且记为 A_e),key为"id:name"。同时会注册监听器redisPubSubListener。

(1) semaphore的{@link AsyncSemaphore#release()}方法会被调用,从{@link AsyncSemaphore#listeners}中取出一个{@link AsyncSemaphore.Entry}对象(线程B),并进而调用{@link AsyncSemaphore#acquire}方法此时能够成功获取信号量,并执行{@code listener.run}方法。在上述执行{@code listener.run}方法时,{@code E entry = entries.get(entryName);}获取到的{@link RedissonLockEntry}对象是前面线程A写入的A_e。接着调用{@code RedissonPromise#onComplete}方法为线程A的newPromise添加监听,监听器保存在{@link RedissonPromise#promise}对象的{@link DefaultPromise#listeners}中。监听器的作用用于在线程A的newPromise({@link RedissonLockEntry#promise})完成时将其的结果同步到当前线程(线程B、C、etc)的newPromise
继续调用semaphore的{@link AsyncSemaphore#release()}方法,以此类推。。。重复上述步骤(1)

代码如下:

@Override
public void onComplete(BiConsumer<? super T, ? super Throwable> action) {
    promise.addListener(f -> {
        if (!f.isSuccess()) {
            action.accept(null, f.cause());
            return;
        }
        
        action.accept((T) f.getNow(), null);
    });
}

(2) 监听器的{@link BaseRedisPubSubListener#onStatus}方法被调用,标记A_e的{@link RedissonLockEntry#promise}也即线程A的newPromise完成(执行代码:value.getPromise().trySuccess(value)),其会唤醒注册在newPromise的{@link RedissonPromise#promise}对象的{@link DefaultPromise#listeners}中的所有监听器,从而在线程A的newPromise({@link RedissonLockEntry#promise})完成时将其的结果同步到当前线程(线程B、C、etc)的newPromise

上述(1)(2)所达到的效果就是同步等待(参见 RedissonLock {@code subscribeFuture.await(time, TimeUnit.MILLISECONDS)})注册redis通知完成。

  1. redisPubSubListener监听器在收到redis的通知时,对于{@link @channelName}的消息会调用{@link PublishSubscribe.onMessage}方法释放一个信号量,唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁。

之后会通过如下方式阻塞等待订阅redis通知完成。

if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
    if (!subscribeFuture.cancel(false)) {
        subscribeFuture.onComplete((res, e) -> {
            if (e == null) {
                unsubscribe(subscribeFuture, threadId);
            }
        });
    }
    acquireFailed(threadId);
    return false;
}

如果等待超时,会通过RedissonPromise#cancel方法取消当前线程的订阅。而cancel方法的实现如下:

RPromise<E> newPromise = new RedissonPromise<E>() {
   @Override
    public boolean cancel(boolean mayInterruptIfRunning) {
        return semaphore.remove(listenerHolder.get());
    }
};

//AsyncSemaphore.java
public boolean remove(Runnable listener) {
   synchronized (this) {
        return listeners.remove(new Entry(listener, 0));
    }
}

前面我们提到过,线程B、C、etc因为获取不到信号量,其listener会被封装成{@link AsyncSemaphore.Entry}保存在semaphore的{@link AsyncSemaphore#listeners}中。这里就是各线程将各自的listenersemaphore的{@link AsyncSemaphore#listeners}中移除。对于第一个尝试获取锁失败的线程A,其并不会被保存在listeners中,所以这里移除会失败,即RedissonPromise#cancel方法会返回false。进而走到下面的subscribeFuture.onComplete逻辑。

### Redis 分布式锁Redisson 的实现原理 #### 1. **Redis 分布式锁** Redis 分布式锁的核心思想是通过 `SETNX` 和 `EXPIRE` 来实现加锁操作。在早期版本(Redis 2.6.12之前),由于 `SETNX` 不支持设置过期时间,因此需要分两步完成:先调用 `SETNX` 创建键值对表示锁定状态,再调用 `EXPIRE` 设置过期时间以防止死锁[^3]。 然而这种两步操作无法保证原子性,可能会因网络延迟或其他异常导致锁未成功创建却设置了过期时间。为此,Redis 2.6.12引入了新的命令 `SET key value NX PX milliseconds`,其中 `NX` 表示只有当键不存在时才执行设置操作,`PX` 则用于指定毫秒级的过期时间。这种方式能够在一个命令内完成加锁并设定超时,从而有效解决了上述问题。 #### 2. **Redisson 实现分布式锁** Redisson 是基于 Redis 开发的一个 Java 客户端库,它不仅实现了更高级别的抽象接口,还提供了多种类型的分布式对象和服务功能。对于分布式锁而言,Redisson 提供了一种更加健壮可靠的解决方案——`RLock` 接口及其子类实例化方式。 - **加锁逻辑** Redisson 使用 Lua 脚本来确保整个加锁过程具有原子性。该脚本会检查目标资源是否已被占用;如果未被占用,则尝试获取锁并将当前线程 ID 记录下来作为持有者的唯一标识符[^1]。 - **续命机制** 当某个客户端成功获得锁之后,Redisson 会在后台启动一个定时器任务定期向服务器发送续约请求延长锁的有效期限,直到显式解锁为止。此设计可以避免因长时间运行的任务而导致锁提前失效的情况发生[^2]。 - **自旋重试策略** 如果初次未能取得所需资源,则按照预定义间隔不断重复尝试直至达到最大等待时限或最终放弃争夺控制权。 - **公平性和可靠性保障措施** 在某些特殊情况下(比如网络分区), 可能会出现部分节点认为自己已经拿到了全局唯一的锁,但实际上其他地方也有竞争者存在的情形下, redisson 还特别考虑到了这一点并通过内部复杂的协调算法尽可能减少冲突概率[^4]. #### 性能对比分析 | 特性 | Redis 原生分布锁 | Redisson | |-------------------------|------------------------------------------|----------------------------------| | 加锁效率 | 较高 | 略低 | | 锁安全性 | 存在网络抖动等问题 | 更安全可靠 | | 功能扩展能力 | 单纯提供基础加解鎖功能 | 支持更多特性如自动续租、可重入等 | | 易用程度 | 需要开发者手动处理很多细节 | API 封装良好易于集成 | 从表中可以看出虽然原生态方法简单高效但在实际应用过程中往往面临诸多挑战;而借助第三方工具包则可以在一定程度上弥补这些不足之处. ```java // 示例代码展示如何利用Redisson进行分布式锁管理 import org.redisson.api.RLock; import org.redisson.api.RedissonClient; public class DistributedLockExample { private final RedissonClient redissonClient; public void acquireAndReleaseLock(String lockName) throws InterruptedException{ RLock lock = redissonClient.getLock(lockName); try { boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS); // 尝试获取锁最长等待时间为10秒 if(isLocked){ System.out.println(Thread.currentThread().getName()+" acquired the lock."); Thread.sleep(5000L); // Simulate some work }else{ System.err.println("Failed to get lock after waiting..."); } } finally { if(lock.isHeldByCurrentThread()){ lock.unlock(); System.out.println(Thread.currentThread().getName()+ " released the lock."); } } } } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值