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

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

一、问题引入

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

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


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

java 复制代码
@Transactional
public void updateInventory(Integer skuId, Integer quantity) {
    // 1. 加锁
    DistributedLock lock = lockProvider.getLock("inventory-" + skuId);
    lock.tryLock();

    try {
        // 2. 查询当前库存
        Inventory inventory = 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,丢失了一次扣减!

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

java 复制代码
@Transactional
public void updateInventory(Integer skuId, Integer quantity) {
    // 1. 加锁
    DistributedLock lock = lockProvider.getLock(
        "inventory-" + skuId, TimeUnit.MINUTES, 2);
    if (!lock.tryLock(TimeUnit.SECONDS, 30)) {
        throw new RuntimeException("获取锁超时");
    }

    // 2. 注册事务完成后释放锁(无论提交还是回滚都释放)
    AfterTransactionActionCollector collector =
        new AfterTransactionActionCollector();
    collector.addCommitSyncAction(lock::unlock);
    collector.addRollbackSyncAction(lock::unlock);
    TransactionSynchronizationManager.registerSynchronization(collector);

    // 3. 执行业务逻辑
    Inventory inventory = 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 为什么回滚时也要释放锁

java 复制代码
collector.addCommitSyncAction(lock::unlock);    // 提交后释放
collector.addRollbackSyncAction(lock::unlock);  // 回滚后也释放

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


五、分布式锁基础知识

5.1 什么是分布式锁

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

常见实现:

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

5.2 分布式锁的核心API

java 复制代码
public interface DistributedLock {

    /**
     * 尝试加锁,等待指定时间.
     * @return true=加锁成功,false=超时未获取到
     */
    boolean tryLock(TimeUnit unit, long timeout);

    /**
     * 释放锁.
     */
    void unlock();
}

public interface DistributedLockProvider {

    /**
     * 获取一把锁.
     * @param key 锁的唯一标识
     * @param unit 锁的最大持有时间单位
     * @param duration 锁的最大持有时间(防止死锁的兜底)
     */
    DistributedLock getLock(String key, TimeUnit unit, long duration);
}

5.3 锁的超时时间

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

超时时间的设置原则:

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

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

6.1 业务场景

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

6.2 完整代码

java 复制代码
@Service
public class OrderProcessServiceImpl implements OrderProcessService {

    @Resource
    private DistributedLockProvider distributedLockProvider;

    @Resource
    private OrderRepository orderRepository;

    @Resource
    private StockService stockService;

    @Resource
    private PaymentService paymentService;

    /**
     * 处理订单(MQ消费者调用).
     * 使用分布式锁防止同一订单被并发处理.
     */
    @Transactional(rollbackFor = Exception.class)
    public void processOrder(Integer orderId) {
        // ====== 第一步:加锁 ======
        String lockKey = "order-process-" + orderId;
        DistributedLock lock = distributedLockProvider.getLock(
            lockKey, TimeUnit.MINUTES, 2);

        // 等待锁,最多等30秒
        if (!lock.tryLock(TimeUnit.SECONDS, 30)) {
            log.warn("获取订单处理锁超时, orderId:{}", orderId);
            throw new RuntimeException("订单正在处理中,请稍后重试");
        }

        // ====== 第二步:注册事务完成后释放锁 ======
        AfterTransactionActionCollector collector =
            new AfterTransactionActionCollector();
        collector.addCommitSyncAction(lock::unlock);
        collector.addRollbackSyncAction(lock::unlock);
        TransactionSynchronizationManager
            .registerSynchronization(collector);

        // ====== 第三步:执行业务逻辑 ======
        // 查询订单
        Order order = 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 决定了并发控制的范围

java 复制代码
// 粗粒度:按会员维度加锁(同一会员的所有操作串行)
String lockKey = "member-" + memberId;

// 细粒度:按订单维度加锁(只有同一订单的操作串行)
String lockKey = "order-" + orderId;

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

7.2 锁 Key 的命名规范

java 复制代码
// 推荐格式:业务域-操作-唯一标识
"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 加锁在事务外面

java 复制代码
// 错误:锁在事务外加,事务内释放
public void outerMethod(Integer orderId) {
    DistributedLock lock = lockProvider.getLock("order-" + orderId);
    lock.tryLock();
    try {
        innerTransactionalMethod(orderId); // @Transactional
    } finally {
        lock.unlock(); // 此时事务可能还没提交!
    }
}

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

8.3 忘记释放锁

java 复制代码
@Transactional
public void processOrder(Integer orderId) {
    DistributedLock lock = lockProvider.getLock("order-" + orderId);
    lock.tryLock();

    // 如果这里抛异常,锁永远不会释放(直到超时)
    Order order = orderRepository.findById(orderId).orElseThrow();
    // ...
}

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

java 复制代码
collector.addCommitSyncAction(lock::unlock);
collector.addRollbackSyncAction(lock::unlock);

8.4 可重入性问题

java 复制代码
@Transactional
public void methodA(Integer orderId) {
    DistributedLock lock = lockProvider.getLock("order-" + orderId);
    lock.tryLock();
    // ...
    methodB(orderId); // 内部也尝试加同一把锁
}

@Transactional
public void methodB(Integer orderId) {
    DistributedLock lock = lockProvider.getLock("order-" + orderId);
    lock.tryLock(); // 如果锁不支持可重入,这里会死锁!
}

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


九、与本地锁的对比

java 复制代码
// 本地锁(只在单个JVM内有效)
private final ReentrantLock localLock = new ReentrantLock();

public void localMethod() {
    localLock.lock();
    try {
        // 业务逻辑
    } finally {
        localLock.unlock();
    }
}

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

十、总结

问题 答案
为什么锁不能在事务内释放? 释放锁后其他线程可能读到未提交的数据
为什么回滚时也要释放锁? 避免锁被永久持有导致其他线程阻塞
锁的超时时间怎么设? 业务最大耗时的 2-3 倍
锁的 Key 怎么设计? 业务域-操作-唯一标识,粒度越细并发度越高
和 try-finally 释放有什么区别? try-finally 在事务提交前释放,事务回调在提交后释放
相关推荐
muqsen7 小时前
Java 分布式相关面试题总结
java·开发语言·分布式
phltxy8 小时前
RabbitMQ 入门与安装
分布式·rabbitmq
阿坤带你走近大数据8 小时前
Kafka的基本概念,基本用法及常见使用场景
分布式·kafka
逻极8 小时前
RabbitMQ 从入门到精通:构建高可用、高性能的消息中间件系统
分布式·rabbitmq·消息中间件
Lyyaoo.8 小时前
Kafka快速入门
分布式·kafka
懂AI的老郑8 小时前
OpenClaw:高效管理分布式Agent开发团队
分布式·ai编程
来自星星的谢广坤8 小时前
OpenClaw做分布式合适吗?
分布式·openclaw
元拓数智19 小时前
智能分析落地卡壳?先补好「数据关系+语义治理」这层技术基建
大数据·分布式·ai·spark·数据关系·语义治理
GIS数据转换器1 天前
农村生活污水治理智慧管控平台
大数据·人工智能·分布式·数据分析·生活·智慧城市