035缓存模块Redisson 分布式锁实战:可重入锁与阻塞锁的设计与实现
本项目代码: https://gitee.com/yunjiao-source/tutorials4j
1. 引言
在分布式系统中,锁机制是解决并发冲突、保证数据一致性的基础组件。Redisson 作为 Redis 的 Java 客户端,提供了丰富的分布式锁实现,尤其是可重入锁(RLock)支持自动续期(看门狗)和灵活的租约控制。然而,直接使用 Redisson API 存在样板代码、锁释放易遗漏、异常处理不统一等问题。本文基于一套封装良好的 Redisson 锁服务代码,分析其设计思想、核心实现、适用场景及最佳实践。
2. 总体设计
代码分为两个平行的锁服务体系:
- 可重入锁服务(
RedissonReentrantLockService) :基于RLock.tryLock(),支持超时等待和租约控制。 - 阻塞锁服务(
RedissonBlockLockService) :基于RLock.lock(),会无限阻塞直到获取锁。
每个服务体系都包含:
- 一个 Service 类 (
RedissonReentrantLockService/RedissonBlockLockService),提供FixedLease(固定租约)和AutoRenewal(自动续期)两种内部操作类。 - 一个 注解 (
@RedissonReentrantLockable/@RedissonBlockLockable),声明式配置锁参数。 - 一个 Aspect 切面 (
RedissonReentrantLockableAspect/RedissonBlockLockableAspect),利用 Spring AOP 实现无侵入的锁管理。
整体架构遵循关注点分离原则:业务代码只需添加注解,锁的获取、释放、超时、异常处理均由切面和锁服务透明完成。
3. 核心组件分析
3.1 RedissonReentrantLockService(可重入锁服务)
该服务提供两个内部类:FixedLease 和 AutoRenewal。
固定租约模式(FixedLease)
java
public <T> T doInLock(String lockKey, Duration waitTime, Duration expireTime, Supplier<T> task)
- 使用
lock.tryLock(waitTime, expireTime, TimeUnit),在waitTime内等待锁,成功则持有锁最多expireTime。 - 租约到期后自动释放,不会续期,适合执行时间确定的短任务。
- 获取锁失败立即抛出
LockCreateException,避免无限等待。
自动续期模式(AutoRenewal)
java
public <T> T doInLock(String lockKey, Duration waitTime, Callable<T> task)
- 调用
lock.tryLock(waitTime, TimeUnit),只指定等待时间,不指定租约。 - Redisson 会启动看门狗线程,默认每 10 秒续期一次(续期至 30 秒),直到显式
unlock()。 - 适用于执行时间不确定的任务,如批处理、RPC 调用等。
注意 :自动续期模式下,
Callable的异常会被包装为LockException;中断处理会恢复中断标志。
3.2 RedissonBlockLockService(阻塞锁服务)
与可重入锁服务的核心区别在于获取锁的行为:
java
// 固定租约:直接 lock,不等待,阻塞直到成功
lock.lock(expireTime, TimeUnit.MILLISECONDS);
// 自动续期:无超时的 lock,永久阻塞
lock.lock();
- 没有
waitTime概念,调用线程会无限阻塞直到获得锁。 - 风险:可能导致线程堆积、死锁或服务雪崩。必须确保持有锁的时间可控,或配合超时机制使用。
- 适用场景:低并发、对延迟不敏感、且必须成功获取锁的临界区(如定时任务调度)。
3.3 安全解锁模式
两个服务共享相同的 unlock(RLock lock) 实现:
java
if (lock.isHeldByCurrentThread() && lock.isLocked()) {
lock.unlock();
}
- 双重检查防止:锁已过期被 Redis 自动释放、锁已被其他线程持有、或锁已被提前 unlock。
- 避免
IllegalMonitorStateException,确保解锁操作安全。
4. 注解驱动的 AOP 实现
4.1 注解设计
| 注解属性 | 作用 | 默认值 |
|---|---|---|
prefix |
锁 Key 前缀 | "" |
key |
SpEL 表达式,动态生成锁 Key | 必填 |
waitTime |
等待锁的最大时间(仅可重入锁) | 3000 ms |
expireTime |
锁租约时间,≤0 表示自动续期 | -1 |
timeUnit |
时间单位 | MILLISECONDS |
@RedissonBlockLockable 没有 waitTime 属性(因为阻塞锁不支持等待超时)。
4.2 切面逻辑
java
@Around("@annotation(redissonReentrantLockable)")
public Object around(ProceedingJoinPoint joinPoint, RedissonReentrantLockable redissonReentrantLockable)
- 解析方法参数,通过
SpelMethodBasedExpressionEvaluator计算key表达式的值。 - 拼接
prefix + value得到最终锁 Key。 - 根据
expireTime > 0选择固定租约或自动续期模式。 - 在锁保护的 Lambda 中执行
joinPoint.proceed(),并捕获Throwable包装为RuntimeException。 - 切面本身不直接处理锁释放,完全委托给锁服务的
doInLock方法(它负责finally中的unlock)。
优点:
- 业务代码零侵入,只需注解。
- 锁 Key 支持动态 SpEL 表达式,灵活性高。
- 异常处理统一,业务异常不会导致锁泄漏。
局限性:
- 业务方法抛出的受检异常(
throws Exception)会被转换为RuntimeException,可能丢失类型信息。可改为直接抛出原始异常(修改Supplier为支持throws Throwable的函数式接口)。
5. 锁模式对比与选型建议
| 特性 | ReentrantLock + tryLock | BlockLock + lock |
|---|---|---|
| 超时等待 | ✅ 支持 waitTime |
❌ 不支持,永久阻塞 |
| 租约控制 | ✅ 支持固定租约和自动续期 | ✅ 支持固定租约和自动续期 |
| 中断响应 | ✅ 可响应中断,恢复标志 | ✅ 可响应中断(lockInterruptibly 未被使用) |
| 适用场景 | 高并发、需要快速失败的场景 | 低并发、必须成功执行的场景 |
| 风险 | 获取锁失败抛异常,需要业务兜底 | 阻塞可能导致线程池耗尽 |
选型建议:
- 优先使用可重入锁服务 (
@RedissonReentrantLockable),并合理设置waitTime(如 1~3 秒)和expireTime。对于执行时间不稳定的任务,使用自动续期(expireTime = -1)。 - 谨慎使用阻塞锁服务 :仅当业务逻辑必须 获取锁才能继续,且对延迟无要求时使用(如单机定时任务的防重执行)。务必配合
expireTime防止 Redis 连接异常导致锁永久不释放。
6. 异常处理与线程中断
6.1 中断处理策略
在 tryLock 可能抛出 InterruptedException 时,代码采用标准模式:
java
catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new LockException(lockKey, e);
}
- 重新设置中断标志,保留上层调用者的中断状态。
- 将中断转换为业务异常
LockException,让调用方感知锁获取失败。
6.2 锁异常分类
LockCreateException:获取锁超时(仅tryLock模式)。LockException:解锁失败、任务执行异常、中断等通用锁错误。
这种细分有助于调用方针对不同失败原因做降级处理(如超时后走本地缓存、异步重试等)。
7. 最佳实践与改进建议
7.1 最佳实践
- 锁粒度 :
prefix应区分业务模块(如"order:pay:"),key应使用业务唯一标识(如订单号),避免锁范围过大。 - 租约时间:固定租约应设置为任务执行时间 P99 的 2~3 倍。过短会导致任务未完成锁自动释放,过长则降低并发度。
- 等待时间 :
waitTime应结合业务容忍度设置。高频接口建议 500ms 内,后台任务可设 3~5 秒。 - 监控告警 :对
LockCreateException应配置监控,警惕锁竞争激烈或死锁。
7.2 改进建议
- 受检异常保持 :修改切面中的 Lambda,使用类似
SupplierThrows的自定义接口,避免将受检异常包装为RuntimeException。 - 锁 Key 前缀优化 :可支持在注解中使用
prefix的 SpEL 表达式,实现动态前缀(如${environment})。 - 支持
lockInterruptibly:阻塞锁服务可增加可中断锁方法,提升响应性。 - 统计与日志:在锁服务内部增加执行耗时、等待耗时等 Metrics(如 Micrometer),便于性能分析。
8. 总结
本文分析的 Redisson 锁封装实现,通过服务类封装 + 注解 + AOP,提供了一套清晰、健壮的分布式锁解决方案。它区分了 可重入(tryLock) 与 阻塞(lock) 两种语义,并各自支持固定租约与自动续期,满足了大多数分布式并发场景的需求。代码在解锁安全、中断处理、异常分类等方面均有细致考量,具有较高的生产可用性。开发者可根据实际业务特性选择合适的锁模式,并遵循文中建议的最佳实践,有效避免锁滥用带来的性能与稳定性风险。