Spring Boot + 事务钩子,完美解决事务并发问题

在 Spring Boot 中,事务并发问题(如脏读、不可重复读、幻读、超卖 / 超扣等)的核心缘由是多线程同时操作同一批数据时,事务隔离级别未匹配业务场景,或缺乏有效的并发控制机制。事务钩子(如
TransactionSynchronization)本身不直接解决并发问题,但能与
事务隔离级别、乐观锁、悲观锁结合,形成 “隔离 + 控制 + 钩子回调” 的完整方案,完美解决并发场景下的数据一致性问题。

本文将从 “问题本质→核心方案→实战实现→避坑指南” 展开,结合 Spring Boot 事务钩子,提供可落地的并发解决方案。

一、先明确:事务并发问题的本质

事务并发的核心矛盾是「多线程对共享数据的读写冲突」,Spring 事务的隔离级别(@Transactional(isolation = …))是基础,但仅靠隔离级别不够:

  • 低隔离级别(如 READ UNCOMMITTED):无法避免脏读 / 不可重复读;
  • 高隔离级别(如 SERIALIZABLE):性能极差,不适合高并发场景;
  • 默认隔离级别(READ COMMITTED):仍可能出现幻读、超卖等问题。

因此,需要隔离级别 + 并发控制(乐观锁 / 悲观锁)+ 事务钩子(回调处理) 三者配合,既保证一致性,又兼顾性能。

二、核心方案:隔离级别 + 乐观锁 + 事务钩子

1. 方案选型逻辑

场景

并发控制方式

事务隔离级别

事务钩子作用

高并发读多写少(如商品库存、积分)

乐观锁(版本号 / 时间戳)

READ COMMITTED(默认)

事务提交后触发异步操作(如记录日志、发送通知),避免阻塞主流程

低并发写多读少(如订单确认、数据审核)

悲观锁(行锁 / 表锁)

REPEATABLE READ

事务完成后释放锁资源,或回调处理后续逻辑

本文以高并发超卖场景为例(最典型的事务并发问题),采用「乐观锁 + 默认隔离级别 + 事务钩子」方案,既保证性能,又解决数据一致性。

2. 关键技术说明

  • 乐观锁:通过数据库字段(如 version)控制,更新时校验版本号,冲突则重试 / 失败(非阻塞,适合高并发);
  • 事务钩子:Spring 的 TransactionSynchronization 或 @TransactionalEventListener,用于在事务提交成功后执行异步操作(如扣减库存后发送消息通知),避免事务内阻塞,同时保证操作仅在事务成功后执行;
  • 事务隔离级别:默认 READ COMMITTED,避免脏读,兼顾性能。

三、实战实现:解决商品超卖问题

环境准备

  • Spring Boot 2.x+
  • MyBatis-Plus(简化 CRUD,自带乐观锁插件)
  • MySQL 8.0(InnoDB 支持行锁和事务)

1. 数据库设计(商品表)

sql

