Redisson 分布式锁深度解析:API 使用与底层源码探秘

简介: 本文深入解析Redisson分布式锁的使用与源码实现,涵盖可重入锁、公平锁、读写锁、红锁等核心API的应用场景与配置方法,并通过Lua脚本、Hash结构和看门狗机制剖析其原子性、重入性与自动续期原理,助力开发者高效安全地实现分布式并发控制。

Redisson 分布式锁深度解析:API 使用与底层源码探秘

在分布式系统中,并发控制是永恒的话题。单机环境下的synchronizedLock接口能轻松解决线程安全问题,但在多节点、跨进程的分布式场景中,传统锁机制完全失效。基于 Redis 的分布式锁是主流解决方案之一,而Redisson作为 Redis 的 Java 客户端,不仅封装了原生 Redis 的分布式锁实现,还解决了原子性、重入性、超时续期等痛点,提供了丰富的锁类型和简洁的 API。

本文将从实际应用出发,详细讲解 Redisson 分布式锁核心 API 的使用方法,并深入剖析RLock的底层源码,带你理解 Redisson 分布式锁的实现原理。

一、Redisson 环境搭建

在使用 Redisson 分布式锁前,需先完成基础环境的搭建。我们以 Spring Boot 项目为例,演示快速集成 Redisson 的步骤。

1.1 引入 Maven 依赖

pom.xml中添加 Redisson 的 Spring Boot Starter 依赖(推荐使用官方适配版本,避免 Redis 版本冲突):

<!-- Redisson Spring Boot Starter -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.5</version>
</dependency>

<!-- Redis客户端依赖(若已引入可省略) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

1.2 配置 Redisson 客户端

application.yml中配置 Redis 连接信息(以单机模式为例,集群 / 哨兵模式可参考官方文档):

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 0

# Redisson配置(可选,默认使用Spring Redis配置)
redisson:
  singleServerConfig:
    address: redis://127.0.0.1:6379
    password: 123456
    database: 0

1.3 注入 RedissonClient

Redisson 的 Starter 会自动创建RedissonClient实例,直接通过@Autowired注入即可使用:

import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class RedissonLockDemo {
   

    @Autowired
    private RedissonClient redissonClient;

}

二、Redisson 分布式锁核心 API 使用

Redisson 提供了多种分布式锁实现,包括可重入锁、公平锁、读写锁、联锁、红锁等,满足不同业务场景的需求。其中,RLock(可重入锁)是最基础、最常用的锁类型,其他锁均基于其扩展。

2.1 可重入锁(RLock)

可重入锁是指同一个线程可以多次获取同一把锁,不会导致死锁。Redisson 的RLock实现了 Java 的java.util.concurrent.locks.Lock接口,使用方式与原生ReentrantLock几乎一致。

基本使用示例

import org.redisson.api.RLock;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class ReentrantLockService {
   

    @Autowired
    private RedissonClient redissonClient;

    // 定义锁名称
    private static final String LOCK_KEY = "distributed:lock:demo";

    public void doBusiness() {
   
        // 1. 获取锁实例
        RLock lock = redissonClient.getLock(LOCK_KEY);

        try {
   
            // 2. 获取锁:支持超时自动释放、可中断
            // lock.lock(); // 无参:默认30秒过期,看门狗自动续期
            // lock.lock(10, TimeUnit.SECONDS); // 指定过期时间,无看门狗
            boolean isLocked = lock.tryLock(3, 10, TimeUnit.SECONDS); // 尝试获取锁,3秒等待,10秒过期

            if (isLocked) {
   
                // 3. 执行业务逻辑(分布式并发控制)
                System.out.println("获取锁成功,执行核心业务...");
                Thread.sleep(5000); // 模拟业务耗时
            } else {
   
                System.out.println("获取锁失败,业务执行中断");
            }
        } catch (InterruptedException e) {
   
            Thread.currentThread().interrupt();
            throw new RuntimeException("线程被中断", e);
        } finally {
   
            // 4. 释放锁:必须在finally中执行,避免死锁
            if (lock.isHeldByCurrentThread()) {
   
                lock.unlock();
                System.out.println("释放锁成功");
            }
        }
    }
}

