Redisson可重入锁(RLock)的使用与原理

Redisson可重入锁(RLock)的使用与原理

Redisson的可重入锁(RLock)是分布式环境下实现线程安全的核心组件,它基于Redis实现了Java java.util.concurrent.locks.Lock接口,支持与本地锁相似的语义,但具备跨进程的分布式特性。

一、核心特性

  1. 可重入性:同一线程可多次获取同一把锁而不会死锁,通过内部计数器实现。
  2. 自动续期(看门狗):锁默认持有30秒,业务未执行完时自动续期,防止锁提前释放。
  3. 公平锁支持 :通过getFairLock()获取公平锁,按请求顺序分配锁。
  4. 异步/同步支持 :提供同步(如lock())和异步(如lockAsync())接口。
  5. 联锁/红锁:支持将多个锁作为一个整体管理(如RedLock算法)。

二、使用示例

java 复制代码
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

public class RedissonLockExample {
    private static RedissonClient redisson;

    static {
        // 配置Redisson客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        redisson = Redisson.create(config);
    }

    // 1. 基础用法(推荐)
    public void basicUsage() {
        RLock lock = redisson.getLock("order:create:1001");
        try {
            // 尝试获取锁,最多等待10秒,持有锁30秒后自动释放
            boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (isLocked) {
                // 执行业务逻辑
                processOrder();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            // 确保释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    // 2. 可重入性演示
    public void reentrantDemo() {
        RLock lock = redisson.getLock("reentrant:demo");
        try {
            // 第一次获取锁
            lock.lock();
            System.out.println("第一次获取锁成功");

            // 同一线程可再次获取锁(可重入)
            lock.lock();
            System.out.println("第二次获取锁成功");

            // 执行业务逻辑
        } finally {
            // 需释放锁两次
            lock.unlock();
            System.out.println("第一次释放锁");
            lock.unlock();
            System.out.println("第二次释放锁");
        }
    }

    // 3. 公平锁用法
    public void fairLockDemo() {
        RLock fairLock = redisson.getFairLock("fair:lock");
        try {
            fairLock.lock();
            // 按请求顺序获取锁
            processWithFairness();
        } finally {
            fairLock.unlock();
        }
    }

    // 4. 异步锁用法(高并发场景)
    public CompletableFuture<Void> asyncLockDemo() {
        RLock lock = redisson.getLock("async:lock");
        return lock.tryLockAsync(10, 30, TimeUnit.SECONDS)
            .thenAccept(acquired -> {
                if (acquired) {
                    try {
                        // 异步执行逻辑
                        processAsync();
                    } finally {
                        lock.unlockAsync();
                    }
                }
            });
    }

    private void processOrder() {
        // 业务逻辑
    }

    private void processWithFairness() {
        // 公平锁保护的业务逻辑
    }

    private void processAsync() {
        // 异步业务逻辑
    }
}

三、底层实现原理

1. 数据结构

Redisson使用Redis的Hash结构存储锁信息:

  • Key :锁名称(如order:lock:1001
  • Field:线程唯一标识(UUID:ThreadId)
  • Value:重入次数计数器
2. 加锁流程

通过Lua脚本原子性执行以下逻辑:

lua 复制代码
-- KEYS[1]: 锁名称
-- ARGV[1]: 锁过期时间(默认30秒)
-- ARGV[2]: 线程标识(UUID:ThreadId)

-- 1. 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
    -- 2. 不存在则创建锁,初始重入次数为1
    redis.call('hset', KEYS[1], ARGV[2], 1);
    -- 3. 设置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;

-- 4. 锁已存在,检查是否被当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
    -- 5. 是当前线程持有,则重入次数+1
    redis.call('hincrby', KEYS[1], ARGV[2], 1);
    -- 6. 重置过期时间
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;

-- 7. 锁被其他线程持有,返回剩余过期时间
return redis.call('pttl', KEYS[1]);
3. 解锁流程

同样通过Lua脚本原子性执行:

lua 复制代码
-- KEYS[1]: 锁名称
-- ARGV[1]: 线程标识
-- ARGV[2]: 锁释放通知频道名称

-- 1. 检查锁是否存在且被当前线程持有
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
    return nil;
end;

-- 2. 重入次数-1
local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);

-- 3. 判断重入次数是否为0
if (counter > 0) then
    -- 4. 大于0则重置过期时间
    redis.call('pexpire', KEYS[1], ARGV[2]);
    return 0;
else
    -- 5. 等于0则删除锁
    redis.call('del', KEYS[1]);
    -- 6. 发布锁释放通知(用于唤醒等待线程)
    redis.call('publish', KEYS[2], ARGV[1]);
    return 1;
end;
4. 看门狗机制

当未指定锁过期时间时,Redisson会启动定时任务自动续期:

  1. 加锁成功后,启动一个每10秒执行一次的定时任务(默认看门狗超时时间30秒)。
  2. 任务通过Lua脚本检查锁是否存在且被当前线程持有,若是则重置过期时间为30秒。
  3. 解锁时取消定时任务,避免无限续期。

四、与本地锁(ReentrantLock)的对比

特性 ReentrantLock(本地) RLock(分布式)
跨进程支持
自动续期 ✅(看门狗机制)
Redis依赖
公平锁实现 基于AQS队列 基于Redis List队列
异常处理 需手动释放锁 自动续期,减少锁泄漏风险
性能 高(本地内存操作) 低(网络通信开销)

五、最佳实践

  1. 使用tryLock而非lock :避免线程无限阻塞(如tryLock(10, 30, TimeUnit.SECONDS))。
  2. 确保锁释放 :始终在finally块中调用unlock(),并检查isHeldByCurrentThread()
  3. 合理设置过期时间:预估业务执行时间,或依赖看门狗自动续期。
  4. 锁粒度最小化:只锁定关键资源(如订单ID),避免大范围锁定。
  5. 异常处理 :捕获InterruptedException并恢复中断状态。

六、注意事项

  1. Redis部署模式影响

    • 单节点:存在单点故障风险;
    • 主从:主节点故障时可能丢失锁信息;
    • 红锁(RedLock):需向多个独立节点加锁,提升可靠性。
  2. 长事务慎用:长时间持有锁会降低系统吞吐量,建议异步化处理。

  3. 集群环境时钟同步:若依赖Redis的过期时间,需确保集群节点时钟同步。

相关推荐
追逐时光者25 分钟前
面试第一步,先准备一份简洁、优雅的简历模板!
后端·面试
慕木兮人可34 分钟前
Docker部署MySQL镜像
spring boot·后端·mysql·docker·ecs服务器
发粪的屎壳郎42 分钟前
ASP.NET Core 8 轻松配置Serilog日志
后端·asp.net·serilog
倔强青铜三2 小时前
苦练Python第4天:Python变量与数据类型入门
前端·后端·python
倔强青铜三2 小时前
苦练Python第3天:Hello, World! + input()
前端·后端·python
倔强青铜三2 小时前
苦练Python第2天:安装 Python 与设置环境
前端·后端·python
Kookoos2 小时前
ABP VNext + .NET Minimal API:极简微服务快速开发
后端·微服务·架构·.net·abp vnext
倔强青铜三2 小时前
苦练Python第1天:为何要在2025年学习Python
前端·后端·python
LjQ20403 小时前
Java的一课一得
java·开发语言·后端·web
求知摆渡4 小时前
共享代码不是共享风险——公共库解耦的三种进化路径
java·后端·架构