Redis分布式锁

加锁

java 复制代码
// org.redisson.RedissonLock
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
        if (ttl != null) {
            CompletableFuture<RedissonLockEntry> future = this.subscribe(threadId);
            this.pubSub.timeout(future);
            RedissonLockEntry entry;
            if (interruptibly) {
                entry = (RedissonLockEntry)this.commandExecutor.getInterrupted(future);
            } else {
                entry = (RedissonLockEntry)this.commandExecutor.get(future);
            }

            try {
                while(true) {
                    ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
                    if (ttl == null) {
                        return;
                    }

                    if (ttl >= 0L) {
                        try {
                            // `ttl >=0`:锁有过期时间(不是看门狗模式,手动指定 leaseTime)
                            entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        } catch (InterruptedException e) {
                            if (interruptibly) { // 如果是 `lockInterruptibly`(interruptibly=true):遇到中断直接抛异常
                                throw e;
                            }
												// 不支持中断:吃掉本次异常,继续阻塞等待
                            entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                        }
                    } else if (interruptibly) {
                        // `ttl < 0` → 就是 `-1`:代表**看门狗模式(leaseTime=-1),锁没有设置过期时间**
                        // 锁不会自己过期,只能等别人 unlock 发 publish 消息唤醒。
                        // 可中断阻塞
                        entry.getLatch().acquire();
                    } else { 
                        // `ttl < 0` → 就是 `-1`:代表**看门狗模式(leaseTime=-1),锁没有设置过期时间**
                        // 锁不会自己过期,只能等别人 unlock 发 publish 消息唤醒。
                        // 不可中断阻塞
                        entry.getLatch().acquireUninterruptibly();
                    }
                }
            } finally {
                this.unsubscribe(entry, threadId);
            }
        }
    }

tryAcquire

lua 复制代码
// 锁不存在,或者当前线程已经持有锁了。锁的持有值+1、重置过期时间
if ((redis.call('exists', KEYS[1]) == 0) or (redis.call('hexists', KEYS[1], ARGV[2]) == 1)) 
then 
redis.call('hincrby', KEYS[1], ARGV[2], 1); 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; 
end; 
// 锁已经被别人持有了,返回`KEYS[1]` 距离过期还剩多少毫秒。
return redis.call('pttl', KEYS[1]);

参数:
Collections.singletonList(this.getRawName() 锁名), 
new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId) 表示=> this.getServiceManager().getId() : 线程id
}

在tryAcquire的过程中,涉及到看门狗定时任务

在tryAcuire()的过程中,如果抢到了锁,会定时延长锁的有效期。

底层:不是 JDK ScheduledExecutorService,是 Netty 的 HashedWheelTimer(时间轮) 调度的异步定时任务。

java 复制代码
private RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        if (leaseTime > 0L) {
            ttlRemainingFuture = this.<Long>tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = this.<Long>tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }

        CompletionStage<Long> s = this.handleNoSync(threadId, ttlRemainingFuture);
        ttlRemainingFuture = new CompletableFutureWrapper(s);
        CompletionStage<Long> f = ttlRemainingFuture.thenApply((ttlRemaining) -> {
            if (ttlRemaining == null) { // 抢到锁了
                if (leaseTime > 0L) { 
                    this.internalLockLeaseTime = unit.toMillis(leaseTime);
                } else { // leaseTime是-1,也就是未设置
                    this.scheduleExpirationRenewal(threadId);
                }
            }

            return ttlRemaining;
        });
        return new CompletableFutureWrapper(f);
    }
    
