Redisson分布式锁原理深入分析

 更新时间:2026年04月26日 10:28:13   作者:风吹迎面入袖凉  
文章介绍了Redisson分布式锁的工作原理,并详细解释了加锁、续期、解锁的核心流程,Redisson利用Redis的单线程、自动续期和Lua脚本原子性等特点,避免了传统锁常见的死锁、锁争用和误删等问题,此外,还总结了使用Redisson分布式锁时的注意事项和避坑点

一. Redisson是什么?

先澄清一个误区:Redisson不是新的中间件,它就是一个操作Redis的Java工具(客户端),基于Netty做的,速度很快。它的核心作用,就是把Redis里复杂的分布式锁操作,封装成了简单的Java代码——咱们不用记Redis的命令,不用写复杂的逻辑,调用它的API就能实现分布式锁,就像用本地的Lock一样简单。

Redisson里最核心的就是RLock这个接口,它和咱们本地用的Lock用法差不多,学起来特别简单。但它背后的逻辑,比咱们想的要严谨,这也是它能避免很多坑的原因。

这里贴一段咱们实际开发中最常用的Redisson锁代码,先有个直观感受:

// 1. 获取Redisson客户端(项目里一般是配置好的,直接注入)
RedissonClient redissonClient = Redisson.create();
// 2. 获取一把分布式锁(锁的名字自己定义,比如“order:lock:123”,唯一就行)
RLock lock = redissonClient.getLock("order:lock:123");
try {
    // 3. 加锁:默认30秒过期,也能自己设置,比如lock.lock(60, TimeUnit.SECONDS)
    lock.lock();
    // 4. 执行业务逻辑(比如修改订单状态、扣减库存,这部分是咱们自己的代码)
    doBusiness();
} finally {
    // 5. 解锁:必须放在finally里,防止业务报错,锁没释放
    lock.unlock();
}

就是这么简单!一行lock()加锁,一行unlock()解锁,剩下的底层逻辑,Redisson全帮咱们搞定了。接下来,咱们就扒一扒这两行代码背后,Redisson到底做了什么。

二. Redisson锁能干活,全靠Redis这3个本事

Redisson分布式锁,本质上是靠Redis实现的,没有Redis,它也玩不转。主要依赖Redis的3个核心能力:

  1. Redis是单线程干活:Redis同一时间只执行一个命令,不会出现两个命令同时执行的情况。这就天然保证了,同一时刻只有一个线程能抢到锁,不会出现“两个人同时抢到锁”的尴尬。
  2. 锁能自动过期:给锁设置一个过期时间(比如30秒),就算持有锁的线程崩溃了、网络断了,过了这个时间,锁会自动消失,不会一直占着资源,避免了“死锁”(锁一直没人放,其他线程都抢不到)。
  3. 一堆命令能一次性执行完:Redisson的加锁、解锁这些操作,都是用Lua脚本写的。Lua脚本能把多个Redis命令打包,要么全部执行成功,要么全部失败,不会出现“执行了一半卡住”的情况。比如“检查锁是否存在→创建锁”,这两步能一次性完成,避免了“两个线程同时检查到锁不存在,同时创建锁”的问题。

除此之外,Redisson还做了个优化:把Lua脚本缓存起来,不用每次都传输完整脚本,能节省时间、提升速度,尤其是在多台Redis组成的集群里,效果更明显。

三. 核心流程:加锁、续期、解锁

Redisson分布式锁的核心,就是“加锁→续期→解锁”这三步。

1. 加锁:怎么保证只有一个线程能抢到锁?

咱们平时调用的lock()方法,底层最终会调用RedissonLock类的lockInterruptibly()方法(核心加锁方法),再往下走,会调用tryAcquire()方法,尝试获取锁。这里贴tryAcquire()的核心源码:

