本文最后更新于:17 天前
引言
在分布式 Java 应用中,锁是保证并发安全的常见手段。Redisson 基于 Redis,可提供分布式、可重入、阻塞式的 RLock
。为了避免在业务层重复编写锁获取/释放逻辑,本文通过 自定义注解 + Spring AOP 封装一套轻量级分布式锁方案,既保持了 Redisson 的强大特性,又让业务代码足够「clean」。
方案设计目标
目标 |
说明 |
零侵入 |
业务侧仅需加一个注解即可完成加解锁 |
灵活拼锁 |
支持 SpEL 表达式,自由组合多维度键(如用户 ID、业务单号) |
超时可控 |
兼容显式 leaseTime 与 Redisson 自带 watch-dog 机制 |
异常兜底 |
获取锁失败、业务异常、释放锁失败均有友好的提示与日志 |
注解
代码
创建注解 RLock 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RLock {
String prefix() default "";
String expr() default "";
long waitTime() default 0L;
long leaseTime() default 30 * 1000L;
TimeUnit unit() default TimeUnit.MILLISECONDS;
String errorMessage() default "当前操作正在进行中,请稍后再试";
}
|
关键点
- prefix
- 用于区分不同业务域,建议与业务缩写保持一致,如
ORDER
、PAY
。
- expr
- 为空:使用「方法全限定名 + 用户 ID」默认拼锁。
#
开头:按照 SpEL 解析,动态读取方法入参。
- 其他:视为普通字符串。
- waitTime / leaseTime
waitTime
决定阻塞时间;到达上限仍未抢到锁,则抛异常。
leaseTime
> 0:固定持锁时长; = 0:开启 watch-dog,Redisson 默认每 10 s 自动续期,直到线程正常释放锁或宕机超时。
切面定义
流程代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @Slf4j @Aspect @Component @RequiredArgsConstructor public class RLockAspect {
private static final String LOCK_KEY_PREFIX = "REDISSON_LOCK";
private final RedissonClient redissonClient; private final SpelExpressionParser parser = new SpelExpressionParser(); private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Pointcut("@annotation(me.zhengjie.annotation.RLock)") public void pointcut() { }
@Around("pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method signatureMethod = signature.getMethod(); RLock annotation = signatureMethod.getAnnotation(RLock.class);
String lockKey = getKey(joinPoint);
org.redisson.api.RLock lock = redissonClient.getLock(lockKey); boolean isLocked = lock.tryLock(annotation.waitTime(), annotation.leaseTime(), annotation.unit()); if (!isLocked) { throw new BadRequestException(annotation.errorMessage()); }
try { return joinPoint.proceed(); } catch (Throwable throwable) { log.error("执行方法时发生异常: {}", throwable.getMessage(), throwable); throw throwable; } finally { try { if (lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } } catch (Exception e) { log.error("释放锁 {} 时发生异常: {}", lockKey, e.getMessage(), e); } } }
private String getKey(ProceedingJoinPoint joinPoint) { }
}
|
运行机制
步骤 |
细节 |
风险点 & 处理 |
① 解析注解 |
通过 MethodSignature 拿到 RLock 实例 |
—— |
② 拼接锁名 |
getKey() 将全局前缀、业务前缀、表达式结果合并成 LOCK_KEY_PREFIX:prefix:expr |
解析失败即抛业务异常,避免锁名为 null |
③ 尝试加锁 |
tryLock(wait, lease, unit) - 返回 true :进入临界区 - 返回 false :直接抛自定义提示 |
防止「假死」:未获取锁立即失败 |
④ 执行业务 |
joinPoint.proceed() |
业务异常无论何时抛出皆进入 finally |
⑤ 释放锁 |
仅当前线程且锁仍持有时调用 unlock() |
避免误解锁他人持有的锁; 释放失败打印 error 日志 |
获取锁名称
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| private String getKey(ProceedingJoinPoint joinPoint) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method signatureMethod = signature.getMethod(); RLock annotation = signatureMethod.getAnnotation(RLock.class);
String expr = annotation.expr(); String prefix = StrUtil.blankToDefault(annotation.prefix(), "DEFAULT"); if (StrUtil.isBlank(expr)) { String userId = null; try { userId = SecurityUtils.getCurrentUserId().toString(); } catch (Exception e) { log.warn("获取用户ID失败,使用默认值: {}", e.getMessage()); } return StrUtil.format("{}:{}:{}:{}", LOCK_KEY_PREFIX, prefix, signatureMethod.getDeclaringClass().getName() + "." + signatureMethod.getName(), userId); } if (expr.startsWith("#")) { try { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); EvaluationContext ctx = new MethodBasedEvaluationContext(null, method, joinPoint.getArgs(), nameDiscoverer); Object val = parser.parseExpression(expr).getValue(ctx); if (val == null) { throw new BadRequestException("锁名称表达式计算结果为null,请检查表达式: " + expr); } return StrUtil.format("{}:{}:{}", LOCK_KEY_PREFIX, prefix, String.valueOf(val)); } catch (Exception e) { log.error("解析锁名称表达式失败: {}", expr, e); throw new BadRequestException("锁名称表达式解析失败,请检查表达式: " + expr); } } else { return StrUtil.format("{}:{}:{}", LOCK_KEY_PREFIX, prefix, expr); } }
|
关键思路:
- 多维度唯一性:
LOCK_KEY_PREFIX
与 prefix
避免跨业务冲突;表达式或方法签名确保同业务不同场景隔离。
- 弹性 Fallback:开发者若忘记写
expr
,亦能拿到一个规则化且唯一的锁键。
- SpEL 灵活取值:支持链式取参(
#param.sub.id
),适配复杂入参结构。
实践
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| @RestController @RequestMapping("/test") public class TestController {
@RequestMapping("/1") @RLock(prefix = "test", expr = "11") public void test1() { System.out.println("Hello, World!"); }
@GetMapping("/2/{id}") @RLock(prefix = "test", expr = "#id") public void test2(@PathVariable("id") long id) { System.out.println("Hello, World!"); }
@PostMapping("/3/{pathVar}") @RLock(prefix = "test", expr = "#param.param2.id") public void test3(@PathVariable("pathVar") String pathVar, @RequestBody TestParam param) { System.out.println("Hello, World!"); }
@GetMapping("/4") @RLock(prefix = "test4") public void test4() { System.out.println("Hello, World!"); }
@Setter @Getter public static class TestParam { private long id; private TestParam2 param2;
}
@Setter @Getter public static class TestParam2 { private long id; private String name; }
}
|
实战建议
- 锁范围越小越好
- waitTime 合理设置
- UI 请求链建议 ≤ 1 s,定时任务可适当放宽。
- leaseTime 建议为 0
- 交由 Redisson watch-dog 自动续约,不易因单机 GC 暂停误释放。
- 锁粒度设计
- 订单、库存等按「业务主键」锁;
- 全量任务或定时脚本可用固定字符串,如
"GLOBAL"
。
总结
通过 @RLock
+ RLockAspect
,我们将分布式锁逻辑彻底下沉,业务侧只需关注「业务前缀 + 唯一键」即可安全并发。与原生 Redisson 相比,此封装:
- 去除了样板代码,降低认知负担;
- 保持了 Redisson 高可用、高性能的优势;
- 通过 SpEL 提供了高度自定义的锁粒度;
- 兼容 watch-dog 机制,稳定性更佳。
一句话:让分布式锁像 @Transactional
一样自然。