分布式锁获取失败时应主动抛出自定义异常而非依赖框架:一要区分连接级失败(如RedisTimeoutException)和业务抢占失败(tryLock返回false),二要封装锁键、超时参数、订单ID等上下文信息,三需在调用后显式判断并抛出,四用@ControllerAdvice统一处理返回友好提示。

在分布式锁获取失败时触发自定义异常,核心不是“让框架自动抛它”,而是由你主动判断失败场景、封装上下文、再显式抛出——Redisson 等主流客户端不会为“没抢到锁”这种业务结果抛异常,它把 tryLock() == false 当作正常流程返回。
明确哪些情况该抛自定义异常
不是所有失败都值得包装成异常。重点覆盖两类需强干预的场景:
-
连接或通信级失败:如
NoConnectionAvailableException、RedisTimeoutException、RedisConnectionClosedException,说明底层不可用,业务无法继续 -
业务语义明确的抢占失败:比如对订单号加锁时,
tryLock(3, 30, SECONDS) == false,且你认定此时必须拒绝请求(而非降级或重试)
定义带业务字段的自定义异常类
不要继承 RuntimeException 就完事,要携带可定位的关键信息:
- 锁的唯一标识(如
"order:123456") - 当前尝试的超时参数(等待时间 + 持有时间)
- 触发该异常的业务上下文(如订单ID、用户ID、方法名)
- 构造时传入原始异常(
cause),保留栈轨迹
示例:
立即学习“Java免费学习笔记(深入)”;
public class DistributedLockAcquireException extends RuntimeException {private final String lockKey;
private final long waitTime;
private final long leaseTime;
private final String orderId;
public DistributedLockAcquireException(String lockKey, long waitTime, long leaseTime,
String orderId, String message, Throwable cause) {
super(message, cause);
this.lockKey = lockKey;
this.waitTime = waitTime;
this.leaseTime = leaseTime;
this.orderId = orderId;
}
}
在加锁逻辑中主动判断并抛出
别依赖“框架替你抛”,要在调用后立刻检查结果:
- 对
tryLock()返回false的情况,手动构建并抛出你的异常 - 对捕获到的 Redisson 子异常(如
RedisTimeoutException),包装后抛出,不直接 throw 原始异常 - 确保日志输出时把自定义异常作为最后一个参数,SLF4J 才能打印完整堆栈
示例代码片段:
RLock lock = redissonClient.getLock("order:" + orderId);try {
if (!lock.tryLock(3, 30, TimeUnit.SECONDS)) {
throw new DistributedLockAcquireException(
"order:" + orderId, 3, 30, orderId,
"Failed to acquire lock within timeout", null);
}
// 执行业务
} catch (RedisTimeoutException e) {
throw new DistributedLockAcquireException(
"order:" + orderId, 3, 30, orderId,
"Redis connection timeout during lock acquisition", e);
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
配合全局异常处理器统一响应
Spring 中可通过 @ControllerAdvice 拦截你的自定义异常,转为标准错误码和提示:
- 前端看到的是 “订单正被处理,请稍后再试”,而不是堆栈或“RedisTimeoutException”
- 日志里保留完整异常链,便于排查是网络问题还是竞争太激烈
- 监控系统可基于异常类型+关键字段(如
lockKey)做聚合告警


















