Redisson 分布式锁底层原理实现详解

1、概述

Redisson 是基于 Redis 实现的分布式锁,其核心思想是利用 Redis 的 SET NX(SET if Not eXists)+ PX(过期时间) 来实现锁的互斥性 ,同时通过 Lua 脚本 处理加锁、解锁、续期等原子操作,保证分布式环境下的安全性。

2、主要过程

2.1、加锁(tryLock)

定义: 通过 SET NX 实现锁的互斥功能。

  • 命令: SET key value NX PX expireTime
  • 作用: 使用 SETNX 保证互斥性,使用 PX 设置过期时间防止死锁
  • 数据结构:
    • key: lock:{name},如 lock:order:123
    • value: 存储唯一标识(UUID + 线程ID),用于区分不同客户端的锁
  • 特点:
    如果 key 不存在,创建并返回 OK,表示加锁成功。
    如果 key 存在,返回 nil,表示加锁失败。

2.2、锁续约(Watchdog 机制)

定义:在tryLock未设置 leaseTime 时候会启动看门狗机制,定期(默认 10s)延长30s锁的过期时间。

实现方式:

Redisson 在获取RFuture时候,如果获取锁成功,就会执行scheduleExpirationRenewal()方法,执行对应的代码


scheduleExpirationRenewal代码如下:

java 复制代码
// 续期锁的过期时间
private void renewExpiration() {
    // 从过期续期映射中获取锁的过期条目
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return; // 如果找不到条目,说明没有锁需要续期
    }

    // 创建一个定时任务,用于定期续期锁的过期时间
    Timeout task = commandExecutor.getServiceManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 重新获取过期条目
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return; // 如果条目已被移除,结束任务
            }
            // 获取持有锁的线程 ID
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return; // 如果没有线程 ID,说明没有线程持有该锁,结束任务
            }

            // 异步续期锁的过期时间
            CompletionStage<Boolean> future = renewExpirationAsync(threadId);
            future.whenComplete((res, e) -> {
                if (e != null) {
                    // 如果续期过程中发生错误,记录日志并移除续期条目
                    log.error("Can't update lock {} expiration", getRawName(), e);
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }

                if (res) {
                    // 如果续期成功,重新调度续期任务
                    renewExpiration();
                } else {
                    // 如果续期失败,取消续期操作
                    cancelExpirationRenewal(null);
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS); // 定时任务每 internalLockLeaseTime / 3 毫秒执行一次
    
    // 设置定时任务到过期条目中
    ee.setTimeout(task);
}

// 启动续期操作,首次获取锁时会调用此方法
protected void scheduleExpirationRenewal(long threadId) {
    // 创建新的过期条目
    ExpirationEntry entry = new ExpirationEntry();
    // 尝试将新的条目加入到续期映射中
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        // 如果条目已存在,说明已有其他线程在续期,添加当前线程 ID 到条目中
        oldEntry.addThreadId(threadId);
    } else {
        // 如果是首次添加,开始进行续期操作
        entry.addThreadId(threadId);
        try {
            // 启动锁过期续期任务
            renewExpiration();
        } finally {
            // 如果当前线程被中断,取消续期操作
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

2.3、解锁(unlock)

命令: 通过Lua 脚本确保原子性

2.4、可重入性

实现方式: Redisson 维护一个 可重入计数器**(Hash 结构)**,如果同一线程再次获取锁,计数器递增。

存储结构:

key: lock:{name}

value: UUID:threadId 作为唯一标识

hash 结构(可重入计数器)

lua 复制代码
HINCRBY lock:{name} UUID:threadId 1

解锁时,只有计数器 == 0,才真正删除 Redis 锁

2.5、失败重试

策略:

线程获取锁失败时,不会立即放弃,而是进入 自旋等待。

主要机制:

通过计算计算减去锁消耗时间得到的锁最大等待时间,如果 >0 执行对应的订阅功能,订阅其他获取锁的信号,如果其他线程释放锁会发布信号,收到订阅后再执行重试获取锁机制,,等待订阅最大时间是 waitime ,从而不是直接进行循环,导致性能的损耗,最后进行 while(true) 直至获取锁成功为止。

相关推荐
码农小河6611 小时前
基于SSM+JSP的高校专业信息管理系统【附源码+数据库】
java·开发语言·数据库
风若飞13 小时前
Spring Ai 入门遇到问题1
java·后端·spring
Nuanyt13 小时前
LeetCode Hot 100 链表专题(五)· 2 题(Java)
java·算法·leetcode·链表
黑马程序员毕设14 小时前
基于 Java 的餐饮点餐系统设计与应用报告
java·开发语言
SL_staff14 小时前
从文档工具到知识中枢:基于JVS数字底座构建企业级统一内容管理平台的技术实践
java·开源·全栈
SL_staff14 小时前
从‘连得上’到‘管得好’:JVS-IOT双引擎驱动设备运维闭环
java·物联网·产品
w_zero_one16 小时前
递归算法(一)
java·开发语言·算法
SL_staff17 小时前
私有化低代码不是减配版开发:JVS双模式建模如何重建IT交付信任
java·低代码·spring cloud
墨着染霜华17 小时前
Redocly 生成 OpenAPI 接口文档踩坑实录|Node 版本兼容 + 接口地址错误问题解决
java
文丑颜不良啊17 小时前
LeetCode1881~1990题目解析
java·leetcode