CREATE TABLE `product` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `name` varchar(255) NOT NULL COMMENT '商品名称',
  `stock` int NOT NULL DEFAULT '0' COMMENT '库存',
  `version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

-- 初始化数据:商品ID=1,库存=100
INSERT INTO `product` (`name`, `stock`, `version`) VALUES ('测试商品', 100, 0);

2. 配置乐观锁(MyBatis-Plus)

MyBatis-Plus 提供
OptimisticLockerInnerInterceptor 插件,自动处理版本号校验和更新:

@Configuration
public class MyBatisPlusConfig {
    // 乐观锁插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

3. 实体类与 Mapper

// 实体类(@Version 注解标记乐观锁版本号)
@Data
@TableName("product")
public class Product {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String name;
    private Integer stock; // 库存
    @Version // 乐观锁版本号字段
    private Integer version;
}

// Mapper(继承BaseMapper,MyBatis-Plus自动生成CRUD)
public interface ProductMapper extends BaseMapper<Product> {
    // 自定义扣减库存SQL(也可使用MyBatis-Plus的updateById,自动带版本号)
    @Update("UPDATE product SET stock = stock - #{num}, version = version + 1 WHERE id = #{id} AND version = #{version}")
    int deductStock(@Param("id") Long id, @Param("num") Integer num, @Param("version") Integer version);
}

4. 服务层:事务控制 + 乐观锁重试

核心逻辑:扣减库存时校验版本号,冲突则重试(避免并发导致更新失败),事务提交后通过钩子执行异步操作。

4.1 自定义重试注解(解决乐观锁冲突)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryOnOptimisticLockingFailure {
    int maxRetries() default 3; // 最大重试次数
    long delay() default 100; // 重试间隔(毫秒)
}

4.2 重试切面(AOP 实现乐观锁冲突重试)

@Aspect
@Component
@Slf4j
public class RetryAspect {
    @Around("@annotation(retryOnOptimisticLockingFailure)")
    public Object retry(ProceedingJoinPoint joinPoint, RetryOnOptimisticLockingFailure retryOnOptimisticLockingFailure) throws Throwable {
        int maxRetries = retryOnOptimisticLockingFailure.maxRetries();
        long delay = retryOnOptimisticLockingFailure.delay();
        int retryCount = 0;
        
        while (true) {
            try {
                return joinPoint.proceed(); // 执行目标方法
            } catch (OptimisticLockingFailureException e) { // MyBatis-Plus乐观锁冲突异常
                if (retryCount >= maxRetries) {
                    log.error("乐观锁冲突,重试{}次后失败", maxRetries, e);
                    throw e; // 重试耗尽,抛出异常
                }
                retryCount++;
                log.warn("乐观锁冲突,第{}次重试(间隔{}ms)", retryCount, delay);
                Thread.sleep(delay);
            }
        }
    }
}

4.3 业务服务(事务 + 库存扣减)

java

运行

@Service
@Slf4j
public class ProductService {
    @Autowired
    private ProductMapper productMapper;
    @Autowired
    private ApplicationEventPublisher eventPublisher; // 用于发布事务事件(钩子触发)

    /**
     * 扣减库存(核心方法)
     * 1. @Transactional:保证库存扣减的原子性
     * 2. @RetryOnOptimisticLockingFailure:乐观锁冲突时重试
     */
    @Transactional(rollbackFor = Exception.class)
    @RetryOnOptimisticLockingFailure(maxRetries = 3, delay = 100)
    public void deductStock(Long productId, Integer num) {
        // 1. 查询商品(带版本号)
        Product product = productMapper.selectById(productId);
        if (product == null) {
            throw new RuntimeException("商品不存在");
        }
        // 2. 校验库存
        if (product.getStock() < num) {
            throw new RuntimeException("库存不足");
        }
        // 3. 扣减库存(乐观锁校验:version不匹配则更新失败,抛出OptimisticLockingFailureException)
        int affectedRows = productMapper.deductStock(productId, num, product.getVersion());
        if (affectedRows == 0) {
            throw new OptimisticLockingFailureException("库存扣减失败,乐观锁冲突");
        }
        // 4. 发布事件(事务提交后触发钩子)
        eventPublisher.publishEvent(new StockDeductEvent(productId, num));
    }
}

5. 事务钩子:事务提交后执行异步操作

使用 Spring 的 @
TransactionalEventListener(本质是
TransactionSynchronization 的封装),确保操作仅在事务
提交成功后执行(避免事务回滚但后续操作已执行的问题)。

5.1 定义事件

java

运行

// 库存扣减事件
@Data
public class StockDeductEvent {
    private Long productId;
    private Integer num;

    public StockDeductEvent(Long productId, Integer num) {
        this.productId = productId;
        this.num = num;
    }
}

5.2 事件监听器(事务钩子)

@Component
@Slf4j
public class StockDeductListener {
    /**
     * 事务提交后执行(钩子回调)
     * phase = TransactionPhase.AFTER_COMMIT:默认值,事务提交后触发
     * 其他phase:BEFORE_COMMIT(提交前)、AFTER_ROLLBACK(回滚后)、AFTER_COMPLETION(完成后,无论提交/回滚)
     */
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleStockDeductEvent(StockDeductEvent event) {
        // 异步执行:记录日志、发送消息通知、更新统计数据等
        log.info("事务提交成功,执行后续操作:商品{}扣减库存{},发送消息通知...", event.getProductId(), event.getNum());
        // 示例:调用消息队列发送通知(如RabbitMQ/Kafka)
        // messageProducer.send("stock_deduct_topic", event);
    }
}

6. 测试:验证并发场景下无超卖

使用 JUnit 结合 CountDownLatch 模拟 1000 个并发请求,扣减 100 库存:

@SpringBootTest
public class ProductConcurrentTest {
    @Autowired
    private ProductService productService;

    private static final int CONCURRENT_COUNT = 1000; // 并发数
    private static final CountDownLatch LATCH = new CountDownLatch(CONCURRENT_COUNT);

    @Test
    public void testConcurrentDeductStock() throws InterruptedException {
        Long productId = 1L;
        Integer numPerRequest = 1; // 每个请求扣减1库存

        // 启动1000个线程并发扣减
        for (int i = 0; i < CONCURRENT_COUNT; i++) {
            new Thread(() -> {
                try {
                    productService.deductStock(productId, numPerRequest);
                } catch (Exception e) {
                    log.error("扣减失败:{}", e.getMessage());
                } finally {
                    LATCH.countDown();
                }
            }).start();
        }

        LATCH.await(); // 等待所有线程执行完毕
        Product product = productService.getProductById(productId);
        log.info("最终库存:{}", product.getStock()); // 预期结果:0(无超卖)
    }
}

测试结果

  • 最终库存为 0(无超卖);
  • 部分请求因乐观锁冲突重试后成功,少量重试耗尽的请求抛出 “库存扣减失败”(可返回给前端 “操作过频,请重试”);
  • 事务钩子仅在库存扣减成功后触发,日志正常输出。

四、进阶:悲观锁场景的事务钩子用法

对于低并发写多读少场景(如订单确认),可使用悲观锁(行锁),事务钩子用于释放资源或回调。

1. 悲观锁实现(SQL 行锁)

在查询时添加 FOR UPDATE 关键字,锁住行数据:

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
    // 悲观锁:查询订单时锁住行,避免并发修改
    @Select("SELECT * FROM `order` WHERE id = #{id} FOR UPDATE")
    Order selectByIdForUpdate(Long id);
}

2. 服务层(事务 + 悲观锁)

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Transactional(rollbackFor = Exception.class, isolation = Isolation.REPEATABLE_READ)
    public void confirmOrder(Long orderId) {
        // 1. 悲观锁查询订单(锁住行,其他线程需等待事务结束)
        Order order = orderMapper.selectByIdForUpdate(orderId);
        if (order == null) {
            throw new RuntimeException("订单不存在");
        }
        if (order.getStatus() == 1) {
            throw new RuntimeException("订单已确认");
        }
        // 2. 更新订单状态
        order.setStatus(1);
        orderMapper.updateById(order);
        // 3. 发布事件(事务提交后触发钩子)
        eventPublisher.publishEvent(new OrderConfirmEvent(orderId));
    }
}

3. 事务钩子:释放资源

@Component
public class OrderConfirmListener {
    @TransactionalEventListener
    public void handleOrderConfirmEvent(OrderConfirmEvent event) {
        log.info("订单{}确认成功,释放库存锁定...", event.getOrderId());
        // 释放库存锁定、通知仓库发货等操作
    }
}

五、避坑指南

  1. 乐观锁重试次数不宜过多:默认 3-5 次即可,过多重试会导致线程阻塞,影响性能;
  2. 事务钩子不能修改事务内数据:AFTER_COMMIT 阶段事务已提交,修改数据会导致新的事务,需避免;
  3. 高并发场景避免悲观锁:悲观锁是阻塞锁,并发量高时会导致线程排队,性能急剧下降;
  4. 隔离级别与锁机制匹配:READ COMMITTED 适合乐观锁,REPEATABLE READ 适合悲观锁,避免隔离级别过高 / 过低;
  5. 异步操作提议用消息队列:事务钩子中执行的异步操作(如发送通知),提议通过消息队列实现,避免钩子执行失败导致数据不一致。

六、总结

Spring Boot 解决事务并发问题的核心是「隔离级别兜底 + 并发控制(乐观 / 悲观锁)解决冲突 + 事务钩子处理后续逻辑」:

  • 高并发读多写少:乐观锁 + READ COMMITTED + 事务钩子(异步回调);
  • 低并发写多读少:悲观锁 + REPEATABLE READ + 事务钩子(释放资源);
  • 事务钩子的核心价值是「解耦事务内逻辑与后续操作」,同时保证操作仅在事务成功后执行,避免数据一致性问题。

该方案兼顾了数据一致性和性能,是 Spring Boot 项目中解决事务并发的最优实践之一。

© 版权声明

相关文章

9 条评论

  • 头像
    野狐禅蒋恺 读者

    收藏了,内容不错

    无记录
    回复
  • 头像
    浩天装饰 读者

    感谢关注,,持续技术分享

    无记录
    回复
  • 头像
    MemeBoi 投稿者

    真不戳💪

    无记录
    回复
  • 头像
    莨家妇女_ 投稿者

    收藏了,感谢分享

    无记录
    回复
  • 头像
    Epiphanic_Lines 投稿者

    关注一下,感谢,

    无记录
    回复
  • 头像
    捏捏n_ 读者

    关注,感谢,持续技术分享

    无记录
    回复
  • 头像
    Elio柚 投稿者

    好好学习天天向上

    无记录
    回复
  • 头像
    读者

    关注一下,感谢您的,持续技术分享

    无记录
    回复
  • 头像
    那对夫妻KimNico 读者

    Spring Boot + 事务钩子,完美解决事务并发问题

    无记录
    回复