Redisson框架是如何实现分布式锁的?

来源:MAC教程作者:小菜鸟头衔:草根站长
导读:本期聚焦于小菜鸟创作的《Redisson框架是如何实现分布式锁的?》,敬请观看详情。Redisson的分布式锁底层并不是简单地对Redis执行一条SETNX命令。它把锁抽象成一个Hash结构,字段是当前线程的唯一标识,值是重入次数。加锁时客户端向Redis发送一段Lua脚本,脚本先判断锁是否存在,不存在则通过hincrby写入线程标识并设置过期时间,存在且属于同一线程则递增计数,否则返回剩余存活时间。默认锁租约设为30秒,同时启动看门狗任务,每10秒检查并续期,避免业务执行时间超过租约造成锁提前释放。解锁时同样依赖Lua脚本校验线程标识,只有持有锁的线程才能删除对应的Hash字段,防止误删他人锁。基于这套机制,Redisson实现了可重入、自动续期和原子释放,配合公平锁、读写锁、红锁可以覆盖多数分布式并发场景。

Redisson实现分布式锁的方式,与很多手写SETNX加过期时间的方案有本质区别。它把锁对象映射为Redis中的一个Hash结构,键名是业务锁标识,Hash字段是当前持有锁的线程ID,字段值则是该线程的重入次数。加锁、续期、解锁操作全部通过Lua脚本发送到Redis执行,这样能保证在多线程并发下每个操作都是原子性的。

Redisson框架是如何实现分布式锁的?

如果不理解这套结构,很容易在使用Redisson时踩坑。比如只看过简单的setnx示例,就以为Redisson只是在setnx外面包了一层客户端API。实际上,Redisson的锁信息是放在Hash里,而不是一个普通的字符串键。Hash字段使用线程ID加上连接ID组成,避免不同JVM里的相同线程ID互相误判。只有理解了锁的存储形式和Lua脚本的作用,才能解释可重入、自动续期这些能力是怎么来的。

一、加锁与解锁的原子流程

Redisson默认通过一段Lua脚本来完成加锁。脚本会先判断目标锁对应的Hash键是否存在,如果不存在,表示当前没有线程持有锁,脚本就执行hincrby命令,将当前线程标识作为Hash字段写入,并把值设为1,同时设置锁的过期时间。如果锁已经存在,脚本会继续判断当前Hash字段是否就是请求线程自己,如果是,则说明同一个线程再次获取锁,也就是可重入场景,此时把Hash字段的值加一,并重新刷新过期时间。其他情况下脚本会直接返回锁的剩余存活时间,表示加锁失败。

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;
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]);

解锁同样不是简单地删除键,而是通过Lua脚本先校验Hash中是否存在当前线程的字段。如果存在,就把对应的计数值减一。减一后如果计数值仍然大于零,说明该线程还持有锁,只需要刷新过期时间,不需要删除整个Hash。只有当计数值归零时,脚本才会真正删除锁对应的Hash键。这样可以保证解锁操作只对自己持有的锁生效,不会把别的线程刚获取到的锁误删掉。

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]);
    return 0;
else
    redis.call('del', KEYS[1]);
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end;
return nil;

把加锁和解锁都放在Lua脚本里执行,是为了利用Redis单线程执行脚本的特性。脚本中的exists、hincrby、pexpire等命令会作为一个整体连续执行,中间不会被其他客户端命令插入。如果只是先执行setnx,再单独执行expire,一旦两个命令之间客户端崩溃,就可能留下一个永不过期的锁。Redisson从设计上就规避了这种经典问题。

二、可重入锁与看门狗续期机制

可重入是Redisson分布式锁的重要特性之一。同一个线程在已经持有锁的情况下,可以再次调用lock方法而不会死锁。实现原理正是基于前面提到的Hash结构:每次加锁时,同一个线程的Hash字段值会加一;每次解锁时,字段值减一,直到零才真正删除锁。这种设计让递归调用、嵌套方法调用或者同一个线程串行执行多个加锁段成为可能。相比之下,如果使用简单的setnx方案,第二次setnx会失败,如果没有额外处理,线程就会阻塞在自己持有的锁上。

RLock lock = redisson.getLock("order:pay:1001");
try {
    lock.lock();
    // 第一层业务逻辑
    processOrder();
    // 同一个线程再次获取同一把锁,不会阻塞
    lock.lock();
    try {
        updateStock();
    } finally {
        lock.unlock();
    }
} finally {
    lock.unlock();
}