// 核心加锁方法:尝试获取锁,leaseTime是过期时间,unit是时间单位
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    // 1. 如果设置了过期时间,直接调用tryLockInnerAsync(真正执行加锁的方法)
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 2. 如果没设置过期时间(用默认30秒),先尝试加锁
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    // 3. 加锁成功后,启动“看门狗”(续期用的),后面会讲
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e == null) {
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

// 真正执行加锁的方法:调用Lua脚本,和咱们之前讲的Lua逻辑一致
private <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisCommand<T> command) {
    // 转换过期时间为毫秒
    long ttl = unit.toMillis(leaseTime);
    // 返回Lua脚本的执行结果,这里就是调用Redis执行Lua脚本
    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return nil; " +
            "end; " +
            "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(getName()), ttl, getLockName(threadId));
}

逐行解读源码,不用懂复杂语法:

  • tryAcquireAsync方法:就是“尝试获取锁”的核心,接收三个参数——过期时间(leaseTime)、时间单位(unit)、当前线程ID(threadId)。

  • 第1步:如果咱们自己设置了过期时间(比如lock(60, TimeUnit.SECONDS)),就直接调用tryLockInnerAsync方法,真正去执行加锁。

  • 第2步:如果没设置过期时间,就用Redisson默认的30秒过期时间(getLockWatchdogTimeout()就是默认30秒),先尝试加锁。

  • 第3步:加锁成功后,启动“看门狗”(scheduleExpirationRenewal方法),作用是“续期”——防止业务没执行完,锁就过期了,后面会详细讲。

  • tryLockInnerAsync方法:真正执行加锁的逻辑,本质就是调用Redis执行咱们之前讲的Lua脚本,参数对应关系很简单: - KEYS[1]:锁的名字(getName()获取,就是咱们自己定义的“order:lock:123”); - ARGV[1]:过期时间(ttl,转换为毫秒); - ARGV[2]:线程身份证(getLockName(threadId),就是UUID+线程ID,比如“abc123:456”)。

这段源码其实就是把咱们之前讲的“加锁逻辑”,用Java代码实现了一遍,核心还是Lua脚本的原子性,保证只有一个线程能抢到锁。这里再强调3个关键设计,彻底解决咱们自己写锁的坑:

  • 不会抢乱:因为Lua脚本是一次性执行完的,不会出现“你检查锁不存在,正要创建,别人先创建了”的情况,保证了只有一个人能抢到锁。

  • 自己能多次抢锁(可重入):比如你的线程抢到锁后,又需要调用另一个需要同一把锁的方法,这时候不用等自己释放锁,直接再抢一次就行,计数会加1;解锁的时候,计数减1,直到计数为0,才真正把锁删掉——和本地锁的逻辑一样,很灵活。

  • 不会删别人的锁:每个线程都有自己的“身份证”(UUID+线程ID),只有持有锁的线程,才能操作这把锁,别人就算想删,也删不了,避免了误删别人锁的问题。

2. 续期:看门狗机制

很多人会有疑问:如果我的业务逻辑比较复杂,执行时间超过了锁的过期时间(比如默认30秒),怎么办?这时候锁会自动过期,当锁一过期的时候,就给了其他重试获取锁的线程可乘之机,它们会抢到锁执行自己的操作,导致数据错乱。

Redisson早就想到了这个问题,自带了“看门狗”机制(Watch Dog),核心作用就是:只要线程还持有锁,就会每隔一段时间(默认10秒),把锁的过期时间刷新回30秒,直到线程释放锁。

核心源码:

// 启动看门狗,续期用的
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    // 把当前线程的续期信息,存到本地缓存里
    EXPIRATION_RENEWAL_MAP.put(getEntryName(), entry);
    // 启动一个定时任务,每隔10秒执行一次,刷新锁的过期时间
    entry.task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 调用续期的Lua脚本,把锁的过期时间刷新回30秒
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    // 续期失败,移除本地缓存,停止续期
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                if (res) {
                    // 续期成功,继续启动下一个定时任务,循环续期
                    scheduleExpirationRenewal(threadId);
                } else {
                    // 续期失败,移除本地缓存
                    cancelExpirationRenewal(threadId);
                }
            });
        }
    }, getLockWatchdogTimeout() / 3, TimeUnit.MILLISECONDS);
}

