分布式锁防并发 与 事务后置动作

分布式锁防并发 与 事务后置动作


一、分布式锁防并发

1.1 解决什么问题

在分布式微服务环境下,同一个请求可能因为用户重复点击、MQ 重试、定时任务并发等原因被多个线程/多个实例同时执行。Java 的 synchronizedReentrantLock 只能锁住单个 JVM 进程内的线程,跨实例无效。

分布式锁通过外部中间件(Redis、ZooKeeper、数据库)提供跨进程、跨机器的互斥能力。

1.2 核心概念

概念 说明
锁粒度 按业务 key 加锁(如订单号),不同订单不互斥,同一订单互斥
获取方式 阻塞等待(tryLock with timeout)或立即失败(tryLock 0ms)
自动释放 设置过期时间,防止持有者崩溃后死锁
可重入 同一线程可重复获取同一把锁(取决于实现)
Redisson vs 自建 Redisson 提供看门狗续期、可重入、公平锁等高级特性;自建一般用 SET NX EX

1.3 Redis 分布式锁原理

复制代码
加锁: SET lock_key unique_value NX PX 30000
        → NX: 只有 key 不存在时才设置(互斥)
        → PX: 30 秒后自动过期(防死锁)

释放: 用 Lua 脚本保证原子性
      if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
      end

1.4 代码示例(通用)

java 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class OrderService {

    private final StringRedisTemplate redisTemplate;

    // 释放锁的 Lua 脚本:确保只有持有者能释放
    private static final String RELEASE_SCRIPT =
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "  return redis.call('del', KEYS[1]) " +
        "else " +
        "  return 0 " +
        "end";

    public OrderService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void processOrder(String orderId) {
        String lockKey = "lock:order:" + orderId;
        String lockValue = UUID.randomUUID().toString(); // 唯一标识,防止误删他人锁
        boolean locked = false;

        try {
            // 尝试加锁,30秒自动过期
            locked = Boolean.TRUE.equals(
                redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS)
            );

            if (!locked) {
                throw new RuntimeException("订单正在处理中,请勿重复提交");
            }

            // ========= 业务逻辑 =========
            doBusinessLogic(orderId);
            // ============================

        } finally {
            if (locked) {
                // 原子释放:只释放自己加的锁
                DefaultRedisScript<Long> script = new DefaultRedisScript<>(RELEASE_SCRIPT, Long.class);
                redisTemplate.execute(script, Collections.singletonList(lockKey), lockValue);
            }
        }
    }

    private void doBusinessLogic(String orderId) {
        // 扣库存、生成发货单等...
    }
}

1.5 使用 try-with-resources 封装

java 复制代码
/**
 * 分布式锁封装,实现 AutoCloseable 支持 try-with-resources.
 */
public class RedisDistributedLock implements AutoCloseable {

    private final StringRedisTemplate redisTemplate;
    private final String lockKey;
    private final String lockValue;
    private boolean acquired = false;

    public RedisDistributedLock(StringRedisTemplate redisTemplate, String lockKey) {
        this.redisTemplate = redisTemplate;
        this.lockKey = lockKey;
        this.lockValue = UUID.randomUUID().toString();
    }

    public boolean tryLock(long timeout, TimeUnit unit) {
        this.acquired = Boolean.TRUE.equals(
            redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, timeout, unit)
        );
        return this.acquired;
    }

    @Override
    public void close() {
        if (acquired) {
            String script =
                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
                "  return redis.call('del', KEYS[1]) " +
                "else return 0 end";
            redisTemplate.execute(
                new DefaultRedisScript<>(script, Long.class),
                Collections.singletonList(lockKey), lockValue
            );
        }
    }
}

使用方式:

java 复制代码
try (RedisDistributedLock lock = new RedisDistributedLock(redisTemplate, "lock:order:" + orderId)) {
    if (lock.tryLock(30, TimeUnit.SECONDS)) {
        doBusinessLogic(orderId);
    } else {
        throw new RuntimeException("获取锁失败");
    }
}
// close() 自动释放锁

1.6 注意事项

问题 解决
锁过期但业务未执行完 Redisson 看门狗机制自动续期;或评估好超时时间
Redis 主从切换丢锁 RedLock 算法(多数派加锁),但复杂度高,非强一致场景一般不用
锁粒度太粗 lock:order:{orderId} 而非 lock:order,避免全局串行
释放他人的锁 用 UUID 标记持有者,Lua 脚本原子校验

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、事务编排 + 事务后置动作

2.1 解决什么问题

一个典型场景:数据库写入成功后需要发 MQ 消息通知下游。如果在同一个事务方法中直接发消息:

  • 消息已发,但事务回滚 → 下游收到脏消息
  • 事务已提交,但消息发送失败 → 下游丢消息

事务后置动作保证:只有事务成功提交后才执行消息发送等副作用操作。

2.2 Spring 事务同步机制

Spring 提供了 TransactionSynchronization 接口,可以注册回调在事务的不同阶段执行:

java 复制代码
public interface TransactionSynchronization {
    void beforeCommit(boolean readOnly);  // 提交前
    void beforeCompletion();              // 完成前(无论成功失败)
    void afterCommit();                   // 提交成功后 ★
    void afterCompletion(int status);     // 完成后(带状态码)
}

通过 TransactionSynchronizationManager.registerSynchronization() 注册。

2.3 核心知识点

