真实面经题目 · 原创解析
分布式锁代码逻辑里如果发生异常 catch 的时候需要做什么?
分布式锁保护的业务逻辑发生异常时,catch 的重点不是立刻解锁,而是让失败可见、记录足够上下文、决定补偿或重试。锁释放应统一放在 finally,并在释放前确认当前请求仍然持有锁,避免锁泄漏、误删他人锁和异常被吞导致的数据不一致。
真实面经题目 · 原创解析
分布式锁保护的业务逻辑发生异常时,catch 的重点不是立刻解锁,而是让失败可见、记录足够上下文、决定补偿或重试。锁释放应统一放在 finally,并在释放前确认当前请求仍然持有锁,避免锁泄漏、误删他人锁和异常被吞导致的数据不一致。
可以先把职责拆开:catch 负责异常处理,finally 负责资源释放。业务代码在分布式锁内抛异常时,catch 里应记录锁 key、业务 id、请求 token、耗时和异常类型,区分业务异常与系统异常,然后继续抛出、转成明确失败结果,或进入可追踪补偿流程,不能只打印日志后返回成功。解锁动作放在 finally 中,因为正常返回、异常返回、提前 return 都需要释放资源。释放锁也不能无脑删除 key,必须确认当前执行单元仍然持有这把锁。自己实现 Redis 锁时,加锁要写入唯一 value 并设置过期时间,解锁要用 Lua 脚本原子完成 value 校验和删除,避免锁过期后被别人重新获取却被当前请求误删。使用 Redisson 时也要在 finally 中判断当前线程是否持有锁,理解 leaseTime、watchdog 和线程绑定边界。最后还要补充幂等、补偿、超时和告警,因为分布式锁只能提供互斥控制,不能替代事务和一致性设计。
catch 的职责是处理异常本身,例如记录上下文、判断异常类型、返回明确失败、触发告警或安排补偿;unlock 是资源释放动作,应放在 finally 中统一执行。这样能覆盖正常执行、业务异常、系统异常和提前返回等路径,降低异常分支遗漏释放的概率。
分布式锁通常保护库存、订单、账户、任务调度等关键临界区。如果 catch 只打印日志然后返回成功,上层会继续推进后续流程,监控也很难发现真实失败。更稳妥的做法是抛出异常、转成失败响应,或写入补偿任务,让失败保持可见且可追踪。
finally 只是保证释放动作有机会执行,并不代表删除锁一定安全。业务耗时可能超过锁过期时间,锁已经被其他请求重新获取;此时当前请求再 delete,就会误删别人的锁。安全解锁必须校验持有者身份,只释放自己加的锁。
自行实现 Redis 分布式锁时,加锁通常要使用唯一 token 和过期时间。解锁时不能先 get 再 delete,因为两条命令之间可能发生锁过期和重新加锁。应使用 Lua 脚本在 Redis 侧一次性完成“比较 token”和“删除 key”,把并发窗口收掉。
Redisson 封装了可重入锁、线程持有关系和看门狗续期,但使用者仍要理解边界。加锁和解锁要保持同一线程语义,线程池切换或异步回调可能破坏持有关系;显式 leaseTime 会影响续期行为,业务耗时超过租期也可能提前释放锁。
释放锁只解决资源占用,不代表业务状态恢复正确。如果异常发生在部分写入之后,需要结合数据库事务、状态机、幂等键、补偿任务和告警处理。重试必须建立在幂等之上,否则可能把一次失败扩大成重复扣减、重复提交或状态反复流转。
catch 只覆盖被捕获的异常路径,无法统一覆盖正常返回、提前 return 或未捕获异常。解锁是资源释放,放在 finally 更完整;catch 可以记录和传播异常,但不应承担主要释放职责。
不一定。finally 只能保证会尝试释放,真正安全还要确认当前请求仍然持有锁。Redis 锁要校验唯一 token,Redisson 要确认当前线程持有锁,再执行 unlock。
可以,但必须有明确失败语义,例如返回失败结果、写入补偿记录或触发告警。不能只是打印日志后当作成功,否则调用方和监控都会误判业务已经完成。
说明锁生命周期设计有风险。可以调整过期时间、使用续期机制、缩短临界区、增加业务超时,并通过幂等和状态校验防止同一业务被并发重复执行。
因为校验 token 和删除 key 必须不可分割。若先 get 后 delete,中间可能发生锁过期并被别人重新加锁,当前请求会删除他人的新锁。Lua 能把两个动作合并为一次原子执行。