看门狗机制解决的是锁租约与业务执行时间不匹配的问题。默认情况下,如果调用lock方法时没有显式指定leaseTime,Redisson会把锁的租约设置为30秒,同时启动一个后台任务,每隔10秒检查一次锁是否仍然被当前线程持有。如果持有,就把过期时间续期到30秒。这样即使业务逻辑执行超过30秒,锁也不会提前释放。只有当客户端进程崩溃或者连接断开时,看门狗无法继续续期,锁才会在到期后自动释放,避免死锁。

需要特别注意的是,看门狗只在没有手动指定leaseTime的情况下生效。一旦调用lock或tryLock时传入了leaseTime,Redisson会严格按照传入的时间设置过期时间,不会再启动自动续期任务。因此,如果业务执行时间无法预估,更适合使用不带leaseTime的lock方法,让看门狗动态续期。如果业务时间可以精确预估,手动指定leaseTime可以节省看门狗带来的少量Redis通信开销。

三、典型用法与参数调优

在实际项目中使用Redisson分布式锁,通常先配置RedissonClient,然后通过getLock方法获取锁对象。常见的Spring Boot集成方式是把RedissonClient交给容器管理,业务代码直接注入使用。下面的例子演示了带超时时间的tryLock用法,这种方式比无参lock更适合接口请求场景,因为可以避免线程无限等待锁释放。

@Autowired
private RedissonClient redissonClient;

public void handleRequest(String orderId) {
    RLock lock = redissonClient.getLock("order:handle:" + orderId);
    boolean acquired = false;
    try {
        acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
        if (!acquired) {
            throw new RuntimeException("获取锁超时");
        }
        // 执行业务逻辑
        doBusiness(orderId);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        if (acquired && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

tryLock的参数中,第一个参数表示等待获取锁的最长时间,第二个参数表示锁的自动释放时间,第三个参数是时间单位。上面的配置表示最多等待3秒拿锁,拿到后最多持有10秒。这里由于指定了leaseTime为10秒,所以看门狗不会参与续期,业务逻辑如果超过10秒,锁会自动释放。因此使用tryLock时,必须保证业务执行时间严格小于leaseTime,否则后面的线程可能拿到同一把锁,造成并发问题。

锁粒度和锁名称的设计也很关键。锁的粒度应该尽量小,只包住真正需要互斥的那段逻辑。例如订单支付场景下,不要对整个订单模块加一把大锁,而是针对具体订单号生成锁名,这样不同订单之间仍然可以并行处理。锁名最好带有业务前缀和唯一标识,例如order:pay:1001,这样在排查Redis键时可以快速定位。还要避免使用用户的手机号、身份证号等敏感信息作为锁名,防止敏感数据出现在Redis键空间中。

四、红锁和集群环境注意点

在Redis主从架构中,如果客户端向主节点成功加锁后,主节点还没来得及把锁数据同步给从节点就发生了宕机,此时从节点被提升为主节点,锁数据就丢失了。另一个客户端可能在新主节点上再次获取同一把锁,从而出现两个客户端同时持锁的情况。对于一致性要求极高的场景,单实例Redisson锁可能不够安全。为此Redisson提供了RedLock红锁实现,它要求客户端在多个互相独立的Redis节点上依次获取锁,只有超过半数节点加锁成功,才算真正持有锁。

RLock lock1 = redissonClient1.getLock("common:lock");
RLock lock2 = redissonClient2.getLock("common:lock");
RLock lock3 = redissonClient3.getLock("common:lock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
    if (redLock.tryLock(5, 30, TimeUnit.SECONDS)) {
        // 业务逻辑
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    redLock.unlock();
}

使用红锁时需要准备至少三个独立的Redis实例,最好部署在不同物理机或不同机架上,否则某个实例宕机可能导致多个节点同时不可用。红锁的加锁过程会在每个节点上尝试获取锁,并在规定时间内完成,如果成功节点数没有超过一半,就需要在已经加锁成功的节点上执行解锁回滚。红锁的引入会增加加锁耗时和Redis压力,因此只有在真正需要高一致性保证时才使用,普通业务场景下用单实例Redisson锁配合合理的租约和续期已经足够。

此外,在集群和容器化环境中,要特别注意时钟漂移和网络分区问题。如果多个Redis节点所在的服务器时间不同步,红锁依赖的过期时间判断就可能失效。客户端进程被GC暂停、网络抖动导致续期线程延迟,也可能让锁在业务尚未结束时过期释放。生产环境应当对看门狗续期任务的执行频率进行监控,并为核心业务设置合理的降级策略,例如获取锁失败时直接快速失败或者进入队列重试,而不是让所有请求都堆积在锁等待上。

Redisson分布式锁Redis修改时间:2026-09-18 04:01:45

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0918/58666.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。