1. 背景概述
在构建高并发系统(如秒杀、电商交易)时,分布式锁是解决资源竞争的核心组件。为了平衡开发效率与运行性能,我设计了一套包含“AOP 声明式”与“编程式工具类”的双模分布式锁框架。
本文将拆解该框架在简历中的描述方式,并深入剖析其背后的技术难点与面试中可能遇到的“钩子”问题。
2. 技术点一:基于 AOP 的声明式分布式锁
2.1 简历写法建议
核心描述: 独立设计并研发了基于 AOP + Redisson 的声明式分布式锁组件,通过自定义注解实现业务逻辑的无侵入加锁。利用自定义 SpEL 解析器实现了复杂业务场景下的动态 Key 绑定,并巧妙利用切面优先级控制机制,彻底解决了高并发下数据库事务提交与锁释放时序冲突导致的数据不一致问题。
2.2 核心原理与设计难点
2.2.1 解决“事务与锁”的时序冲突(核心考点)
在 Spring 生态中,如果一个方法同时被 @Transactional(事务管理)和自定义的 @ServiceLock(分布式锁)标记,AOP 的执行顺序至关重要。
问题场景: 若锁切面优先级低于事务切面,执行顺序为:
开启事务 -> 加锁 -> 业务逻辑 -> 释放锁 -> 提交事务。 在高并发下,当锁释放后但事务尚未提交的毫秒级窗口期内,其他线程可能获取到锁并读取数据。此时由于前一个事务未提交,后续线程读到的是旧数据(脏读),导致数据覆盖或逻辑错误。解决方案: 利用 Spring AOP 的
@Order注解控制切面优先级。将锁切面的优先级设为最高(如@Order(-10)),确保其包裹在事务切面之外。 正确顺序:加锁 -> 开启事务 -> 业务逻辑 -> 提交事务 -> 释放锁。
2.2.2 动态 Key 解析 (SpEL)
业务加锁通常需要锁定特定的业务 ID(如 orderId),而非锁死整个方法。
- 实现:利用
Spring ExpressionParser解析注解中的 SpEL 表达式(如@ServiceLock(key = "#dto.orderNo"))。 - 流程:拦截器从
JoinPoint获取方法参数 -> 解析表达式 -> 提取具体 ID -> 拼接 Redis Key。
3. 技术点二:高性能编程式分布式锁工具
3.1 简历写法建议
核心描述: 针对秒杀下单等对性能极度敏感的场景,为了解决注解式锁粒度过粗的问题,封装了基于函数式编程的分布式锁工具类。实现了锁生命周期的自动化管理,规避了手动释放锁可能引发的死锁风险,并支持多样化的锁超时降级策略,将核心业务的锁持有时间压缩至毫秒级,显著提升了系统吞吐量。
3.2 核心原理与设计难点
3.2.1 锁粒度的精细化控制
- 痛点:注解式锁(
@ServiceLock)作用于整个方法。如果方法中包含非核心耗时操作(如 RPC 调用、复杂参数校验),会无故延长锁持有时间,降低并发度。 - 优化:
ServiceLockTool允许在方法内部的代码块级别加锁。仅包裹最核心的资源扣减逻辑(Critical Section),将锁持有时间从几百毫秒压缩至几毫秒。
3.2.2 函数式编程与自动化管理
采用模板方法模式,通过函数式接口 TaskRun(无返回值)和 TaskCall(有返回值)封装业务逻辑。
- 优势:开发者无需手写
lock.lock()和lock.unlock(),工具类内部使用try-finally结构强制释放锁,彻底杜绝因业务异常导致的死锁风险。
4. 面试官追问预演 (Hooks & Answers)
Q1:你提到的“数据库事务提交与锁释放时序冲突”具体是什么问题?你是怎么解决的?
【解析】:这是考察你对 Spring AOP 机制及并发数据一致性的理解。
- 回答范本:
- 问题:默认情况下,Spring AOP 切面的执行顺序是不确定的。如果锁切面在事务切面内部(即:先开启事务,后加锁;或者先释放锁,后提交事务),就会出问题。最典型的是“先释放锁,后提交事务”。在这个极其短暂的时间差内,数据库事务还没 Commit,但锁已经让出了。第二个线程进来拿到锁,读到的还是旧数据(因为前一个事务没提交),导致数据不一致。
- 解决:我在切面类上使用了
@Order注解(例如@Order(-10)),明确将分布式锁切面的优先级调至高于事务切面。这样保证了执行链条是:先加锁,再开事务;先提交事务,最后才释放锁。确保锁完全覆盖了事务的生命周期。
Q2:为什么说注解式锁“粒度过粗”?你的工具类具体是怎么优化的?
【解析】:考察对性能优化的敏感度及锁的使用场景。
- 回答范本:
- 分析:
@ServiceLock是方法级注解,意味着从方法进入到退出的全过程都持有锁。在秒杀场景中,一个下单方法可能包含:参数校验、用户信息查询、库存扣减、异步通知。其中只有“库存扣减”需要互斥,其他操作如果也占着锁,并发性能会大打折扣。 - 优化:我封装了
ServiceLockTool,它允许我在方法内部,只针对“库存扣减”这几行核心代码加锁。其他耗时操作放在锁外并发执行。 - 效果:通过这种“代码块级”的加锁,锁的持有时间可能从 200ms 降低到 5ms,系统的理论吞吐量直接提升了数十倍。
- 分析:
Q3:你的工具类利用“函数式编程”实现了“锁生命周期自动化管理”,具体指什么?
【解析】:考察代码设计能力及对安全性的考虑。
- 回答范本:
- 设计:我定义了
TaskRun和TaskCall接口。工具类的execute方法接收一个 Lambda 表达式作为参数。 - 实现:在工具类内部,我把通用的“加锁 -> 执行 -> 释放”逻辑写死在模板里。
- 价值:
- 代码简洁:业务方只需要关注业务逻辑。
- 安全兜底:利用
try-finally块确保无论业务逻辑是否抛出异常,finally中的unlock()永远会被执行,从根源上消灭了程序员“忘记写释放锁”或“异常导致锁未释放”造成的死锁问题。
- 设计:我定义了
Q4:锁获取失败后的“降级策略”是怎么设计的?
【解析】:考察系统容错性及用户体验设计。
- 回答范本:
- 策略:当
tryLock在指定时间内未获取到锁时,不应简单粗暴地抛出异常,而是根据业务重要性分级处理。我设计了一个策略枚举:- FAIL_FAST:快速失败,抛出系统忙异常(适用于非核心业务)。
- KEEP_ACQUIRE:自旋重试,继续尝试获取(适用于必须要执行成功的业务)。
- CUSTOM:自定义回调。
- 亮点:特别是
CUSTOM策略,利用反射机制调用业务类中指定的 fallback 方法。比如秒杀抢购失败,我可以降级为返回“排队中,请稍后”的友好提示,而不是直接报错,极大提升了用户体验。
- 策略:当