Redisson 分布式锁深度解析:API 使用与底层源码探秘
在分布式系统中,并发控制是永恒的话题。单机环境下的synchronized或Lock接口能轻松解决线程安全问题,但在多节点、跨进程的分布式场景中,传统锁机制完全失效。基于 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()方法时,最终会执行RedissonLock的lock(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 脚本逻辑解析:
- 若锁(KEYS [1])不存在:通过
hincrby创建 Hash 结构的锁,field为线程标识,value为 1(重入次数),并设置过期时间,返回nil(表示获取成功)。 - 若锁已存在且当前线程持有该锁:将重入次数 + 1,重置过期时间,返回
nil(表示重入成功)。 - 若锁已存在且被其他线程持有:返回锁的剩余过期时间(
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 脚本逻辑解析:
- 若当前线程未持有锁,返回
nil,触发IllegalMonitorStateException。 - 若当前线程持有锁,将重入次数 - 1。
- 若重入次数 > 0:重置过期时间,返回 0(表示重入锁未完全释放)。
- 若重入次数 = 0:删除锁,发布锁释放事件,返回 1(表示锁完全释放)。
四、Redisson 分布式锁最佳实践
- 必须在 finally 中释放锁:避免业务异常导致锁无法释放,引发死锁。
- 避免手动指定过期时间过短:若业务耗时超过过期时间,锁会提前释放,导致并发问题;建议使用无参
lock(),依赖看门狗自动续期。 - 禁止跨线程释放锁:仅能由持有锁的线程调用
unlock(),可通过isHeldByCurrentThread()做前置判断。 - 合理选择锁类型:读多写少用读写锁,避免饥饿用公平锁,集群场景用红锁。
- 设置锁的等待时间:使用
tryLock指定等待时间,避免线程无限阻塞。
五、总结
Redisson 作为 Redis 的高级 Java 客户端,将分布式锁的实现封装得极为优雅,解决了原生 Redis 分布式锁的原子性、重入性、超时续期等痛点。其核心RLock通过 Lua 脚本保证操作原子性,通过 Hash 结构实现可重入,通过看门狗机制解决锁超时问题,通过订阅 / 发布机制实现锁的阻塞等待。
在实际开发中,我们无需重复造轮子,只需根据业务场景选择合适的锁类型,遵循最佳实践即可。而深入理解其底层源码,能帮助我们更好地排查问题、优化性能,让分布式锁的使用更安全、更高效。