ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring自定义锁注解实现高并发控制

Spring自定义锁注解实现高并发控制 1. 为什么需要自定义锁注解在Spring应用开发中处理并发问题就像在早高峰的地铁站维持秩序——如果没有合理的排队机制所有人都挤在闸机口最终谁都进不去。传统的synchronized关键字虽然能解决问题但就像给整个地铁站上了把大锁效率低下且缺乏灵活性。我最近在电商秒杀系统中就遇到了典型的并发问题当多个用户同时抢购同一商品时库存扣减出现了超卖现象。数据库报出的1205 - lock wait timeout exceeded错误日志正是MySQL在提醒我们锁竞争已经超时。2. 方案设计与技术选型2.1 注解式锁的优势相比直接在代码中写Lock API注解方案有三大优势非侵入性业务代码不会被锁实现污染可维护性锁策略变更只需修改注解实现可读性方法上的Lock一眼就能看出需要并发控制2.2 核心组件拆解实现这个方案需要三个关键部分自定义注解定义包括锁名称、超时时间等属性AOP切面编程负责注解的解析和锁的获取/释放锁实现层支持不同锁策略Redis、Zookeeper等3. 完整实现步骤3.1 定义Lock注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface Lock { String key(); // 锁的键前缀 int expire() default 30; // 锁过期时间(秒) int timeout() default 5; // 获取锁超时时间(秒) TimeUnit timeUnit() default TimeUnit.SECONDS; }3.2 实现AOP切面Aspect Component RequiredArgsConstructor public class LockAspect { private final RedissonClient redissonClient; Around(annotation(lock)) public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable { String lockKey buildLockKey(joinPoint, lock); RLock rLock redissonClient.getLock(lockKey); try { boolean acquired rLock.tryLock(lock.timeout(), lock.expire(), lock.timeUnit()); if (!acquired) { throw new RuntimeException(获取锁超时); } return joinPoint.proceed(); } finally { if (rLock.isHeldByCurrentThread()) { rLock.unlock(); } } } private String buildLockKey(ProceedingJoinPoint joinPoint, Lock lock) { // 构建基于方法签名的唯一键 MethodSignature signature (MethodSignature) joinPoint.getSignature(); return String.format(%s:%s, lock.key(), DigestUtils.md5Hex(signature.toString())); } }3.3 业务层使用示例Service public class InventoryService { Lock(key inventory_lock, expire 10) public void deductStock(Long productId, int quantity) { // 库存扣减逻辑 } }4. 关键问题与优化方案4.1 锁粒度控制常见误区是锁粒度过大比如对全部商品使用同一个锁。应该根据业务场景选择商品维度productId作为锁key部分用户维度userId作为锁key部分组合维度userId_productId组合4.2 锁续期机制对于长时间操作建议实现看门狗机制private void startLockRenewal(RLock lock, long leaseTime) { Thread renewalThread new Thread(() - { while (!Thread.currentThread().isInterrupted()) { try { Thread.sleep(leaseTime * 1000 / 3); lock.expire(leaseTime, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }); renewalThread.setDaemon(true); renewalThread.start(); }4.3 异常处理策略建议定义不同的锁获取失败策略public enum LockFailStrategy { FAIL_FAST, // 立即抛出异常 RETRY, // 重试指定次数 QUEUE, // 进入队列等待 IGNORE // 跳过执行 }5. 性能优化实践5.1 锁分段技术借鉴ConcurrentHashMap思想将热点商品锁分散private static final int LOCK_SEGMENTS 16; private final RLock[] locks new RLock[LOCK_SEGMENTS]; public RLock getSegmentLock(String key) { int segment Math.abs(key.hashCode() % LOCK_SEGMENTS); return locks[segment]; }5.2 锁等待统计通过Micrometer监控锁竞争情况Around(annotation(lock)) public Object around(ProceedingJoinPoint joinPoint, Lock lock) throws Throwable { Timer.Sample sample Timer.start(); try { // 获取锁逻辑 return joinPoint.proceed(); } finally { sample.stop(Metrics.timer(lock.acquire.time, key, lock.key())); } }6. 不同场景下的锁选型6.1 本地锁 vs 分布式锁特性ReentrantLockRedis锁Zookeeper锁性能最高高低可靠性单机可靠较高最高实现复杂度低中高6.2 Spring事务整合特别注意锁与事务的执行顺序事务注解Transactional和Lock同时存在时应该把Lock放在外层。 因为事务提交后才释放数据库锁而我们需要先释放分布式锁。正确顺序Lock(key order_lock) Transactional public void createOrder(OrderDTO dto) { // 下单逻辑 }7. 生产环境踩坑记录锁过期时间设置不当曾设置5秒过期但GC暂停导致锁提前释放。解决方案是根据历史执行时间统计设置合理值。未处理锁释放异常网络抖动时解锁失败导致死锁。现在增加了解锁重试机制int retry 3; while (retry-- 0) { try { lock.unlock(); break; } catch (Exception e) { Thread.sleep(100); } }线程池任务丢失锁使用线程池异步处理时任务可能被不同线程执行。解决方案是使用TransmittableThreadLocal保存锁持有信息。8. 扩展应用场景8.1 接口幂等控制Lock(key idempotent_#{#request.id}, expire 24*3600) public void processPayment(IdempotentRequest request) { // 支付处理逻辑 }8.2 定时任务防重Scheduled(cron 0 0/5 * * * ?) Lock(key report_generate, expire 300) public void generateDailyReport() { // 报表生成逻辑 }8.3 分布式工作流控制Lock(key workflow_#{#flowId}_step_#{#step}, expire 3600) public void executeWorkflowStep(String flowId, int step) { // 工作流步骤执行 }在实际项目中这个自定义锁注解已经帮我们解决了90%的并发问题。特别是在大促期间系统从每分钟出现几十笔超卖订单到现在保持零差错运行。最关键的体会是好的并发控制应该像交通信号灯——既要有明确的规则又要给不同方向的车辆合理的通行时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进