知识点 说明
@Transactional 方法级事务,Spring AOP 代理管理 begin/commit/rollback
TransactionSynchronizationManager 线程绑定的事务同步管理器,每个事务可注册多个回调
afterCommit() 事务提交成功后触发,此时数据已持久化,适合发 MQ/调外部接口
afterCompletion(STATUS_ROLLED_BACK) 事务回滚后触发,适合做补偿/告警
Propagation 嵌套事务(REQUIRES_NEW)场景下,同步回调跟随各自事务独立触发

2.4 代码示例(通用)

java 复制代码
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final MessageProducer messageProducer;
    private final NotificationService notificationService;

    public PaymentService(PaymentRepository paymentRepository,
                          MessageProducer messageProducer,
                          NotificationService notificationService) {
        this.paymentRepository = paymentRepository;
        this.messageProducer = messageProducer;
        this.notificationService = notificationService;
    }

    @Transactional(rollbackFor = Exception.class)
    public void completePayment(String paymentId, String userId) {
        // ======= 事务内操作(数据库) =======
        Payment payment = paymentRepository.findById(paymentId)
            .orElseThrow(() -> new RuntimeException("支付单不存在"));
        payment.setStatus("COMPLETED");
        paymentRepository.save(payment);

        // 扣减库存
        inventoryRepository.deductStock(payment.getProductId(), payment.getQuantity());

        // ======= 注册事务后置动作 =======
        // 只有上面的 save + deductStock 都成功提交后,才会执行以下逻辑
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronization() {

                @Override
                public void afterCommit() {
                    // 发送 MQ 消息通知物流系统
                    messageProducer.send("payment.completed", paymentId);

                    // 发送用户通知
                    notificationService.notifyUser(userId, "您的支付已完成");
                }

                @Override
                public void afterCompletion(int status) {
                    if (status == STATUS_ROLLED_BACK) {
                        // 事务回滚后的补偿逻辑(如告警)
                        log.warn("支付事务回滚, paymentId={}", paymentId);
                    }
                }
            }
        );
    }
}

2.5 封装为可复用的 Collector 工具类

可使用 AfterTransactionActionCollector 来简化注册多个后置动作的场景:

java 复制代码
import org.springframework.transaction.support.TransactionSynchronization;
import java.util.ArrayList;
import java.util.List;

/**
 * 事务后置动作收集器.
 * 在事务方法中收集多个后置动作,事务提交后统一执行.
 */
public class AfterTransactionActionCollector implements TransactionSynchronization {

    private final List<Runnable> commitActions = new ArrayList<>();
    private final List<Runnable> rollbackActions = new ArrayList<>();

    /** 添加事务提交后执行的动作. */
    public void addCommitSyncAction(Runnable action) {
        commitActions.add(action);
    }

    /** 添加事务回滚后执行的动作. */
    public void addRollbackAction(Runnable action) {
        rollbackActions.add(action);
    }

    @Override
    public void afterCommit() {
        for (Runnable action : commitActions) {
            try {
                action.run();
            } catch (Exception e) {
                // 后置动作失败不影响已提交的事务,仅记录日志
                log.error("事务后置动作执行失败", e);
            }
        }
    }

    @Override
    public void afterCompletion(int status) {
        if (status == STATUS_ROLLED_BACK) {
            for (Runnable action : rollbackActions) {
                try {
                    action.run();
                } catch (Exception e) {
                    log.error("回滚后置动作执行失败", e);
                }
            }
        }
    }
}

使用方式:

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderRequest request) {
    // 数据库操作
    Order order = orderRepository.save(buildOrder(request));

    // 收集多个后置动作
    AfterTransactionActionCollector collector = new AfterTransactionActionCollector();

    collector.addCommitSyncAction(() -> 
        mqProducer.send("order.created", order.getId())
    );
    collector.addCommitSyncAction(() -> 
        pushService.pushToUser(order.getUserId(), "下单成功")
    );
    collector.addRollbackAction(() -> 
        alertService.alert("订单创建事务回滚: " + request.getOrderNo())
    );

    // 注册到当前事务
    TransactionSynchronizationManager.registerSynchronization(collector);
}

2.6 与其他方案对比

方案 优点 缺点
事务后置动作(本方案) 简单直接,无额外中间件 应用崩溃时消息可能丢失
本地消息表 可靠性最高,可重试 需要额外表 + 定时补偿任务
RocketMQ 事务消息 中间件级保障 依赖特定 MQ 实现,编码复杂
@TransactionalEventListener Spring 原生注解,解耦优雅 事件驱动模式,需额外定义事件类

2.7 注意事项

问题 说明
afterCommit 中抛异常 不会导致事务回滚(已提交),但会中断后续 action,需要 try-catch
没有活跃事务 调用 registerSynchronization 会抛异常,需确保在 @Transactional 方法内
异步 vs 同步 afterCommit 默认同步执行,长耗时操作建议投递到线程池
REQUIRES_NEW 嵌套事务各自独立,内层事务提交时触发内层的 afterCommit,不等外层

三、两者如何配合

在发货场景中,两者组合使用的完整时序:

复制代码
1. 获取分布式锁(Redis)
     ↓ 成功
2. 开启数据库事务(@Transactional)
     ↓
3. 业务逻辑:校验 → 扣库存 → 生成发货单 → 保存明细
     ↓
4. 注册事务后置动作(发 MQ 给物流)
     ↓
5. 事务提交
     ↓
6. afterCommit 触发 → MQ 消息发出
     ↓
7. 释放分布式锁(finally / try-with-resources)

锁保证同一订单不会被并发处理;事务后置保证消息不会因为事务回滚而成为脏数据。