// 续期的核心方法:调用Lua脚本,刷新过期时间
private RFuture<Boolean> renewExpirationAsync(long 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()), getLockWatchdogTimeout(), getLockName(threadId));
}

大白话解读看门狗机制:

  • 当咱们调用lock()方法(不设置过期时间)时,Redisson会自动启动看门狗,也就是scheduleExpirationRenewal方法。

  • 看门狗会启动一个定时任务,每隔10秒(默认30秒/3)执行一次,调用renewExpirationAsync方法,执行续期的Lua脚本。

  • 续期的Lua脚本逻辑很简单:检查当前锁是不是当前线程持有,如果是,就把锁的过期时间刷新回30秒,返回1(续期成功);如果不是,返回0(续期失败)。

  • 只要续期成功,就会继续启动下一个定时任务,循环续期;如果续期失败(比如锁已经被释放了),就停止续期,移除本地缓存。

  • 注意:如果咱们自己设置了过期时间(比如lock(60, TimeUnit.SECONDS)),Redisson不会启动看门狗,锁到期后会自动释放——因为你已经明确指定了锁的存活时间,Redisson默认你能把控业务执行时间。

3. 解锁 :怎么正确释放锁?

解锁的逻辑和加锁对应,核心是“只有持有锁的线程,才能释放锁”,而且要处理“可重入”的情况(计数减1,直到为0才删除锁)。咱们平时调用的unlock()方法,底层调用的是RedissonLock类的unlockAsync()方法:

// 核心解锁方法
public RFuture<Void> unlockAsync(long threadId) {
    // 调用解锁的Lua脚本,返回解锁结果
    RFuture<Boolean> future = unlockInnerAsync(threadId);
    future.onComplete((opStatus, e) -> {
        // 解锁成功后,停止看门狗续期
        cancelExpirationRenewal(threadId);
        if (e != null) {
            throw new CompletionException(e);
        }
        // 如果返回null,说明解锁失败(不是锁的持有者)
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + getNodeId() + " thread-id: " + threadId);
        }
    });
    return future;
}

// 真正执行解锁的方法:调用Lua脚本
private RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then " +
                "return nil; " +  // 不是锁的持有者,返回null,解锁失败
            "end; " +
            "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +  // 重入计数减1
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 计数还大于0,刷新过期时间
                "return 1; " +  // 解锁成功(只是计数减1,没删除锁)
            "else " +
                "redis.call('del', KEYS[1]); " +  // 计数为0,删除锁
                "return 1; " +  // 解锁成功(删除锁)
            "end;",
            Collections.singletonList(getName()), getLockWatchdogTimeout(), getLockName(threadId));
}

解锁源码:

  • unlockAsync方法:核心是调用unlockInnerAsync方法,执行解锁的Lua脚本,然后停止看门狗续期(cancelExpirationRenewal方法)。

  • 解锁的Lua脚本逻辑,分3步: 1. 先检查当前线程是不是锁的持有者(hexists判断),如果不是,返回null,解锁失败,抛出异常(比如你试图解锁别人的锁); 2. 如果是持有者,就把重入计数减1(hincrby -1); 3. 如果计数减1后还大于0(说明线程还在重入,没执行完所有业务),就刷新锁的过期时间,返回1(解锁成功,但没删除锁);如果计数为0(说明线程所有业务都执行完了),就删除锁(del命令),返回1(解锁成功,删除锁)。

  • 这里有个坑:一定要在finally里调用unlock()!如果业务逻辑报错,没执行到unlock(),锁就会一直被持有(虽然有看门狗续期,但线程崩溃后,看门狗也会停止,锁会过期释放,但会有延迟),可能导致其他线程一直抢不到锁。