private void renewExpiration() {
        // 这里的EXPIRATION_RENEWAL_MAP是
        // private static final ConcurrentMap<String, ExpirationEntry> EXPIRATION_RENEWAL_MAP = new ConcurrentHashMap();
        ExpirationEntry ee = (ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.getServiceManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    ExpirationEntry ent = (ExpirationEntry)RedissonBaseLock.EXPIRATION_RENEWAL_MAP.get(RedissonBaseLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            CompletionStage<Boolean> future = RedissonBaseLock.this.renewExpirationAsync(threadId);
                            future.whenComplete((res, e) -> {
                                if (e != null) {
                                    if (!RedissonBaseLock.this.getServiceManager().isShuttingDown(e)) {
                                        RedissonBaseLock.log.error("Can't update lock {} expiration", RedissonBaseLock.this.getRawName(), e);
                                        RedissonBaseLock.EXPIRATION_RENEWAL_MAP.remove(RedissonBaseLock.this.getEntryName());
                                    }
                                } else {
                                    if (res) {
                                        RedissonBaseLock.this.renewExpiration();
                                    } else {
                                        RedissonBaseLock.this.cancelExpirationRenewal((Long)null, (Boolean)null);
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

如下 "延长锁的有效期的Lua脚本"可知:看门狗是每个锁一个独立的时间轮任务,不是全局一个线程轮询所有锁。

java 复制代码
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) 
then 
redis.call('pexpire', KEYS[1], ARGV[1]); 
return 1; 
end; 
return 0;

参数:
Collections.singletonList(this.getRawName()) 锁名
new Object[]{this.internalLockLeaseTime  this.getServiceManager().getCfg().getLockWatchdogTimeout(), 
this.getLockName(threadId) this.getServiceManager().getId() + ":" + threadId}

定时任务总结

Redisson 分布式锁有看门狗后台定时续期任务,仅在不加 leaseTime 的 lock 模式启用;默认 30s 租期,每隔 10s 刷新一次过期时间;一旦调用 unlock,任务就取消;如果服务宕机,看门狗停止,锁到期自动释放。如果手动指定 leaseTime,看门狗直接关闭,锁到期自动释放,不会续期。

this.subscribe(threadId)

如下所示,要获取一个名字的分布式锁,会获取到同一个AsyncSemaphore semaphore。

AsyncSemaphore 初始化 counter = 1(一个频道只有 1 个许可,保证同一时刻只有一个线程在走订阅创建流程)

java 复制代码
// org.redisson.pubsub.PublishSubscribe
参数1 this.getEntryName() 是:this.getServiceManager().getId() + ":" + 锁名;
参数2 this.getChannelName()是:prefixName("redisson_lock__channel",锁名);
public CompletableFuture<E> subscribe(String entryName, String channelName) {
        AsyncSemaphore semaphore = this.service.getSemaphore(new ChannelName(channelName));
        CompletableFuture<E> newPromise = new CompletableFuture();
        semaphore.acquire().thenAccept((c) -> {
            if (newPromise.isDone()) {
            	 // 超时定时器、或者外部 cancel
                semaphore.release();
            } else {
                E entry = (E)(this.entries.get(entryName));
                if (entry != null) {
                    // 锁名已经存在了,entry的引用数+1
                    entry.acquire();
                    semaphore.release();
                    entry.getPromise().whenComplete((r, e) -> {
                        if (e != null) {
                            newPromise.completeExceptionally(e);
                        } else {
                            newPromise.complete(r);
                        }
                    });
                } else { // 锁名不存在,创建新的entry
                    E value = this.createEntry(newPromise);
                    value.acquire();
                    E oldValue = (E)(this.entries.putIfAbsent(entryName, value));
                    if (oldValue != null) { // 在这里放进去之前,有其他的线程放进去了
                        oldValue.acquire(); // 锁名已经存在了,entry的引用数+1
                        semaphore.release();
                        oldValue.getPromise().whenComplete((r, e) -> {
                            if (e != null) {
                                newPromise.completeExceptionally(e);
                            } else {
                                newPromise.complete(r);
                            }
                        });
                    } else { // 需要真正向 Redis 发起 SUBSCRIBE 订阅命令
                        // 创建消息监听器!
                        // 就是前面说的 `LockPubSub.onMessage`,收到解锁 publish 消息时执行 `latch.release()` 的那个回调。
                        // 这个 listener 绑定当前新建的 entry。
                        RedisPubSubListener<Object> listener = this.createListener(channelName, value);
                        // 这里把 semaphore 传给底层,订阅成功 / 失败后底层会释放这个 semaphore。
                        CompletableFuture<PubSubConnectionEntry> s = this.service.subscribeNoTimeout(LongCodec.INSTANCE, channelName, semaphore, new RedisPubSubListener[]{listener});
                        newPromise.whenComplete((r, e) -> {
                            if (e != null) {
                            // 监听本次订阅的 newPromise,如果调用方提前取消了这个订阅 promise,把底层订阅 s 也标记异常。
                                s.completeExceptionally(e);
                            }

                        });
                        s.whenComplete((r, e) -> {
                            if (e != null) {
                            // 订阅 Redis 失败(网络异常、Redis 报错):
												// - 把 entry 从 entries map 删掉
												// - 标记 entry 内部 promise 异常,所有等待这个 entry 的线程都会收到异常
                                this.entries.remove(entryName);
                                value.getPromise().completeExceptionally(e);
                            } else {
                            // 订阅 Redis 成功:
											  // - 完成 entry 内部 promise,把 entry 传给所有等待线程;
											  // - 如果 entry 的 promise 已经异常(外部取消),回滚删掉 entry。
                                if (!value.getPromise().complete(value) && value.getPromise().isCompletedExceptionally()) {
                                    this.entries.remove(entryName);
                                }

                            }
                        });
                    }
                }
            }
        });
        return newPromise;
    }

semaphore.acquire()

在上一个future complete之前,这个future无法complete

java 复制代码
public CompletableFuture<Void> acquire() {
        CompletableFuture<Void> future = new CompletableFuture();
        this.listeners.add(future);
        // 尝试去执行消费逻辑
        this.tryForkAndRun();
        return future;
    }

    private void tryForkAndRun() {
        if (this.executorService != null) {
            int val = this.tasksLatch.get();
            // `stackSize > 25 * val`,栈太深了,不能继续同步递归跑
            //  CAS 修改 `tasksLatch`,提交任务到线程池去执行 `tryRun()`
            // 任务执行完,tasksLatch 减回来
            if (this.stackSize.get() > 25 * val && this.tasksLatch.compareAndSet(val, val + 1)) {
                this.executorService.submit(() -> {
                    this.tasksLatch.decrementAndGet();
                    this.tryRun();
                });
                return;
            }
        }

        this.tryRun();
    }

    // 如果成功唤醒了一个等待者,本次 tryRun 结束。
    private void tryRun() {
        do {
            if (this.counter.decrementAndGet() >= 0) {
                CompletableFuture<Void> future = (CompletableFuture)this.listeners.poll();
                if (future == null) {
                    this.counter.incrementAndGet();
                    return;
                }

                boolean complete;
                if (this.executorService != null) {
                    this.stackSize.incrementAndGet();
                    complete = future.complete((Object)null);
                    this.stackSize.decrementAndGet();
                } else {
                    complete = future.complete((Object)null);
                }

                if (complete) {
                    return;
                }
            }
        } while(this.counter.incrementAndGet() > 0);

    }

   public void release() {
        this.counter.incrementAndGet();
        this.tryForkAndRun();
    }

如果第一个线程进来,counter从1变为0,成功让一个future complete。

第二个线程进来,counter从0变为-1 -> while counter从-1变0,无法进入循环了。要等semaphore.release()的执行,counter又从0变1了,并再次执行逻辑。

解锁

lua 复制代码
// 1. 如果 key 存在(`val ~= false`):说明**本次解锁逻辑已经执行过一遍了**,直接返回存好的值(0 或者 1),后面逻辑不再跑。
> 作用:**幂等防重**。网络重试、异步回调重复调用 unlock 脚本的时候,不会重复解锁、重复 publish。
local val = redis.call('get', KEYS[3]); 
if val ~= false then return tonumber(val);
end; 
// **这个线程根本不持有这把锁**,直接返回 nil,解锁失败
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) 
then 
return nil;
end; 
// 重入次数 **减 1**
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) 
then 
// 重置锁的过期时间(刷新 TTL)
redis.call('pexpire', KEYS[1], ARGV[2]); 
// 写入标记 key,值 = 0,代表本次解锁只是重入减计数,锁还存在
redis.call('set', KEYS[3], 0, 'px', ARGV[5]); 
return 0; 
else 
redis.call('del', KEYS[1]);
// 往 pubsub 频道发送解锁消息 
redis.call(ARGV[4], KEYS[2], ARGV[1]); 
// 写入标记 key,值 = 1,代表本次是**真正释放锁、发了 publish**。px表示设置毫秒级的过期时间
redis.call('set', KEYS[3], 1, 'px', ARGV[5]); 
return 1; 
end; 

参数:
Arrays.asList(
this.getRawName() 锁名, 
this.getChannelName() prefixName("redisson_lock__channel", 锁名),
this.getUnlockLatchName(requestId 随机id) prefixName("redisson_unlock_latch", 锁名) + ":" + requestId)
new Object[]{
LockPubSub.UNLOCK_MESSAGE, 
this.internalLockLeaseTime, 
this.getLockName(threadId) this.getServiceManager().getId() + ":" + threadId, 
this.getSubscribeService().getPublishCommand(), 
timeout}

整个调用流程

先抢锁,如果锁没有抢到,返回key的剩余过期时间;

抢锁失败,去订阅锁对应的 pubsub 频道;

pubSub.timeout(future):给订阅 future 加超时,防止订阅一直卡着;

阻塞等待订阅完成,拿到 RedissonLockEntry,所有的线程只要抢的是同一把锁,那么就会得到同一个entry对象;

在循环中:不停的阻塞(当Semaphore被释放时就尝试获取)和抢锁;

调用 unsubscribe,entry 引用计数 - 1;引用计数归零才发 UNSUBSCRIBE,取消 Redis 订阅。

unlock调用 → 执行这个lua重入次数归零 → del锁key + publish channel 0

Netty IO线程收到publish消息 → LockPubSub.onMessage → latch.release()→ 业务线程卡在 entry.getLatch().tryAcquire() 被唤醒 →

while循环再次tryAcquire抢锁。

java 复制代码
public class LockPubSub extends PublishSubscribe<RedissonLockEntry> {
    public static final Long UNLOCK_MESSAGE = 0L;
    public static final Long READ_UNLOCK_MESSAGE = 1L;

    public LockPubSub(PublishSubscribeService service) {
        super(service);
    }

    protected RedissonLockEntry createEntry(CompletableFuture<RedissonLockEntry> newPromise) {
        return new RedissonLockEntry(newPromise);
    }
    // 解锁那里发布了publish消息,这里收到就释放Semaphore。所以在尝试加锁的entry.getLatch().tryAcquire部分就能继续往下走了。
    protected void onMessage(RedissonLockEntry value, Long message) {
        if (message.equals(UNLOCK_MESSAGE)) {
            value.tryRunListener();
            value.getLatch().release();
        } else if (message.equals(READ_UNLOCK_MESSAGE)) {
            value.tryRunAllListeners();
            value.getLatch().release(value.getLatch().getQueueLength());
        }

    }
}
相关推荐
❀͜͡傀儡师3 小时前
MySQL REST Service 认证鉴权体系与 MySQL 26.10.0 内置化演进
数据库·mysql·adb
4SAPI3 小时前
营销内容生成大模型 API 生产选型指南:4SAPI、硅基流动与 OpenRouter 的架构权衡
数据库·人工智能·架构·php
zyxqyy&∞3 小时前
sqli-labs-union联合查询(Less1-Less4)
数据库·安全
数据库小学妹4 小时前
MySQL binlog格式怎么选?ROW、STATEMENT、MIXED的区别与坑
数据库·mysql·binlog·主从复制·数据一致性
小冯不疯4 小时前
查询旺店通货品策略实战教程:从源系统拉取到轻易云的端到端落地
java·大数据·数据库
RacentCLM4 小时前
HTTPS只在登录时有用?关于TLS的误解讲清
数据库·网络协议·https
杨云龙UP5 小时前
生产实战:Keepalived + Nginx + MySQL 双向复制高可用架构部署与原理详解
运维·数据库·mysql·nginx·架构·keepalived·高可用架构
代钦塔拉5 小时前
【C++/Qt 进阶】设计模式系列(7):过滤器模式——把“筛选条件“变成可组合的积木
数据库·系统架构