关键方法说明

  • lock():无参获取锁,默认过期时间 30 秒,看门狗(Watch Dog) 会自动续期,直到锁被释放。
  • lock(long leaseTime, TimeUnit unit):指定锁的过期时间,此时看门狗失效,锁到期后自动释放。
  • tryLock(long waitTime, long leaseTime, TimeUnit unit):尝试获取锁,最多等待waitTime,获取成功后锁有效期为leaseTime,超时未获取则返回false
  • unlock():释放锁,必须由持有锁的线程调用,否则会抛出IllegalMonitorStateException
  • isHeldByCurrentThread():判断当前线程是否持有该锁,避免误释放其他线程的锁。

2.2 公平锁(RFairLock)

公平锁保证多个线程按请求顺序获取锁,避免 “饥饿” 问题。Redisson 的RFairLock实现了公平锁机制,使用方式与RLock一致,仅需通过getFairLock获取锁实例:

public void fairLockDemo() {
   
    // 获取公平锁实例
    RLock fairLock = redissonClient.getFairLock("distributed:fair:lock");

    try {
   
        fairLock.lock(10, TimeUnit.SECONDS);
        System.out.println("公平锁获取成功,执行业务...");
    } finally {
   
        if (fairLock.isHeldByCurrentThread()) {
   
            fairLock.unlock();
        }
    }
}

2.3 读写锁(RReadWriteLock)

读写锁适用于读多写少的场景,遵循 “读共享、写独占” 原则:

  • 读锁:多个线程可同时获取,写锁被阻塞。
  • 写锁:仅一个线程可获取,读锁和其他写锁均被阻塞。

使用示例:

public void readWriteLockDemo() {
   
    // 获取读写锁实例
    RReadWriteLock rwLock = redissonClient.getReadWriteLock("distributed:rw:lock");
    RLock readLock = rwLock.readLock();
    RLock writeLock = rwLock.writeLock();

    // 读锁使用
    try {
   
        readLock.lock(5, TimeUnit.SECONDS);
        System.out.println("读锁获取成功,执行读操作...");
    } finally {
   
        if (readLock.isHeldByCurrentThread()) {
   
            readLock.unlock();
        }
    }

    // 写锁使用
    try {
   
        writeLock.lock(5, TimeUnit.SECONDS);
        System.out.println("写锁获取成功,执行写操作...");
    } finally {
   
        if (writeLock.isHeldByCurrentThread()) {
   
            writeLock.unlock();
        }
    }
}

2.4 红锁(RedissonRedLock)

红锁适用于 Redis 集群主从切换的场景,解决单主节点宕机导致的锁失效问题。红锁通过同时向多个独立的 Redis 节点(至少 3 个)申请锁,只有超过半数节点获取成功,才认为锁获取成功。

使用示例:

public void redLockDemo() {
   
    // 连接多个独立的Redis节点
    RLock lock1 = redissonClient.getLock("redlock:1");
    RLock lock2 = redissonClient.getLock("redlock:2");
    RLock lock3 = redissonClient.getLock("redlock:3");

    // 创建红锁实例
    RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);

    try {
   
        boolean isLocked = redLock.tryLock(3, 10, TimeUnit.SECONDS);
        if (isLocked) {
   
            System.out.println("红锁获取成功,执行核心业务...");
        }
    } catch (InterruptedException e) {
   
        Thread.currentThread().interrupt();
    } finally {
   
        if (redLock.isHeldByCurrentThread()) {
   
            redLock.unlock();
        }
    }
}

三、RLock 核心源码解析

Redisson 的分布式锁核心是RLock,其底层基于 Redis 的Lua 脚本保证操作的原子性,通过Hash 结构实现可重入,通过定时任务实现看门狗续期。本节将从lock()方法入手,深入剖析RLock的实现原理。

