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());
// 释放库存锁定、通知仓库发货等操作
}
}
五、避坑指南
- 乐观锁重试次数不宜过多:默认 3-5 次即可,过多重试会导致线程阻塞,影响性能;
- 事务钩子不能修改事务内数据:AFTER_COMMIT 阶段事务已提交,修改数据会导致新的事务,需避免;
- 高并发场景避免悲观锁:悲观锁是阻塞锁,并发量高时会导致线程排队,性能急剧下降;
- 隔离级别与锁机制匹配:READ COMMITTED 适合乐观锁,REPEATABLE READ 适合悲观锁,避免隔离级别过高 / 过低;
- 异步操作提议用消息队列:事务钩子中执行的异步操作(如发送通知),提议通过消息队列实现,避免钩子执行失败导致数据不一致。
六、总结
Spring Boot 解决事务并发问题的核心是「隔离级别兜底 + 并发控制(乐观 / 悲观锁)解决冲突 + 事务钩子处理后续逻辑」:
- 高并发读多写少:乐观锁 + READ COMMITTED + 事务钩子(异步回调);
- 低并发写多读少:悲观锁 + REPEATABLE READ + 事务钩子(释放资源);
- 事务钩子的核心价值是「解耦事务内逻辑与后续操作」,同时保证操作仅在事务成功后执行,避免数据一致性问题。
该方案兼顾了数据一致性和性能,是 Spring Boot 项目中解决事务并发的最优实践之一。





收藏了,内容不错
感谢关注,,持续技术分享
真不戳💪
收藏了,感谢分享
关注一下,感谢,
关注,感谢,持续技术分享
好好学习天天向上
关注一下,感谢您的,持续技术分享
Spring Boot + 事务钩子,完美解决事务并发问题