四. Redisson分布式锁的避坑点

  • 必须在finally里解锁:不管业务逻辑有没有报错,都要释放锁,避免锁泄露(锁一直被持有,其他线程抢不到)。

  • 不要混用普通锁和红锁:如果用了红锁,就全程用红锁的API,不要和普通锁混用,否则会导致锁失效。

  • 设置合理的过期时间:如果自己设置过期时间,一定要比业务执行时间长,避免业务没执行完,锁就过期了;如果业务执行时间不确定,就用默认的30秒,依赖看门狗续期。

  • 避免锁的粒度太大:比如不要给整个“订单模块”加一把锁,应该给每个订单加一把锁(比如“order:lock:123”,123是订单ID),这样不同订单的线程可以同时执行,提升并发性能。

  • Redis集群要保证高可用:Redisson锁依赖Redis,所以Redis集群一定要做好高可用(比如主从复制、哨兵模式),避免Redis挂了,整个分布式锁失效。

五. 总结

Redisson分布式锁的核心逻辑:基于Redis的单线程、过期机制和Lua脚本原子性,封装了加锁、续期、解锁的逻辑,还提供了可重入、看门狗、红锁、公平锁、读写锁等实用功能,让我们能“开箱即用”。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • 浅谈Java实现分布式事务的三种方案

    浅谈Java实现分布式事务的三种方案

    现在互联网下,分布式和微服务横行,难免会遇到分布式下的事务问题,当然微服务下可能没有分布式事务,但是很多场景是需要分布式事务的。下面就来介绍下什么是分布式事务和分布式事务的解决方案
    2021-06-06
  • java字符串与日期类型转换的工具类

    java字符串与日期类型转换的工具类

    这篇文章主要为大家详细介绍了java字符串与日期类型转换的工具类,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-12-12
  • java 单例模式和工厂模式实例详解

    java 单例模式和工厂模式实例详解

    这篇文章主要介绍了Java设计模式编程中的单例模式和简单工厂模式以及实例,使用设计模式编写代码有利于团队协作时程序的维护,需要的朋友可以参考下
    2017-04-04
  • IntelliJ IDEA Tomcat控制台中文乱码问题的四种解决方案

    IntelliJ IDEA Tomcat控制台中文乱码问题的四种解决方案

    这篇文章主要给大家分享了4种方法完美解决IntelliJ IDEA Tomcat控制台中文乱码问题,文中有详细的图文介绍,对我们的学习或工作有一定的帮助,需要的朋友可以参考下
    2023-08-08
  • Java面向对象编程(封装/继承/多态)实例解析

    Java面向对象编程(封装/继承/多态)实例解析

    这篇文章主要介绍了Java面向对象编程(封装/继承/多态)实例解析的相关内容,具有一定参考价值,需要的朋友可以了解下。
    2017-10-10
  • 深入解析Java编程中的抽象类

    深入解析Java编程中的抽象类

    这篇文章主要介绍了Java编程中的抽象类,抽象类体现了Java面向对象编程的特性,需要的朋友可以参考下
    2015-10-10
  • java 中归并排序算法详解

    java 中归并排序算法详解

    这篇文章主要介绍了java 中归并排序算法详解的相关资料,归并排序算法又称为合并排序算法,是一种时间复杂度为O(N logN)的排序算法,因而其在平常生活工作中应用非常广泛,需要的朋友可以参考下
    2017-09-09
  • java中JDeps命令使用

    java中JDeps命令使用

    jdeps是一个Java类依赖分析工具,用于分析Java应用程序的依赖情况,包括类、包、模块以及JDK内部API的使用,本文就来详细的介绍一下,感兴趣的可以了解一下
    2024-09-09
  • Springboot接口日志加入链路追踪traceId方式

    Springboot接口日志加入链路追踪traceId方式

    文章介绍通过添加依赖、配置logback-spring.xml、使用@LogTrace注解和LogTraceAspect切面实现请求日志追踪,测试时日志中可显示traceId
    2025-08-08
  • SpringBootTest测试时不启动程序的问题

    SpringBootTest测试时不启动程序的问题

    这篇文章主要介绍了SpringBootTest测试时不启动程序的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01

最新评论