3.1 RLock 的核心实现类

Redisson 的RLock接口由RedissonLock类实现,该类继承自RedissonBaseLock,核心依赖:

  • commandExecutor:Redis 命令执行器,负责发送 Lua 脚本和 Redis 命令。
  • id:当前 Redisson 客户端的唯一标识(格式:客户端ID:线程ID),用于区分不同线程。
  • internalLockLeaseTime:锁的默认过期时间(30 秒),即看门狗的基础续期时间。

3.2 获取锁的核心流程(lock () 方法)

调用lock()方法时,最终会执行RedissonLocklock(long leaseTime, TimeUnit unit, boolean interruptibly)方法,核心逻辑如下:

步骤 1:入口方法

@Override
public void lock(long leaseTime, TimeUnit unit) {
   
    try {
   
        lock(leaseTime, unit, false);
    } catch (InterruptedException e) {
   
        Thread.currentThread().interrupt();
        return;
    }
}

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
   
    long threadId = Thread.currentThread().getId();
    // 1. 尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // 2. 获取成功:直接返回
    if (ttl == null) {
   
        return;
    }
    // 3. 获取失败:订阅锁释放事件,阻塞等待
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    if (interruptibly) {
   
        commandExecutor.syncSubscriptionInterrupted(future);
    } else {
   
        commandExecutor.syncSubscription(future);
    }

    try {
   
        // 4. 循环重试获取锁
        while (true) {
   
            ttl = tryAcquire(leaseTime, unit, threadId);
            if (ttl == null) {
   
                break;
            }
            // 5. 等待锁释放(根据ttl休眠)
            if (ttl > 0) {
   
                try {
   
                    future.getNow().getLatch().await(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
   
                    if (interruptibly) {
   
                        throw e;
                    }
                    Thread.currentThread().interrupt();
                }
            } else {
   
                if (interruptibly) {
   
                    LockSupport.parkNanos(100000);
                } else {
   
                    LockSupport.parkNanos(100000);
                }
            }
        }
    } finally {
   
        // 6. 取消订阅
        unsubscribe(future, threadId);
    }
}

步骤 2:尝试获取锁(tryAcquire 方法)

tryAcquire是获取锁的核心方法,返回锁的剩余过期时间(null表示获取成功),其内部调用tryAcquireAsync实现异步获取:

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
   
    // 1. 指定了过期时间:直接获取锁
    if (leaseTime != -1) {
   
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 2. 未指定过期时间:启动看门狗,默认30秒过期
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
   
        if (e != null) {
   
            return;
        }
        // 3. 获取锁成功,启动看门狗续期
        if (ttlRemaining == null) {
   
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

步骤 3:Lua 脚本实现原子性(tryLockInnerAsync 方法)

tryLockInnerAsync是获取锁的最终实现,通过Lua 脚本保证 “判断锁是否存在→设置锁→设置过期时间” 的原子性:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisCommand<T> command) {
   
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
            // Lua脚本核心逻辑
            "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]);",
            Collections.singletonList(getName()), // KEYS[1]:锁名称
            internalLockLeaseTime, // ARGV[1]:锁过期时间(毫秒)
            getLockName(threadId) // ARGV[2]:线程标识(客户端ID:线程ID)
    );
}

Lua 脚本逻辑解析

  1. 若锁(KEYS [1])不存在:通过hincrby创建 Hash 结构的锁,field为线程标识,value为 1(重入次数),并设置过期时间,返回nil(表示获取成功)。
  2. 若锁已存在且当前线程持有该锁:将重入次数 + 1,重置过期时间,返回nil(表示重入成功)。
  3. 若锁已存在且被其他线程持有:返回锁的剩余过期时间(pttl),表示获取失败。

可重入实现原理:通过 Redis 的 Hash 结构存储锁,field为线程标识,value为重入次数,同一线程再次获取锁时仅需递增value

