Redisson 分布式锁:使用与实现原理
一、Redisson 简介
Redisson 是基于 Redis 的 Java 客户端,提供了分布式锁、分布式集合、信号量等功能。
相比手动使用 SET NX EX 实现分布式锁,Redisson 解决或改善了以下问题:
可重入:同一个线程可以重复获取同一把锁。
可重试:获取锁失败后,可以等待并重新尝试。
自动续期:通过 Watch Dog(看门狗)机制,避免正常业务执行时间超过锁的过期时间。
安全释放:只有持有锁的线程才能释放锁。
原子性:通过 Lua 脚本保证加锁、解锁等操作的原子性。
注意:Redisson 无法彻底解决 Redis 主从切换时可能发生的锁丢失问题。
二、引入 Redisson
Redisson 不属于 Java 8 自带的依赖,需要通过 Maven 引入。
1. Maven 依赖
以下使用兼容 Java 8 的 Redisson 3.x 示例版本:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.27.2</version>
</dependency>2. 配置客户端
@Configuration
public class RedissonConfig {
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
// 看门狗默认超时时间为30秒
config.setLockWatchdogTimeout(30000);
return Redisson.create(config);
}
}如果项目已经使用 spring-boot-starter-data-redis,仍然可以引入 Redisson,但需要注意两者的客户端及连接资源是独立管理的。
三、Redisson 加锁与释放锁
1. 推荐使用 tryLock()
public void createOrder(Long userId) {
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean acquired = false;
try {
// 最多等待3秒获取锁
acquired = lock.tryLock(3, TimeUnit.SECONDS);
if (!acquired) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 执行业务逻辑
System.out.println("创建订单成功");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取锁被中断", e);
} finally {
if (acquired && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}说明:
getLock():获取锁对象,不会立即向 Redis 加锁。tryLock():尝试获取锁。unlock():释放当前线程持有的锁。finally:确保业务结束后执行解锁逻辑。
推荐使用带等待时间的 tryLock(),避免获取锁失败时无限等待。
2. tryLock() 三种方法的区别
第一种:立即尝试获取锁
boolean success = lock.tryLock();获取失败立即返回 false,不会等待锁释放。
第二种:等待指定时间获取锁
boolean success = lock.tryLock(3, TimeUnit.SECONDS);最多等待3秒,获取成功后由看门狗自动续期。
第三种:等待指定时间,并设置锁的固定过期时间
boolean success = lock.tryLock(
3,
30,
TimeUnit.SECONDS
);waitTime = 3:最多等待3秒获取锁。leaseTime = 30:获取成功后,锁的有效期为30秒。
指定 leaseTime 后,看门狗不会自动续期。即使业务没有执行完成,锁也可能在30秒后自动释放。
总结:
tryLock():不等待,获取不到直接返回。tryLock(waitTime, unit):等待指定时间,支持自动续期。tryLock(waitTime, leaseTime, unit):等待指定时间,锁按固定时间过期,不自动续期。
四、Redisson 的加锁流程
假设线程A和线程B同时申请同一把锁。
线程A申请锁
│
▼
Redis判断锁是否存在
│
├── 不存在 → 创建锁 → 获取成功
│ │
│ ▼
│ 执行业务
│ │
│ ▼
│ 释放锁并通知
│
线程B申请锁
│
▼
发现锁已被线程A持有
│
▼
获取失败,订阅释放通知
│
▼
等待通知或超时后重新尝试
│
▼
重新竞争锁
│
▼
获取成功后执行业务具体流程:
线程A调用
getLock()获取锁对象。调用
lock()或tryLock(),Redisson 通过 Lua 脚本尝试加锁。Redis 中锁不存在,创建锁并设置 TTL,线程A获取成功。
线程B申请同一把锁,由于已被A持有,获取失败。
线程B通过 Redis Pub/Sub 订阅锁释放通知,并结合 TTL 进行等待和重试。
线程A完成业务,执行 Lua 脚本释放锁,并在完全释放后发布通知。
线程B重新竞争锁,成功后执行业务。
注意:Pub/Sub 只是通知等待线程重新尝试获取锁,并不是直接将锁交给某个线程。
五、Redisson 可重入锁的实现原理
Redisson 的可重入锁通过 Redis 的 Hash 数据结构实现,而不是普通的 String。
1. Redis 存储结构
Key:lock:order:1001
Type:Hash
Field:客户端唯一标识:线程ID
Value:重入次数例如:
lock:order:1001
│
└── clientId:threadId → 1其中:
Key:分布式锁的名称。
Field:区分不同客户端及线程。
Value:当前线程获取锁的次数。
2. 加锁判断逻辑
申请锁
│
├── 锁不存在
│ └── 创建Hash,计数器=1
│
├── 锁已存在,且属于当前线程
│ └── 计数器+1,可重入
│
└── 锁属于其他线程
└── 获取失败,等待或重试3. 可重入示例
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // 第一次加锁,计数器=1
lock.lock(); // 第二次加锁,计数器=2
try {
// 执行业务逻辑
} finally {
lock.unlock(); // 计数器=1
lock.unlock(); // 计数器=0,真正释放锁
}可重入锁的核心原理:
同一个线程再次获取锁时,不会被自己阻塞,而是将 Hash 中的重入次数加1。
释放锁时,计数器减1。只有计数器归零,才会真正删除锁,并通知其他等待线程。
因此,加锁多少次,就需要解锁多少次。
六、Watch Dog 看门狗机制
1. 为什么需要看门狗?
假设:
锁的 TTL 为30秒。
业务执行需要60秒。
如果没有自动续期,锁在第30秒过期,其他线程就可能获取该锁,导致多个线程同时执行业务。
Redisson 通过 Watch Dog 自动续期来降低这种风险。
2. 工作原理
默认情况下,看门狗超时时间为30秒,通常约每10秒执行一次续期。
0秒:线程A获取锁,TTL=30秒
│
10秒:看门狗续期,TTL重新设置为30秒
│
20秒:看门狗续期,TTL重新设置为30秒
│
30秒:看门狗续期,TTL重新设置为30秒
│
40秒:看门狗续期,TTL重新设置为30秒
│
50秒:看门狗续期,TTL重新设置为30秒
│
60秒:业务执行完成,释放锁工作流程:
线程成功获取锁,Redis 设置初始 TTL。
Redisson 启动定时续期任务。
定时检查锁是否仍由对应线程持有。
如果仍持有,则通过 Lua 脚本更新 TTL。
业务完成后释放锁,并取消续期任务。
如果客户端宕机,无法继续续期,锁最终到期自动释放。
3. 哪些方法支持自动续期?
重点:只有不指定固定 leaseTime 的加锁方式,才会启用看门狗自动续期。
看门狗能够降低锁提前失效的风险,但不能完全防止长时间 GC 暂停、网络故障等情况导致的锁失效。
七、实际开发注意事项
合理设计锁粒度:例如
lock:order:用户ID,避免所有用户竞争同一把锁。使用 finally 解锁:确保正常执行或发生异常时都能尝试释放锁。
使用 tryLock 控制等待时间:避免请求长期阻塞。
避免固定 TTL 过短:业务未执行完成时,锁可能提前过期。
注意主从一致性问题:Redis 主从异步复制可能导致故障切换后锁丢失。
重要业务增加数据库保护:库存扣减、余额更新等操作应结合条件更新、唯一约束、幂等控制等机制保障数据正确性。
另外,isHeldByCurrentThread() 只能检查当前时刻是否持有锁,不能保证检查后锁一定不会丢失。解锁过程仍需考虑异常处理。
八、面试总结
Redisson 是基于 Redis 实现的分布式锁工具,相比手动使用 SET NX EX,主要支持可重入、等待重试和自动续期等功能。
它通过 Redis Hash 存储锁信息,其中 Field 表示客户端和线程,Value 表示重入次数,并通过 Lua 脚本保证加锁和解锁的原子性。
当获取锁失败时,线程可以通过 Pub/Sub 机制等待释放通知,然后重新竞争锁,避免持续高频轮询。
Redisson 还提供 Watch Dog 看门狗机制,在没有指定固定锁过期时间时自动续期,减少业务尚未完成而锁提前过期的问题。
不过,Redisson 不能彻底解决 Redis 主从切换导致的锁丢失问题。对于一致性要求较高的业务,还需要结合数据库层面的保护措施。
核心记忆:Hash 实现可重入,Lua 保证原子性,Pub/Sub 实现等待通知,Watch Dog 实现自动续期。
