
分布式锁与事务配合为什么锁要在事务提交后释放一、问题引入在分布式系统中多个实例可能同时处理同一条数据。为了防止并发冲突我们用分布式锁来保证同一时刻只有一个线程在操作某条数据。但一个常见的错误是在事务提交之前就释放了锁导致其他线程读到了未提交的中间状态。二、错误示例锁在事务内释放TransactionalpublicvoidupdateInventory(IntegerskuId,Integerquantity){// 1. 加锁DistributedLocklocklockProvider.getLock(inventory-skuId);lock.tryLock();try{// 2. 查询当前库存InventoryinventoryinventoryRepository.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. 加锁DistributedLocklocklockProvider.getLock(inventory-skuId,TimeUnit.MINUTES,2);if(!lock.tryLock(TimeUnit.SECONDS,30)){thrownewRuntimeException(获取锁超时);}// 2. 注册事务完成后释放锁无论提交还是回滚都释放AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);TransactionSynchronizationManager.registerSynchronization(collector);// 3. 执行业务逻辑InventoryinventoryinventoryRepository.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 的synchronized或ReentrantLock可以保证线程安全。但在分布式环境多个服务实例中这些本地锁无效需要一个所有实例都能访问的中央锁服务。常见实现实现方式原理优点缺点RedisSETNX 过期时间性能高使用广泛主从切换时可能丢锁ZooKeeper临时有序节点强一致性性能较低数据库唯一索引/行锁无需额外中间件性能差不推荐5.2 分布式锁的核心APIpublicinterfaceDistributedLock{/** * 尝试加锁等待指定时间. * return true加锁成功false超时未获取到 */booleantryLock(TimeUnitunit,longtimeout);/** * 释放锁. */voidunlock();}publicinterfaceDistributedLockProvider{/** * 获取一把锁. * param key 锁的唯一标识 * param unit 锁的最大持有时间单位 * param duration 锁的最大持有时间防止死锁的兜底 */DistributedLockgetLock(Stringkey,TimeUnitunit,longduration);}5.3 锁的超时时间// 锁最多持有2分钟超时自动释放防止死锁DistributedLocklocklockProvider.getLock(order-process-orderId,TimeUnit.MINUTES,2);超时时间的设置原则必须大于业务方法的最大执行时间不能太长否则异常退出时其他线程等待时间过久一般设置为业务耗时的 2-3 倍六、完整示例防止订单重复处理6.1 业务场景MQ 消费者可能重复收到同一条消息网络重试、消费者重启等需要保证同一订单不会被并发处理。6.2 完整代码ServicepublicclassOrderProcessServiceImplimplementsOrderProcessService{ResourceprivateDistributedLockProviderdistributedLockProvider;ResourceprivateOrderRepositoryorderRepository;ResourceprivateStockServicestockService;ResourceprivatePaymentServicepaymentService;/** * 处理订单MQ消费者调用. * 使用分布式锁防止同一订单被并发处理. */Transactional(rollbackForException.class)publicvoidprocessOrder(IntegerorderId){// 第一步加锁 StringlockKeyorder-process-orderId;DistributedLocklockdistributedLockProvider.getLock(lockKey,TimeUnit.MINUTES,2);// 等待锁最多等30秒if(!lock.tryLock(TimeUnit.SECONDS,30)){log.warn(获取订单处理锁超时, orderId:{},orderId);thrownewRuntimeException(订单正在处理中请稍后重试);}// 第二步注册事务完成后释放锁 AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);TransactionSynchronizationManager.registerSynchronization(collector);// 第三步执行业务逻辑 // 查询订单OrderorderorderRepository.findById(orderId).orElse(null);if(ordernull){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消费者收到消息orderId123 │ ▼ 加锁order-process-123 │ ├── 加锁成功 │ │ │ ▼ │ 注册事务后释放锁的回调 │ │ │ ▼ │ 查询订单 → 幂等检查 → 扣库存 → 扣款 → 更新状态 │ │ │ ▼ │ 事务提交所有数据库操作生效 │ │ │ ▼ │ afterCommit → 释放锁 │ └── 加锁失败超时 │ ▼ 抛异常 → MQ稍后重试七、锁的粒度设计7.1 锁的 Key 决定了并发控制的范围// 粗粒度按会员维度加锁同一会员的所有操作串行StringlockKeymember-memberId;// 细粒度按订单维度加锁只有同一订单的操作串行StringlockKeyorder-orderId;// 更细粒度按SKU维度加锁只有同一商品的库存操作串行StringlockKeystock-skuId;粒度并发度安全性适用场景粗会员级低高涉及会员多个资源的操作中订单级中中订单状态变更细SKU级高需要额外保证库存扣减7.2 锁 Key 的命名规范// 推荐格式业务域-操作-唯一标识inventory-deduct-skuIdorder-process-orderIddelivery-cancel-deliveryCodecs-outbound-flow-to-ylh-memberId八、常见陷阱8.1 锁超时但事务还没结束线程A 加锁超时2分钟 │ ├── 开始处理业务耗时3分钟 │ ├── 2分钟后锁自动释放 │ 线程B 加锁成功 │ 线程B 开始处理同一数据 │ ├── 3分钟后线程A事务提交 │ 线程B 事务提交 │ 结果数据被覆盖出现并发问题解决方案锁的超时时间要大于业务最大耗时使用看门狗机制自动续期如 Redisson 的 watchdog8.2 加锁在事务外面// 错误锁在事务外加事务内释放publicvoidouterMethod(IntegerorderId){DistributedLocklocklockProvider.getLock(order-orderId);lock.tryLock();try{innerTransactionalMethod(orderId);// Transactional}finally{lock.unlock();// 此时事务可能还没提交}}这种情况下innerTransactionalMethod的事务可能还没提交锁就被释放了。正确做法是把锁的释放放到事务同步回调中。8.3 忘记释放锁TransactionalpublicvoidprocessOrder(IntegerorderId){DistributedLocklocklockProvider.getLock(order-orderId);lock.tryLock();// 如果这里抛异常锁永远不会释放直到超时OrderorderorderRepository.findById(orderId).orElseThrow();// ...}解决方案使用事务同步回调无论提交还是回滚都释放锁。collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);8.4 可重入性问题TransactionalpublicvoidmethodA(IntegerorderId){DistributedLocklocklockProvider.getLock(order-orderId);lock.tryLock();// ...methodB(orderId);// 内部也尝试加同一把锁}TransactionalpublicvoidmethodB(IntegerorderId){DistributedLocklocklockProvider.getLock(order-orderId);lock.tryLock();// 如果锁不支持可重入这里会死锁}解决方案使用支持可重入的分布式锁实现如 Redisson 的 RLock。九、与本地锁的对比// 本地锁只在单个JVM内有效privatefinalReentrantLocklocalLocknewReentrantLock();publicvoidlocalMethod(){localLock.lock();try{// 业务逻辑}finally{localLock.unlock();}}// 分布式锁跨多个JVM实例有效publicvoiddistributedMethod(){DistributedLocklocklockProvider.getLock(key);lock.tryLock();// 注册事务后释放collector.addCommitSyncAction(lock::unlock);collector.addRollbackSyncAction(lock::unlock);// 业务逻辑}维度本地锁分布式锁作用范围单个JVM进程跨多个服务实例实现方式synchronized/ReentrantLockRedis/ZooKeeper性能纳秒级毫秒级网络IO可靠性进程崩溃自动释放需要超时机制兜底适用场景单机部署集群/微服务部署十、总结问题答案为什么锁不能在事务内释放释放锁后其他线程可能读到未提交的数据为什么回滚时也要释放锁避免锁被永久持有导致其他线程阻塞锁的超时时间怎么设业务最大耗时的 2-3 倍锁的 Key 怎么设计业务域-操作-唯一标识粒度越细并发度越高和 try-finally 释放有什么区别try-finally 在事务提交前释放事务回调在提交后释放