步骤 4:看门狗续期(scheduleExpirationRenewal 方法)

当未指定锁的过期时间时,Redisson 会启动看门狗定时任务,每隔internalLockLeaseTime / 3(10 秒)续期一次,将锁的过期时间重置为 30 秒:

private void scheduleExpirationRenewal(long threadId) {
   
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
   
        oldEntry.addThreadId(threadId);
    } else {
   
        entry.addThreadId(threadId);
        // 启动续期任务
        renewExpiration();
    }
}

private void renewExpiration() {
   
    ExpirationEntry entry = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (entry == null) {
   
        return;
    }
    // 定时任务:每隔10秒执行一次
    Timeout task = commandExecutor.getConnectionManager().newTimeout(timerTask -> {
   
        ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
        if (ent == null) {
   
            return;
        }
        Long threadId = ent.getFirstThreadId();
        if (threadId == null) {
   
            return;
        }
        // 执行续期Lua脚本
        RFuture<Boolean> future = renewExpirationAsync(threadId);
        future.onComplete((res, e) -> {
   
            if (e != null) {
   
                log.error("Can't update lock " + getName() + " expiration", e);
                return;
            }
            if (res) {
   
                // 续期成功,递归调用自身,继续续期
                renewExpiration();
            }
        });
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    entry.setTimeout(task);
}

看门狗续期的 Lua 脚本(renewExpirationAsync 方法):

"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
    "return 1; " +
"end; " +
"return 0;"

仅当当前线程持有锁时,才会重置过期时间,避免为其他线程的锁续期。

3.3 释放锁的核心流程(unlock () 方法)

释放锁的核心是unlockAsync方法,同样通过 Lua 脚本保证原子性,逻辑如下:

@Override
public RFuture<Void> unlockAsync(long threadId) {
   
    RPromise<Void> result = new RedissonPromise<>();
    // 执行释放锁的Lua脚本
    RFuture<Boolean> future = unlockInnerAsync(threadId);

    future.onComplete((opStatus, e) -> {
   
        // 取消看门狗续期任务
        cancelExpirationRenewal(threadId);

        if (e != null) {
   
            result.tryFailure(e);
            return;
        }
        // 锁未被当前线程持有,抛出异常
        if (opStatus == null) {
   
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }
        result.trySuccess(null);
    });
    return result;
}

释放锁的 Lua 脚本(unlockInnerAsync 方法):

"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 脚本逻辑解析

  1. 若当前线程未持有锁,返回nil,触发IllegalMonitorStateException
  2. 若当前线程持有锁,将重入次数 - 1。
  3. 若重入次数 > 0:重置过期时间,返回 0(表示重入锁未完全释放)。
  4. 若重入次数 = 0:删除锁,发布锁释放事件,返回 1(表示锁完全释放)。

四、Redisson 分布式锁最佳实践

  1. 必须在 finally 中释放锁:避免业务异常导致锁无法释放,引发死锁。
  2. 避免手动指定过期时间过短:若业务耗时超过过期时间,锁会提前释放,导致并发问题;建议使用无参lock(),依赖看门狗自动续期。
  3. 禁止跨线程释放锁:仅能由持有锁的线程调用unlock(),可通过isHeldByCurrentThread()做前置判断。
  4. 合理选择锁类型:读多写少用读写锁,避免饥饿用公平锁,集群场景用红锁。
  5. 设置锁的等待时间:使用tryLock指定等待时间,避免线程无限阻塞。

五、总结

Redisson 作为 Redis 的高级 Java 客户端,将分布式锁的实现封装得极为优雅,解决了原生 Redis 分布式锁的原子性、重入性、超时续期等痛点。其核心RLock通过 Lua 脚本保证操作原子性,通过 Hash 结构实现可重入,通过看门狗机制解决锁超时问题,通过订阅 / 发布机制实现锁的阻塞等待。

在实际开发中,我们无需重复造轮子,只需根据业务场景选择合适的锁类型,遵循最佳实践即可。而深入理解其底层源码,能帮助我们更好地排查问题、优化性能,让分布式锁的使用更安全、更高效。

目录
相关文章
|
3月前
|
JSON 安全 Java
JDK 21 字符串拼接最佳实践:场景化选择最优方案
JDK 21 字符串拼接需按场景选择最优方案:静态拼接用`+`,编译器自动优化;单线程动态拼接优选`StringBuilder`;格式化模板结合`formatted()`与文本块,提升可读性;集合拼接用`String.join()`或Stream;多线程场景选`StringBuffer`保障安全。
295 8
|
3月前
|
Java Spring
IDEA调出services窗口
本教程分两步指导:首先点击指定选项,然后在Templates中添加Spring Boot并应用,即可调出services窗口,快速完成配置。
200 11
|
3月前
|
存储 缓存 Java
重构一个类,JVM竟省下2.9G内存?
通过重构核心类,将 `HashMap<Long, HashSet<String>>` 优化为 `Long2ObjectOpenHashMap<int[]>`,结合数据分布特征与紧凑存储,JVM 堆内存从 3.13GB 降至 211MB,降幅达 94%,验证了高效数据结构在海量场景下的巨大价值。
384 24
重构一个类,JVM竟省下2.9G内存?
|
2月前
|
存储 自然语言处理 测试技术
一行代码,让 Elasticsearch 集群瞬间雪崩——5000W 数据压测下的性能避坑全攻略
本文深入剖析 Elasticsearch 中模糊查询的三大陷阱及性能优化方案。通过5000 万级数据量下做了高压测试,用真实数据复刻事故现场,助力开发者规避“查询雪崩”,为您的业务保驾护航。
1505 89
|
3月前
|
XML Java 数据库连接
MyBatis Plus 核心功能与用法
MyBatis Plus 是基于 MyBatis 的增强工具,简化单表 CRUD 操作。通过继承 BaseMapper 即可实现增删改查,支持条件构造器、分页、逻辑删除、自动填充、乐观锁等高级功能,大幅提升开发效率,无需编写冗余 SQL 与 XML,助力快速构建 Spring Boot 应用。
264 0
|
3月前
|
消息中间件 存储 负载均衡
【高可用】什么是异地多活、同城容灾?
异地多活与同城容灾均为提升系统高可用的分布式架构。前者实现跨地域数据中心实时同步与故障切换,保障全球服务连续性;后者聚焦同城内快速容灾,通过高速网络实现低延迟、高可靠的数据同步与负载均衡,适用于对延迟敏感的业务场景。
173 11
|
3月前
|
缓存 前端开发 Java
深入理解 Java 类加载器:双亲委派机制的前世今生与源码解析
本文深入解析Java类加载器与双亲委派机制,从Bootstrap到自定义加载器,剖析loadClass源码,揭示类加载的线程安全、缓存机制与委派逻辑,并探讨SPI、Tomcat、OSGi等场景下打破双亲委派的原理与实践价值。(238字)
391 8
深入理解 Java 类加载器:双亲委派机制的前世今生与源码解析
|
3月前
|
Java 程序员 持续交付
Git 从入门到进阶:常用命令与高级用法全解析
本文系统梳理Git常用命令与高级技巧,涵盖初始化、分支管理、变基、储藏、标签、差异对比、二分查找及reflog等核心功能,结合最佳实践与避坑指南,助你从入门到精通,提升代码管理与团队协作效率。
456 74
|
3月前
|
人工智能 分布式计算 算法
AI战略丨大模型应用元年,让智能真正转变为生产力
在大模型能力用到企业、业务的探索过程中,阿里云提供全方位的支持,企业可以放开去尝 试各种不同的路径,共同探索智能未来。
AI战略丨大模型应用元年,让智能真正转变为生产力
|
4月前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
431 0

热门文章

最新文章