[035][缓存模块]Redisson 分布式锁实战:可重入锁与阻塞锁的设计与实现

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(可重入锁服务)

该服务提供两个内部类:FixedLeaseAutoRenewal

固定租约模式(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)
  1. 解析方法参数,通过 SpelMethodBasedExpressionEvaluator 计算 key 表达式的值。
  2. 拼接 prefix + value 得到最终锁 Key。
  3. 根据 expireTime > 0 选择固定租约或自动续期模式。
  4. 在锁保护的 Lambda 中执行 joinPoint.proceed(),并捕获 Throwable 包装为 RuntimeException
  5. 切面本身不直接处理锁释放,完全委托给锁服务的 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 最佳实践

  1. 锁粒度prefix 应区分业务模块(如 "order:pay:"),key 应使用业务唯一标识(如订单号),避免锁范围过大。
  2. 租约时间:固定租约应设置为任务执行时间 P99 的 2~3 倍。过短会导致任务未完成锁自动释放,过长则降低并发度。
  3. 等待时间waitTime 应结合业务容忍度设置。高频接口建议 500ms 内,后台任务可设 3~5 秒。
  4. 监控告警 :对 LockCreateException 应配置监控,警惕锁竞争激烈或死锁。

7.2 改进建议

  • 受检异常保持 :修改切面中的 Lambda,使用类似 SupplierThrows 的自定义接口,避免将受检异常包装为 RuntimeException
  • 锁 Key 前缀优化 :可支持在注解中使用 prefix 的 SpEL 表达式,实现动态前缀(如 ${environment})。
  • 支持 lockInterruptibly:阻塞锁服务可增加可中断锁方法,提升响应性。
  • 统计与日志:在锁服务内部增加执行耗时、等待耗时等 Metrics(如 Micrometer),便于性能分析。

8. 总结

本文分析的 Redisson 锁封装实现,通过服务类封装 + 注解 + AOP,提供了一套清晰、健壮的分布式锁解决方案。它区分了 可重入(tryLock)阻塞(lock) 两种语义,并各自支持固定租约与自动续期,满足了大多数分布式并发场景的需求。代码在解锁安全、中断处理、异常分类等方面均有细致考量,具有较高的生产可用性。开发者可根据实际业务特性选择合适的锁模式,并遵循文中建议的最佳实践,有效避免锁滥用带来的性能与稳定性风险。