死锁悬案服务重启了锁却没释放第二天业务全停。重复扣款网络抖动导致重试用户被扣了两次钱。排查无门系统卡住了不知道是哪个线程持有了锁持有了多久。重入崩溃递归调用或 AOP 嵌套时锁直接报错或逻辑错乱。根据之前做的项目我们要给分布式锁穿上一套“防弹衣”可重入机制、业务幂等性设计、完善的异常兜底、以及可视化的监控体系。核心痛点一可重入 (Reentrancy) —— “自己人别开火”假设你有一个updateOrder方法加了锁而该方法内部又调用了另一个也加了同一把锁的checkStock方法无重入支持线程 A 拿到锁 - 调用内部方法 - 尝试再次加锁 -阻塞等待自己释放锁-死锁。自己等自己的锁 哈哈后果线程池耗尽服务假死 你拿着门禁卡锁进了公司大门。走到电梯口又要刷卡。 不可重入保安说“你已经在里面了不能进”把你拦在电梯口你也出不去死锁。 可重入保安识别出是你说“哦您已经在楼里了请进”计数 1。等你最后离开大楼时计数归零门才真正锁上。底层实现原理 (参考 Redisson)可重入的核心在于区分“是谁加的锁”以及“加了几次”。1. Redis 端数据结构Redis 不再使用简单的String(KeyValue)而是使用Hash结构Key:lock:order:1001Field:UUID:ThreadID(唯一标识当前线程)Value:重入次数 (int)如果 Hash 为空则删除整个 Key。HSET lock:order:1001 uuid:thread-1 1 (第一次加锁) HINCRBY lock:order:1001 uuid:thread-1 1 (重入变为 2) HINCRBY lock:order:1001 uuid:thread-1 -1 (释放一次变为 1) HDEL lock:order:1001 uuid:thread-1 (释放最后一次计数为 0删除 Field)2. 客户端本地缓存 (ThreadLocal)为了减少网络 IO客户端会在内存中维护一个映射MapString, Integer threadLockCounts(Key: 锁名Value: 重入次数)。每次加锁/释放先查本地只有计数归零或首次加锁时才操作 Redis。核心痛点二业务幂等性 (Idempotency) —— “防抖动的终极防线”分布式锁只能保证同一时刻只有一个线程执行代码但不能保证代码只执行一次。场景用户点击支付 - 网关超时 - 前端重试 - 后端收到两个请求。风险虽然锁保证了串行但如果第一个请求执行完还没释放锁就宕机了极小概率或者逻辑本身没做好防护依然可能出问题。更重要的是锁是防御并发的幂等是防御重复请求的。生活化比喻锁 确保同一时间只有一个人在填表格。幂等 确保无论这个人填了多少次表格系统只记录一次有效数据。三重保障架构在生产环境我们采用“锁 唯一键 状态机”的三层防御第一层分布式锁作用拦截 99% 的并发流量保护数据库不被瞬间打挂。粒度细粒度如lock:order:1001。第二层数据库唯一索引 (Unique Index)作用兜底。即使锁失效极端情况数据库也会报DuplicateKeyException。设计建立一张biz_idempotent_table(request_id, biz_type)或者在业务表上加唯一约束。第三层状态机 (State Machine)作用逻辑兜底。设计UPDATE order SET statusPAID WHERE id1001 AND statusUNPAID。如果状态已经是PAID更新行数为 0直接返回成功不执行扣款逻辑核心痛点三异常处理与兜底 —— “无论如何必须松手”boolean isLocked false; try { // 1. 尝试加锁务必设置超时防止无限阻塞 isLocked lock.tryLock(5, 10, TimeUnit.SECONDS); if (!isLocked) { // 2. 获取失败的处理策略 log.warn(获取锁失败业务降级或提示用户重试); throw new BusinessException(系统繁忙请稍后重试); } // 3. 执行业务 doBusiness(); } catch (InterruptedException e) { // 4. 响应中断恢复中断状态 Thread.currentThread().interrupt(); throw new BusinessException(线程被中断); } catch (Exception e) { // 5. 记录日志但不要吞掉异常 log.error(业务执行异常, e); throw e; // 向上抛出让事务回滚 } finally { // 6. 【关键】只有持有锁且是当前线程才释放 if (isLocked lock.isHeldByCurrentThread()) { try { lock.unlock(); } catch (Exception e) { log.error(释放锁异常, e); } } }isHeldByCurrentThread()检查防止误删虽然 Redisson 内部做了但显式检查更安全。finally块确保即使业务报错、return、throw锁都能释放。核心痛点四监控大盘 —— “看不见的锁是最危险的”没有监控的分布式锁就是“黑盒”。你需要知道谁持有了锁持有了多久有多少次获取锁失败平均等待时间是多少指标名称类型含义报警阈值建议dist_lock_acquire_durationHistogram获取锁耗时分布P99 1s 报警dist_lock_hold_durationHistogram锁持有时长分布P99 10s 报警 (可能有长事务)dist_lock_failure_totalCounter获取锁失败次数突增 50% 报警dist_lock_watchdog_renew_totalCounter看门狗续期次数异常高可能意味着死循环Prometheus Grafana 集成示例在 Spring Boot 中利用 Micrometer 自动暴露指标Bean public Timer.Sample lockTimer() { return Timer.start(meterRegistry); } // 在 AOP 或工具类中记录 timer.stop(registry.timer(dist_lock.acquire, resource, resourceName));Grafana 面板设计思路顶部实时 QPS、失败率。中部获取锁耗时热力图Heatmap观察是否有长尾延迟。底部锁持有时长 Top 10 的资源 ID帮助定位慢业务。业务实战Spring AOP 封装“防弹衣”光说不练假把式。下面是一个生产级的 Spring AOP 切面集成了自定义注解DistributedLockSpEL 表达式解析动态锁 Key可重入支持(依赖 Redisson)完整的 Try-Finally 兜底监控埋点优雅的错误处理Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface DistributedLock { // 锁的 Key支持 SpEL如 #orderId String key(); // 等待时间 long waitTime() default 5; // 租约时间 (默认 -1 启用看门狗) long leaseTime() default -1; TimeUnit timeUnit() default TimeUnit.SECONDS; // 失败后的提示信息 String failMessage() default 系统繁忙请稍后重试; }2. AOP 切面实现Aspect Component Slf4j public class DistributedLockAspect { Autowired private RedissonClient redissonClient; Autowired private MeterRegistry meterRegistry; // Prometheus 监控 // 环绕通知 Around(annotation(lockAnnotation)) public Object around(ProceedingJoinPoint pjp, DistributedLock lockAnnotation) throws Throwable { // 1. 解析 SpEL 表达式生成动态 Key String lockKey parseKey(lockAnnotation.key(), pjp); RLock lock redissonClient.getLock(lockKey); boolean isLocked false; Timer.Sample sample Timer.start(meterRegistry); // 开始计时 try { // 2. 尝试加锁 isLocked lock.tryLock( lockAnnotation.waitTime(), lockAnnotation.leaseTime(), lockAnnotation.timeUnit() ); // 3. 记录获取锁耗时 sample.stop(Timer.builder(dist_lock.acquire) .tag(resource, lockKey) .register(meterRegistry)); if (!isLocked) { log.warn(获取分布式锁失败: {}, lockKey); meterRegistry.counter(dist_lock.failure, resource, lockKey).increment(); throw new BusinessException(lockAnnotation.failMessage()); } // 4. 执行业务逻辑 return pjp.proceed(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.error(获取锁被中断: {}, lockKey, e); throw new BusinessException(操作被中断); } catch (BusinessException e) { // 业务异常直接抛出 throw e; } catch (Exception e) { log.error(业务执行异常锁将自动释放: {}, lockKey, e); throw e; } finally { // 5. 【核心】释放锁与监控 if (isLocked lock.isHeldByCurrentThread()) { try { lock.unlock(); // 记录持有时长 if (sample ! null) { // 注意这里需要单独记录持有时长上面 sample 已停止实际项目中需两个 Timer // 简化演示仅记录释放动作 } } catch (IllegalMonitorStateException e) { log.error(释放锁异常 (可能已过期): {}, lockKey, e); } } } } // SpEL 解析工具方法 (简化版) private String parseKey(String keyExpression, ProceedingJoinPoint pjp) { if (!keyExpression.startsWith(#)) { return keyExpression; // 静态 Key } // 实际生产建议使用 Spring ExpressionContext 完整解析 // 这里简单模拟提取 # 后面的参数名 String paramName keyExpression.substring(1); Object[] args pjp.getArgs(); String[] paramNames ((MethodSignature) pjp.getSignature()).getParameterNames(); for (int i 0; i paramNames.length; i) { if (paramNames[i].equals(paramName)) { return lock: args[i]; } } return lock:default; } }3. 业务使用示例Service public class OrderService { DistributedLock(key #orderId, waitTime 3, failMessage 排队人数过多) public void createOrder(String orderId, Long userId) { // 1. 幂等性检查 (数据库唯一键或状态机) checkIdempotency(orderId); // 2. 业务逻辑 // ... 扣减库存、创建订单 ... // 3. 即使这里报错AOP 的 finally 也会保证锁释放 if (userId 9527) { throw new RuntimeException(模拟异常测试); } } // 可重入测试 DistributedLock(key #orderId) public void updateOrderStatus(String orderId) { // 内部调用同一个锁的方法 checkStock(orderId); // 不会死锁因为 Redisson 支持可重入 } DistributedLock(key #orderId) private void checkStock(String orderId) { // ... } }interview生产陷阱篇Q1: 你的分布式锁方案如何处理“锁未释放”的情况回答代码层面严格遵循try-finally范式确保unlock()在任何异常路径下都被执行。框架层面使用 Redisson 的WatchDog机制。只要客户端进程活着锁会自动续期如果客户端宕机Session 过期后锁自动释放。监控层面接入 Prometheus 监控“锁持有时长”对超过阈值如 30s的锁进行报警人工介入排查长事务。兜底层面业务逻辑设计幂等性唯一索引、状态机即使锁意外失效导致并发数据也不会错。Q2: 如果业务逻辑非常复杂执行时间不确定锁的过期时间怎么设回答绝不设置固定的短过期时间。直接使用Redisson 的默认模式不传 leaseTime启用WatchDog。WatchDog 会每 10 秒检测一次如果线程还持有锁就自动续期到 30 秒。这样无论业务跑多久只要不宕机锁都不会过期。同时配合异步化改造将超长耗时任务移出同步锁范围。Q3: 如何保证分布式锁的幂等性锁本身能解决幂等吗回答锁不能解决幂等性。锁只能解决并发互斥。幂等性必须靠业务设计唯一索引数据库层面防止重复插入。状态机UPDATE ... WHERE status INIT利用影响行数判断是否执行成功。Token 机制前端提交前获取 Token后端验证并删除 Token。锁只是为这些机制提供了一个高性能的“前置过滤器”。“程序员的价值不在于写出多复杂的锁算法而在于设计出即使锁失败了系统也能优雅降级、数据依然正确的‘鲁棒’架构。”“可重入是基础幂等性是底线监控是眼睛兜底是救命稻草。缺一不可。”“不要迷信‘绝对不丢锁’。要相信‘即使丢了锁我的数据也不会乱’。这才是分布式系统设计的成熟标志。”