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的过期时间,需确保集群节点时钟同步。

相关推荐
苏三说技术1 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎2 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode2 小时前
Redis 在生产项目的使用
前端·后端
用户559822481222 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode2 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战2 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha2 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn2 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425912 小时前
ShardingJDBC
后端
行者全栈架构师2 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端