当前位置: 首页 > news >正文

分布式锁与事务配合:为什么锁要在事务提交后释放

分布式锁与事务配合:为什么锁要在事务提交后释放

一、问题引入

在分布式系统中,多个实例可能同时处理同一条数据。为了防止并发冲突,我们用分布式锁来保证同一时刻只有一个线程在操作某条数据。

但一个常见的错误是:在事务提交之前就释放了锁,导致其他线程读到了"未提交"的中间状态。


二、错误示例:锁在事务内释放

@TransactionalpublicvoidupdateInventory(IntegerskuId,Integerquantity){// 1. 加锁DistributedLocklock=lockProvider.getLock("inventory-"+skuId);lock.tryLock();try{// 2. 查询当前库存Inventoryinventory=inventoryRepository.findBySkuId(skuId);// 3. 扣减库存inventory.setQuantity(inventory.getQuantity()-quantity);inventoryRepository.save(inventory);}finally{// 4. 释放锁 ← 问题在这里!lock.unlock();}// 5. 方法结束,Spring 才会提交事务}

时序问题

线程A 线程B │ │ ├── 加锁成功 │ ├── 查库存=100 │ ├── 扣减为90 │ ├── save(未提交!) │ ├── 释放锁 ←─────────────────── 此时事务还没提交! │ ├── 加锁成功 │ ├── 查库存=100 ← 读到了旧值! │ ├── 扣减为90 │ ├── save ├── 事务提交(库存=90) ├── 事务提交(库存=90) │ │ 结果:扣了两次,但库存只减了10,丢失了一次扣减!

三、正确做法:事务提交后再释放锁

@TransactionalpublicvoidupdateInventory(IntegerskuId,Integerquantity){// 1. 加锁DistributedLocklock=lockProvider.getLock("inventory-"+skuId,TimeUnit.MINUTES,2);if(!lock.tryLock(TimeUnit.SECONDS,30)){thrownewRuntimeException("获取锁超时");}// 2. 注册事务完成后释放锁(无论提交还是回滚都释放)AfterTransactionActionCollectorcollector=newAfterTransactionActionCollector();collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);TransactionSynchronizationManager.registerSynchronization(collector);// 3. 执行业务逻辑Inventoryinventory=inventoryRepository.findBySkuId(skuId);inventory.setQuantity(inventory.getQuantity()-quantity);inventoryRepository.save(inventory);}

正确的时序

线程A 线程B │ │ ├── 加锁成功 │ ├── 查库存=100 │ ├── 扣减为90 │ ├── save │ ├── 事务提交(库存=90) │ ├── afterCommit → 释放锁 ─────── 此时数据已经持久化 │ ├── 加锁成功 │ ├── 查库存=90 ← 读到了正确的值 │ ├── 扣减为80 │ ├── save │ ├── 事务提交(库存=80) │ ├── 释放锁 │ │ 结果:两次扣减都正确生效,库存从100→90→80

四、核心原理

4.1 事务隔离级别与可见性

在 MySQL 默认的REPEATABLE READ隔离级别下:

  • 事务内的修改,在 COMMIT 之前,其他事务是看不到
  • 只有 COMMIT 之后,修改才对其他事务可见

所以如果锁在 COMMIT 之前释放,其他线程拿到锁后读到的还是旧数据。

4.2 锁的持有时间 = 事务的完整生命周期

加锁 ──────────────────────────────────────── 释放锁 │ │ │ ┌─── 事务开始 ───────── 事务提交 ───┐ │ │ │ │ │ │ │ 查询 → 计算 → 写入 │ │ │ │ │ │ │ └───────────────────────────────────┘ │ │ │ └──────────── 锁必须覆盖整个事务 ──────────────┘

4.3 为什么回滚时也要释放锁

collector.addCommitSyncAction(lock::unlock);// 提交后释放collector.addRollbackSyncAction(lock::unlock);// 回滚后也释放

如果事务回滚了但不释放锁,这把锁就会一直被持有,直到超时自动释放。在超时之前,其他线程都无法获取锁,造成业务阻塞。


五、分布式锁基础知识

5.1 什么是分布式锁

在单机环境中,Java 的synchronizedReentrantLock可以保证线程安全。但在分布式环境(多个服务实例)中,这些本地锁无效,需要一个所有实例都能访问的"中央锁服务"。

常见实现:

实现方式原理优点缺点
RedisSETNX + 过期时间性能高,使用广泛主从切换时可能丢锁
ZooKeeper临时有序节点强一致性性能较低
数据库唯一索引/行锁无需额外中间件性能差,不推荐

5.2 分布式锁的核心API

publicinterfaceDistributedLock{/** * 尝试加锁,等待指定时间. * @return true=加锁成功,false=超时未获取到 */booleantryLock(TimeUnitunit,longtimeout);/** * 释放锁. */voidunlock();}publicinterfaceDistributedLockProvider{/** * 获取一把锁. * @param key 锁的唯一标识 * @param unit 锁的最大持有时间单位 * @param duration 锁的最大持有时间(防止死锁的兜底) */DistributedLockgetLock(Stringkey,TimeUnitunit,longduration);}

5.3 锁的超时时间

// 锁最多持有2分钟,超时自动释放(防止死锁)DistributedLocklock=lockProvider.getLock("order-process-"+orderId,TimeUnit.MINUTES,2);

超时时间的设置原则:

  • 必须大于业务方法的最大执行时间
  • 不能太长,否则异常退出时其他线程等待时间过久
  • 一般设置为业务耗时的 2-3 倍

六、完整示例:防止订单重复处理

6.1 业务场景

MQ 消费者可能重复收到同一条消息(网络重试、消费者重启等),需要保证同一订单不会被并发处理。

6.2 完整代码

@ServicepublicclassOrderProcessServiceImplimplementsOrderProcessService{@ResourceprivateDistributedLockProviderdistributedLockProvider;@ResourceprivateOrderRepositoryorderRepository;@ResourceprivateStockServicestockService;@ResourceprivatePaymentServicepaymentService;/** * 处理订单(MQ消费者调用). * 使用分布式锁防止同一订单被并发处理. */@Transactional(rollbackFor=Exception.class)publicvoidprocessOrder(IntegerorderId){// ====== 第一步:加锁 ======StringlockKey="order-process-"+orderId;DistributedLocklock=distributedLockProvider.getLock(lockKey,TimeUnit.MINUTES,2);// 等待锁,最多等30秒if(!lock.tryLock(TimeUnit.SECONDS,30)){log.warn("获取订单处理锁超时, orderId:{}",orderId);thrownewRuntimeException("订单正在处理中,请稍后重试");}// ====== 第二步:注册事务完成后释放锁 ======AfterTransactionActionCollectorcollector=newAfterTransactionActionCollector();collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);TransactionSynchronizationManager.registerSynchronization(collector);// ====== 第三步:执行业务逻辑 ======// 查询订单Orderorder=orderRepository.findById(orderId).orElse(null);if(order==null){log.warn("订单不存在, orderId:{}",orderId);return;}// 幂等检查:已处理的订单直接跳过if("PROCESSED".equals(order.getStatus())){log.info("订单已处理,跳过, orderId:{}",orderId);return;}// 扣减库存stockService.deductStock(order.getSkuId(),order.getQuantity());// 扣款paymentService.charge(order.getUserId(),order.getAmount());// 更新订单状态order.setStatus("PROCESSED");orderRepository.save(order);log.info("订单处理完成, orderId:{}",orderId);}}

6.3 执行流程图

MQ消费者收到消息(orderId=123) │ ▼ 加锁:order-process-123 │ ├── 加锁成功 │ │ │ ▼ │ 注册事务后释放锁的回调 │ │ │ ▼ │ 查询订单 → 幂等检查 → 扣库存 → 扣款 → 更新状态 │ │ │ ▼ │ 事务提交(所有数据库操作生效) │ │ │ ▼ │ afterCommit → 释放锁 │ └── 加锁失败(超时) │ ▼ 抛异常 → MQ稍后重试

七、锁的粒度设计

7.1 锁的 Key 决定了并发控制的范围

// 粗粒度:按会员维度加锁(同一会员的所有操作串行)StringlockKey="member-"+memberId;// 细粒度:按订单维度加锁(只有同一订单的操作串行)StringlockKey="order-"+orderId;// 更细粒度:按SKU维度加锁(只有同一商品的库存操作串行)StringlockKey="stock-"+skuId;
粒度并发度安全性适用场景
粗(会员级)涉及会员多个资源的操作
中(订单级)订单状态变更
细(SKU级)需要额外保证库存扣减

7.2 锁 Key 的命名规范

// 推荐格式:业务域-操作-唯一标识"inventory-deduct-"+skuId"order-process-"+orderId"delivery-cancel-"+deliveryCode"cs-outbound-flow-to-ylh-"+memberId

八、常见陷阱

8.1 锁超时但事务还没结束

线程A 加锁(超时2分钟) │ ├── 开始处理(业务耗时3分钟) │ ├── 2分钟后:锁自动释放! │ 线程B 加锁成功 │ 线程B 开始处理同一数据 │ ├── 3分钟后:线程A事务提交 │ 线程B 事务提交 │ 结果:数据被覆盖,出现并发问题

解决方案

  • 锁的超时时间要大于业务最大耗时
  • 使用看门狗机制自动续期(如 Redisson 的 watchdog)

8.2 加锁在事务外面

// 错误:锁在事务外加,事务内释放publicvoidouterMethod(IntegerorderId){DistributedLocklock=lockProvider.getLock("order-"+orderId);lock.tryLock();try{innerTransactionalMethod(orderId);// @Transactional}finally{lock.unlock();// 此时事务可能还没提交!}}

这种情况下,innerTransactionalMethod的事务可能还没提交,锁就被释放了。正确做法是把锁的释放放到事务同步回调中。

8.3 忘记释放锁

@TransactionalpublicvoidprocessOrder(IntegerorderId){DistributedLocklock=lockProvider.getLock("order-"+orderId);lock.tryLock();// 如果这里抛异常,锁永远不会释放(直到超时)Orderorder=orderRepository.findById(orderId).orElseThrow();// ...}

解决方案:使用事务同步回调,无论提交还是回滚都释放锁。

collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);

8.4 可重入性问题

@TransactionalpublicvoidmethodA(IntegerorderId){DistributedLocklock=lockProvider.getLock("order-"+orderId);lock.tryLock();// ...methodB(orderId);// 内部也尝试加同一把锁}@TransactionalpublicvoidmethodB(IntegerorderId){DistributedLocklock=lockProvider.getLock("order-"+orderId);lock.tryLock();// 如果锁不支持可重入,这里会死锁!}

解决方案:使用支持可重入的分布式锁实现(如 Redisson 的 RLock)。


九、与本地锁的对比

// 本地锁(只在单个JVM内有效)privatefinalReentrantLocklocalLock=newReentrantLock();publicvoidlocalMethod(){localLock.lock();try{// 业务逻辑}finally{localLock.unlock();}}// 分布式锁(跨多个JVM实例有效)publicvoiddistributedMethod(){DistributedLocklock=lockProvider.getLock("key");lock.tryLock();// 注册事务后释放collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);// 业务逻辑}
维度本地锁分布式锁
作用范围单个JVM进程跨多个服务实例
实现方式synchronized/ReentrantLockRedis/ZooKeeper
性能纳秒级毫秒级(网络IO)
可靠性进程崩溃自动释放需要超时机制兜底
适用场景单机部署集群/微服务部署

十、总结

问题答案
为什么锁不能在事务内释放?释放锁后其他线程可能读到未提交的数据
为什么回滚时也要释放锁?避免锁被永久持有导致其他线程阻塞
锁的超时时间怎么设?业务最大耗时的 2-3 倍
锁的 Key 怎么设计?业务域-操作-唯一标识,粒度越细并发度越高
和 try-finally 释放有什么区别?try-finally 在事务提交前释放,事务回调在提交后释放
http://www.jsqmd.com/news/868541/

相关文章:

  • OAuthlib错误排查实战:从invalid_grant到server_error的根因定位
  • 面试:如果让你设计一个客服 Agent,你会如何划分四大组件的职责?
  • Keil µVision TAB显示异常问题分析与解决方案
  • Agentic o3调度器与Gemma/Nemotron-H推理范式演进
  • 量子退火与模拟退火在组合优化中的应用对比
  • 加拿大AI公共咨询:以人为本的政府技术治理实践
  • NXP MX芯片EMOV指令周期分析与优化
  • 解锁Linux无线网卡配置:RTL8821CU驱动实战深度指南
  • Frida-ps -U 连接失败的五层排查法
  • 量子纠错码与逻辑门优化实现技术解析
  • GE图引擎架构剖析:怎么做到“代码零修改,性能最大化“
  • 用 PS 抠公章最详细步骤|零基础一键抠取透明公章
  • 量子态相似性度量:迹距离与保真度的工程应用
  • 8051串口通信:Keil µVision输入失效问题解析
  • UDS_自动化脚本生成_10服务_V01
  • 去哪儿旅行Bella参数逆向解析:HMAC-SHA256前端签名与Python复现
  • AI国家安全治理:从动态阈值到人机协同的操作化路径
  • 量子扩散模型:量子物理与生成式AI的融合创新
  • 图神经网络在高能物理暗物质探测中的实战应用
  • 海克斯大乱斗:普攻英雄“锻体”收益的严谨数学分析
  • 【紧急预警】Lovable v4.8.2存在未公开API权限漏洞!立即升级+3行代码热修复方案(仅限前500名开发者获取)
  • 暗物质AI建模:物理约束嵌入与可解释神经网络实践
  • Frida绕过Android签名校验实战指南
  • 从账单明细分析不同模型在代码生成任务上的性价比
  • AI Agent Harness状态管理:长对话上下文维护
  • Frida-ps-U连接失败的五层故障排查指南
  • 好莱坞已悄悄启用AI拍片:2024年7部奥斯卡入围作品背后的生成式视频技术全拆解
  • Android签名校验绕过实战:Frida动态Hook四层防御体系
  • Anthropic Managed Agents:智能体运行时的归零时刻与工程范式升级
  • IDECNN:基于改进差分进化的可复现CNN架构搜索方法