Redisson分布式锁的源码解读

之前秒杀项目中就用到了这个 Redisson 分布式锁 👇,这篇就一起来看看源码吧!

tryLock 加锁 流程

复制代码
// RedissonLock.java
@Override
public boolean tryLock() {
    return get(tryLockAsync());
}

@Override
public RFuture<Boolean> tryLockAsync() {
    return tryLockAsync(Thread.currentThread().getId());
}

@Override
public RFuture<Boolean> tryLockAsync(long threadId) {
    return tryAcquireOnceAsync(-1, -1, null, threadId);
}

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    RFuture<Boolean> acquiredFuture;
    // 续租时间:锁的过期时间(没有设置的话就用默认的 internalLockLeaseTime 看门狗时间)
    if (leaseTime > 0) {
        acquiredFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    } else {
        acquiredFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                                           TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }

    CompletionStage<Boolean> f = acquiredFuture.thenApply(acquired -> {
        // lock acquired
        if (acquired) {
            if (leaseTime > 0) {
                internalLockLeaseTime = unit.toMillis(leaseTime);
            } else {
                // 没配置过期时间就执行这里
                scheduleExpirationRenewal(threadId);
            }
        }
        return acquired;
    });
    return new CompletableFutureWrapper<>(f);
}

代码很长,主要看 tryLockInnerAsync 和 scheduleExpirationRenewal 方法。

前置知识

复制代码
// EVAL 命令,用于在 Redis 服务器端执行 Lua 脚本。
RedisStrictCommand<Boolean> EVAL_NULL_BOOLEAN = new RedisStrictCommand<Boolean>("EVAL", new BooleanNullReplayConvertor());

// BooleanNullReplayConvertor 判断是不是 NULL。
public class BooleanNullReplayConvertor implements Convertor<Boolean> {
    @Override
    public Boolean convert(Object obj) {        return obj == null;     }
}

tryLockInnerAsync

复制代码
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    // getRawName 即 锁的名称
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                          // 锁不存在,添加 hash 数据,可重入次数加一,毫秒级别过期时间,返回 null
                          "if (redis.call('exists', KEYS[1]) == 0) then " +
                          "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                          "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                          "return nil; " +
                          "end; " +
                          // 锁存在,可重入次数加一,毫秒级别过期时间,返回 null
                          "if (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; " +
                          // 锁被别人占有, 返回毫秒级别过期时间
                          "return redis.call('pttl', KEYS[1]);",
                          Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}

ARGV1 过期时间

ARGV2 即 getLockName(threadId) ,这里是 redisson 客户端id + 这个线程 ID , 如下 👇

scheduleExpirationRenewal (看门狗机制)

上面加锁完,就来到这段代码。

没有设置过期时间的话,默认给你设置 30 s 过期,并每隔 10s 自动续期,确保锁不会在使用过程中过期。

同时,防止客户端宕机,留下死锁。

复制代码
// RedissonBaseLock.java

protected 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);
        try {
            // 看这里 
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    // 延时任务,10s 续期一次。
    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;
            }
     
            // 续期操作
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getRawName() + " expiration", e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }

                if (res) {
                    // reschedule itself
                    renewExpiration();
                } else {
                    cancelExpirationRenewal(null);
                }
            });
        }
        // 三分之一时间,30s /3= 10s
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    ee.setTimeout(task);
}

// 续期脚本
protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), 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(getRawName()),
                          internalLockLeaseTime, getLockName(threadId));
}

get

上面的加锁操作,最终返回的是 return new CompletableFutureWrapper<>(f); 这个异步操作。

还记得上面的 BooleanNullReplayConvertor 吗,当 eval 执行加锁脚本时,成功会返回 null,并在这里转成 True 。

复制代码
@Override
public <V> V get(RFuture<V> future) {
    if (Thread.currentThread().getName().startsWith("redisson-netty")) {
        throw new IllegalStateException("Sync methods can't be invoked from async/rx/reactive listeners");
    }

    try {
        return future.toCompletableFuture().get();
    } catch (InterruptedException e) {
        future.cancel(true);
        Thread.currentThread().interrupt();
        throw new RedisException(e);
    } catch (ExecutionException e) {
        throw convertException(e);
    }
}

那么,加锁的部分到这里就结束, 解锁 的就简单过一下 👇

unlock 解锁

复制代码
// RedissonLock.java
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                          // 不存在,直接返回 null
                          "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                          "return nil;" +
                          "end; " +
                          // 减一
                          "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                          // 大于0,设置毫秒级过期时间,并返回0
                          "if (counter > 0) then " +
                          "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                          "return 0; " +
                          // 删除锁,并向指定channel发布 0 这个消息,并返回1
                          "else " +
                          "redis.call('del', KEYS[1]); " +
                          "redis.call('publish', KEYS[2], ARGV[1]); " +
                          "return 1; " +
                          "end; " +
                          // 返回 null
                          "return nil;",
                          Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

KEYS1 为锁名,KEYS2 channel 名 👇

ARGV1 为0 👇, ARGV2 过期时间,ARGV3 为 redisson 客户端id + 这个线程 ID

解锁后,取消续期任务。

结尾

通过源码,我们了解到上文提到的 redisson 框架的几个特点:自动续期 ,可重入锁 , lua脚本。

相关推荐
8Qi86 小时前
DDD架构演进
java·分布式·mvc·ddd
方方洛6 小时前
ray教程-23-附录C-术语表
人工智能·分布式·机器学习
海棠Flower未眠14 小时前
下单请求处理流程:商品维度分布式锁与 Redis 预扣库存
redis·分布式·junit
罗四强15 小时前
第10篇:打包上架:签名、打包 HAP、发布到华为应用市场
分布式·操作系统·harmonyos·arkts·鸿蒙系统
秋饼15 小时前
Redis 全面解析:从数据结构到主从/哨兵/集群、分布式锁与面试高频考点,代码到原理一次讲透
数据库·redis·分布式·面试·后端开发
二月夜15 小时前
Redis 高频面试题全解:缓存三大问题、大 key 治理与分布式锁之争(2026 招聘 JD 真题复盘)
redis·分布式·后端·缓存
樱花落木兰16 小时前
Redis 消息队列的可靠性演进:从 List、Pub/Sub 到 Stream 消费者组,以及 8.x 版本改变了什么
java·开发语言·经验分享·redis·分布式·python·软件工程
真上帝的左手18 小时前
12. 消息队列-Kafka进阶
分布式·中间件·kafka
Thanks_ks2 天前
Cookie 和 Session 到底是什么关系——登录态是怎么维持的
分布式·http·jwt·cookie·session·后端开发·登录态
Sayai2 天前
Kafka 跨数据中心日志同步实战:MirrorMaker 2 选型对比 + Flink 中继任务落地(双中心互备与消费者组隔离避坑)
大数据·运维·分布式·flink·kafka·消息队列·高可用