02-Java21虚拟线程与事务管理

java21

前言

作为一名深耕Java后端多年的开发者,从Java 8一路用到Java 21,虚拟线程(Virtual Thread)无疑是JDK近年来最革命性的特性之一——它彻底解决了传统平台线程高内存占用、高上下文切换开销的痛点,让数万甚至数十万并发线程的实现变得轻而易举。

但在实际业务落地中,尤其是我们日常开发最核心的数据库事务管理场景,虚拟线程和Spring事务体系、MyBatis-Plus(以下简称MP)的整合,藏着非常多的坑。

一、两个底层原理

所有的坑,本质上都是对底层原理的误解。在整合之前,必须先搞懂两个核心体系的底层逻辑,尤其是和线程绑定相关的部分。

1. 虚拟线程

虚拟线程是JVM实现的轻量级用户态线程,和传统的平台线程(内核态线程)核心差异如下,重点关注和事务相关的特性:

特性 平台线程 虚拟线程 事务相关影响
实现方式 内核态线程,和OS线程1:1映射 JVM用户态实现,和OS线程N:M映射 无直接影响,核心看ThreadLocal行为
内存占用 栈空间固定1MB左右,创建成本高 栈空间动态伸缩,最小仅几百字节,创建成本极低 支持大量并发事务任务,无OOM风险
阻塞处理 阻塞时会占用整个内核线程,造成资源浪费 阻塞时(如JDBC查询、网络IO)会自动挂载,让出载体线程给其他虚拟线程,阻塞结束后重新挂载 解决了传统JDBC阻塞占用线程的问题,大幅提升数据库操作的并发能力
ThreadLocal绑定 绑定到平台线程,生命周期和线程池一致 完全绑定到虚拟线程本身,和载体线程无关,载体线程切换时不会丢失 这是和Spring事务兼容的核心基础!
生命周期 通常和线程池绑定,长生命周期 Per-Task模式,一个任务对应一个虚拟线程,任务结束立即销毁 事务生命周期和虚拟线程完全绑定,无线程复用带来的事务上下文污染问题

关键纠正:很多人误以为虚拟线程的ThreadLocal会在载体线程切换时丢失,这是完全错误的。Java 21的虚拟线程,ThreadLocal是完全绑定在虚拟线程实例上的,无论载体线程怎么切换,只要是同一个虚拟线程,ThreadLocal的数据就不会丢失,这是Spring事务能在虚拟线程中正常工作的核心前提。

2. Spring事务 + MyBatis-Plus

我们日常用的@Transactional注解,以及MP的CRUD操作,底层完全依赖两个核心组件,而它们的核心都是ThreadLocal

  1. TransactionSynchronizationManager(事务同步管理器)
    1. Spring事务的核心,用ThreadLocal存储当前线程的事务上下文,包括:数据库连接Connection、事务隔离级别、是否只读、事务同步器等。
    2. 同一个事务内的所有数据库操作,必须拿到同一个Connection,才能保证事务的原子性,而这个Connection就是从这个ThreadLocal里获取的。
  2. MyBatis-Plus的SqlSessionUtils
    1. MP的CRUD操作,会通过SqlSessionUtils获取SqlSession,而SqlSession的获取逻辑,优先从TransactionSynchronizationManager的ThreadLocal中获取当前事务绑定的Connection
    2. 也就是说:只要是同一个线程内的MP操作,在@Transactional注解下,一定会拿到同一个数据库连接,保证在同一个事务中

二、环境版本

虚拟线程是Java 21正式发布的稳定特性,而Spring、MP对虚拟线程的兼容,有严格的版本要求,版本不对,大概率会出现各种奇奇怪怪的问题:

组件 最低版本要求 推荐版本 说明
JDK 21 21 LTS 必须使用正式版,不支持预览版
Spring Boot 3.2.0 3.2.x/3.3.x 3.2.0开始正式支持虚拟线程的自动配置,无需手动写Bean
Spring Framework 6.1.0 6.1.x+ 6.1.0开始对虚拟线程做了全面适配,优化了ThreadLocal的传递
MyBatis-Plus 3.5.5 3.5.6+ 3.5.5开始兼容Spring 6+,修复了ThreadLocal相关的兼容性问题
数据库连接池 HikariCP 5.0.0 HikariCP 5.1.0 Spring Boot 3.x默认自带,对虚拟线程的阻塞场景做了优化

注意:绝对不要用Spring Boot 3.0/3.1版本强行整合虚拟线程,这两个版本没有官方的自动配置支持,手动配置很容易出现事务上下文丢失的问题。

三、整合步骤

Spring Boot 3.2+对虚拟线程的支持已经做到了极致,整合步骤非常简单,核心就是开启虚拟线程自动配置 + 规范事务写法

1. 第一步:pom.xml 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!-- 父工程必须是Spring Boot 3.2+ -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>

<dependencies>
<!-- Spring Web 自动适配虚拟线程 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring 事务核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<!-- MyBatis-Plus 最新版 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.6</version>
</dependency>

<!-- 数据库驱动,以MySQL为例 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok 简化代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<!-- 必须指定编译版本为Java 21 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>

2. 第二步:application.yml 配置

核心就一行:开启虚拟线程自动配置,其他都是常规的MP和连接池配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 核心:开启Spring Boot虚拟线程自动配置
# 开启后,@Async异步执行、Spring MVC控制器请求、TaskScheduler定时任务,全部自动使用虚拟线程
spring:
threads:
virtual:
enabled: true

# 数据源配置,HikariCP连接池
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
# HikariCP连接池配置(虚拟线程场景优化项)
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000

# MyBatis-Plus常规配置
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0

划重点:spring.threads.virtual.enabled=true 这一行,是Spring Boot官方提供的最稳定的虚拟线程开启方式,它会自动替换Spring内部的TaskExecutor为虚拟线程执行器,无需手动写任何Bean配置,避免了手动配置带来的事务兼容问题。

3. 第三步:事务写法

开启虚拟线程后,同步事务的写法和之前完全一致,没有任何变化;异步事务需要注意注解的位置,核心原则:@Transactional必须加在虚拟线程执行的方法上。

场景1:同步事务(MVC请求)

开启虚拟线程后,Spring MVC的每个请求都会自动使用虚拟线程处理,事务写法完全不变,开箱即用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@RestController
@RequestMapping("/test")
@RequiredArgsConstructor
public class testController {
private final testService testService;

@PostMapping("/weight/distribute")
public ResponseEntity<Void> distributeWeight(@RequestBody WeightDistributeDTO dto) {
// 这个方法的执行,已经在Spring自动分配的虚拟线程中
testService.distributeWeight(dto.getInbdWoId(), dto.getGoodsWeight(), dto.getUserId());
return ResponseEntity.ok().build();
}
}

@Service
@RequiredArgsConstructor
public class testService {
private final InbdRecItemMapper recItemMapper;
private final InvPalletMapper palletMapper;

// 同步事务,写法和之前完全一致,虚拟线程中完全生效
@Transactional(rollbackFor = Exception.class)
public void distributeWeight(Long inbdWoId, BigDecimal goodsWeight, Long userId) {
// 1. 查询明细
List<InbdRecItem> recItems = recItemMapper.selectList(Wrappers.lambdaQuery(InbdRecItem.class)
.eq(InbdRecItem::getInbdWoId, inbdWoId));
// 2. 重量分配
doDistribute(recItems, goodsWeight);
// 3. MP更新操作,和事务绑定
recItemMapper.updateBatchById(recItems);
// 4. 异常触发回滚
if (recItems.isEmpty()) {
throw new BusinessException("明细不能为空");
}
}
}

场景2:异步事务(@Async)

这是最高频的使用场景,也是最容易出现事务失效的场景,核心规则:@Async@Transactional必须加在同一个方法上,让异步方法的执行(虚拟线程)和事务绑定

正确写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
@RequiredArgsConstructor
public class AsynctestService {
private final InbdRecItemMapper recItemMapper;

// 正确✅️:@Async和@Transactional加在同一个方法上
// 方法执行在虚拟线程中,事务上下文绑定到当前虚拟线程,完全生效
@Async
@Transactional(rollbackFor = Exception.class)
public void asyncBatchHandle(List<InbdRecItem> items) {
// 业务处理
items.forEach(this::handleItem);
// MP批量更新
recItemMapper.updateBatchById(items);
// 异常触发回滚
if (items.stream().anyMatch(item -> item.getActualWeight().compareTo(BigDecimal.ZERO) < 0)) {
throw new BusinessException("重量不能为负数");
}
}
}

错误写法(事务完全失效):

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@RequiredArgsConstructor
public class ErrorAsyncService {
private final AsynctestService asynctestService;

// 错误❌️:@Transactional加在主线程上,@Async方法执行在独立的虚拟线程中
// 主线程和虚拟线程的ThreadLocal不共享,事务完全不生效
@Transactional(rollbackFor = Exception.class)
public void errorAsyncHandle(List<InbdRecItem> items) {
asynctestService.asyncBatchHandle(items);
}
}

四、注意事项(★)

坑1:异步事务失效

问题现象@Async方法里的数据库操作异常,事务没有回滚。

根本原因

  • Spring的@Transactional是基于AOP实现的,事务上下文绑定到当前线程的ThreadLocal。
  • 主线程开启的事务,和@Async虚拟线程是两个完全独立的线程,ThreadLocal不共享,虚拟线程里的操作根本不在主线程的事务中。
  • 更严重的是:如果@Async方法上没有加@Transactional,MP会为每个CRUD操作单独获取连接、单独提交事务,完全没有原子性。

避坑方案

  1. 🔴必须在@Async注解的方法上,同时加上@Transactional注解,让事务绑定到虚拟线程。
  2. 异步事务必须用REQUIRED传播级别(默认),不要用REQUIRES_NEW以外的其他传播级别,避免上下文错乱。
  3. 必须抛出异步方法的异常,否则异常不会被事务切面捕获,无法触发回滚。

坑2:JDBC连接池阻塞

问题现象:开启虚拟线程后,并发量上去了,但是系统吞吐量没有提升,大量虚拟线程处于阻塞状态。

根本原因

  • 虚拟线程的核心优势是解决IO阻塞的线程占用问题,但JDBC连接池的连接数是有限的。
  • 传统平台线程场景,我们通常把HikariCP的最大连接数设为CPU核心数*2,但虚拟线程场景下,大量并发请求会同时争抢连接,连接池满了之后,所有虚拟线程都会阻塞在获取连接的步骤,性能直接打折扣。
  • 更严重的是:如果连接数设置过大,数据库的性能会急剧下降,反而拖垮整个系统。

避坑方案

  1. 虚拟线程场景下,HikariCP的maximum-pool-size不要超过20-30,数据库的最大连接数也要对应调整。
    1. 核心逻辑:数据库的处理能力是有限的,过多的连接只会造成数据库的上下文切换开销,反而降低性能。
    2. 业界最佳实践:MySQL的最佳连接数通常在20-50之间,和虚拟线程的数量无关。
  2. 必须设置合理的connection-timeout(建议3000ms),避免虚拟线程长时间阻塞等待连接。
  3. 🔴绝对不要用无界的连接池,否则会直接把数据库打挂。

坑3:长事务占用连接,导致连接池耗尽

问题现象:系统运行一段时间后,连接池满了,所有请求都超时,数据库连接全部处于占用状态。

根本原因

  • 虚拟线程适合执行短平快的任务,而长事务会长期占用数据库连接,导致连接池被快速耗尽。
  • 比如:在一个事务里,先做数据库查询,然后调用第三方接口(耗时5s),再做数据库更新,整个事务持续5s,连接被占用5s,高并发下连接池瞬间就满了。
  • 虚拟线程的并发量是平台线程的几十倍,长事务的危害被无限放大,之前平台线程场景下不明显的问题,在虚拟线程场景下会直接导致系统雪崩。

避坑方案

  1. 🔴绝对禁止在事务中执行非数据库操作:比如第三方接口调用、文件IO、本地耗时计算,必须把这些操作提到事务之外。
  2. 大事务拆分成小事务:批量处理场景,拆分成多个小批次,每个批次用独立的事务,避免单个事务执行时间过长。
  3. 严格控制事务的执行时间,单个事务的执行时间建议不超过100ms

坑4:ThreadLocal传递错乱,事务上下文污染

问题现象:偶尔出现事务提交/回滚异常,或者拿到了不属于当前事务的数据库连接,极难复现。

根本原因

  • 很多开发者习惯用InheritableThreadLocal传递上下文,比如用户信息、租户ID。
  • 虚拟线程场景下,InheritableThreadLocal会从父线程拷贝数据到子虚拟线程,但如果父线程是平台线程池中的线程,会出现线程复用导致的上下文错乱。
  • 更严重的是:如果不小心把事务上下文的ThreadLocal传递到了其他虚拟线程,会导致多个线程共用同一个数据库连接,直接造成事务错乱。

避坑方案

  1. 虚拟线程场景下,优先使用ThreadLocal,而不是InheritableThreadLocal
  2. 如果必须传递上下文,使用Spring 6.1+提供的ContextSnapshot来传递,而不是手动操作ThreadLocal,它会安全地把父线程的上下文传递到子虚拟线程,不会出现错乱。
  3. 🔴绝对不要手动操作TransactionSynchronizationManager的ThreadLocal,避免事务上下文污染。

坑5:synchronized锁导致虚拟线程无法挂载,性能急剧下降

问题现象:开启虚拟线程后,并发量上去了,CPU使用率飙升,但是吞吐量没有提升,大量虚拟线程处于RUNNABLE状态。

根本原因

  • Java 21之前,虚拟线程遇到synchronized重量级锁时,会被固定在载体线程上,无法挂载,导致载体线程被占用,其他虚拟线程无法使用,完全失去了虚拟线程的优势。
  • Java 21虽然已经优化了这个问题,但是在高并发场景下,synchronized锁依然会成为性能瓶颈,导致大量虚拟线程阻塞在锁上。

避坑方案

  1. 虚拟线程场景下,优先使用java.util.concurrent.locks.ReentrantLock替代synchronized,它完全兼容虚拟线程的挂载机制,阻塞时会让出载体线程。
  2. 必须缩小锁的范围,只锁最核心的资源竞争代码,不要锁整个方法。
  3. 🔴绝对不要在事务中加锁,否则会导致锁的持有时间过长,同时占用连接和锁,放大并发问题。

坑6:事务同步器在虚拟线程中的异常

问题现象:用TransactionSynchronizationManager.registerSynchronization注册的事务同步器,比如afterCommit回调,偶尔不执行,或者执行时报错。

根本原因

  • 事务同步器是绑定到当前事务的ThreadLocal中的,而如果在虚拟线程中注册同步器,虚拟线程在事务结束前就销毁了,会导致同步器丢失。
  • 更常见的是:在主线程中开启事务,然后在子虚拟线程中注册同步器,子虚拟线程的ThreadLocal和主线程不共享,同步器根本没有注册到当前事务中。

避坑方案

  1. 事务同步器必须在事务所在的同一个虚拟线程中注册,绝对不能跨线程注册。
  2. 🔴异步场景下,优先使用@TransactionalEventListener,而不是手动注册事务同步器,它会安全地处理事务事件的监听,兼容虚拟线程。

坑7:MP分页插件、乐观锁插件的兼容性

问题现象:分页查询偶尔出现分页参数错乱,或者乐观锁版本号更新失败。

根本原因

  • MP的分页插件、乐观锁插件,底层是用ThreadLocal来传递分页参数、版本号的。
  • 如果在虚拟线程中,手动把分页参数设置到ThreadLocal中,然后异步执行,会出现参数传递错乱的问题。

避坑方案

  1. 升级MP到3.5.6+版本,官方已经修复了虚拟线程场景下的ThreadLocal兼容性问题。
  2. 🔴分页参数必须在同一个虚拟线程中设置和使用,不要跨线程传递分页参数。
  3. 🔴绝对不要在多个虚拟线程中共用同一个Page对象,避免线程安全问题。

五、最佳实践

1. 异步事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@Service
@RequiredArgsConstructor
public class StandardAsyncService {
private final InbdRecItemMapper recItemMapper;
private final ApplicationEventPublisher eventPublisher;

/**
* 标准异步事务写法:
* 1. @Async + @Transactional 同时加在方法上
* 2. 严格控制事务范围,只在事务内做数据库操作
* 3. 异常捕获与回滚
* 4. 事务提交后发布事件
*/
@Async
@Transactional(rollbackFor = Exception.class)
public CompletableFuture<Void> standardAsyncHandle(List<Long> ids) {
try {
// 1. 事务外:提前查询数据,避免事务内耗时操作
List<InbdRecItem> items = recItemMapper.selectBatchIds(ids);
if (CollUtil.isEmpty(items)) {
return CompletableFuture.completedFuture(null);
}

// 2. 事务内:仅做数据处理和数据库操作,无任何IO
items.forEach(item -> {
item.setActualWeight(calculateWeight(item));
item.setUpdateTime(LocalDateTime.now());
});
recItemMapper.updateBatchById(items);

// 3. 事务提交后发布事件,用事务监听,避免事务内发布
eventPublisher.publishEvent(new WeightDistributeEvent(ids));

return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("异步处理失败", e);
// 异常抛出,触发事务回滚
throw new BusinessException("异步处理失败", e);
}
}

// 事务监听:事务提交后执行,兼容虚拟线程
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleWeightDistributeEvent(WeightDistributeEvent event) {
// 异步后续处理,比如通知、日志等
log.info("重量分配完成,ids: {}", event.getIds());
}
}

2. 批量数据处理

虚拟线程非常适合批量数据处理,但必须避免长事务,正确的做法是拆分小批次 + 虚拟线程并行处理 + 独立事务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Service
@RequiredArgsConstructor
public class BatchHandleService {
private final InbdRecItemMapper recItemMapper;
private final AsyncHandleService asyncHandleService;

// 单次处理10万条数据,拆分批次并行处理
public void batchHandle() {
// 1. 全量查询数据
List<InbdRecItem> allItems = recItemMapper.selectList(Wrappers.lambdaQuery(InbdRecItem.class)
.eq(InbdRecItem::getStatus, 0));
if (CollUtil.isEmpty(allItems)) {
return;
}

// 2. 拆分批次,每批次100条
List<List<InbdRecItem>> batchList = ListUtil.split(allItems, 100);

// 3. 虚拟线程并行处理每个批次,每个批次独立事务
List<CompletableFuture<Void>> futures = batchList.stream()
.map(batch -> asyncHandleService.handleBatch(batch))
.toList();

// 4. 等待所有批次处理完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
log.info("批量处理完成,总条数: {}", allItems.size());
}
}

@Service
@RequiredArgsConstructor
public class AsyncHandleService {
private final InbdRecItemMapper recItemMapper;

// 每个批次独立的异步事务,失败仅回滚当前批次
@Async
@Transactional(rollbackFor = Exception.class)
public CompletableFuture<Void> handleBatch(List<InbdRecItem> batch) {
try {
// 批次处理逻辑
batch.forEach(item -> item.setStatus(1));
recItemMapper.updateBatchById(batch);
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
log.error("批次处理失败", e);
throw new BusinessException("批次处理失败", e);
}
}
}

3. 连接池最佳配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
datasource:
hikari:
# 核心:最大连接数不要超过30,和虚拟线程数量无关
maximum-pool-size: 20
# 最小空闲连接数,保持和核心数一致,避免频繁创建连接
minimum-idle: 10
# 连接超时时间3s,避免虚拟线程长时间阻塞
connection-timeout: 3000
# 空闲连接超时10分钟
idle-timeout: 600000
# 连接最大生命周期30分钟
max-lifetime: 1800000
# 连接测试查询,保证连接有效
connection-test-query: SELECT 1

4. 监控与排查

  1. 虚拟线程监控:Spring Boot 3.2+已经集成了虚拟线程的监控,可通过Actuator暴露/actuator/threads端点,查看虚拟线程的数量、状态、阻塞情况。
  2. 事务监控:开启Spring事务的debug日志,查看事务的创建、提交、回滚情况,定位事务失效问题:
1
2
3
4
logging:
level:
org.springframework.transaction: debug
com.baomidou.mybatisplus: debug
  1. 连接池监控:通过HikariCP的Metrics,监控连接池的活跃连接数、等待线程数,及时发现连接池瓶颈。

六、测试验证

1. 同步事务回滚测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@SpringBootTest
public class VirtualThreadTransactionTest {
@Autowired
private testService testService;
@Autowired
private InbdRecItemMapper recItemMapper;

@Test
public void testSyncTransactionRollback() {
// 准备测试数据
Long inbdWoId = 1L;
BigDecimal goodsWeight = new BigDecimal("100");
Long userId = 1L;

// 执行方法,预期会抛出异常,事务回滚
try {
testService.distributeWeight(inbdWoId, goodsWeight, userId);
} catch (BusinessException e) {
System.out.println("异常捕获成功");
}

// 验证:数据库中没有更新的数据,事务回滚成功
List<InbdRecItem> items = recItemMapper.selectList(Wrappers.lambdaQuery(InbdRecItem.class)
.eq(InbdRecItem::getInbdWoId, inbdWoId));
items.forEach(item -> {
assert item.getActualWeight() == null;
});
System.out.println("同步事务回滚测试通过");
}
}

2. 异步事务回滚测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@SpringBootTest
public class AsyncTransactionTest {
@Autowired
private AsynctestService asynctestService;
@Autowired
private InbdRecItemMapper recItemMapper;

@Test
public void testAsyncTransactionRollback() throws Exception {
// 准备测试数据
List<InbdRecItem> items = recItemMapper.selectList(Wrappers.lambdaQuery(InbdRecItem.class)
.eq(InbdRecItem::getInbdWoId, 1L));
// 设置非法重量,预期会抛出异常,事务回滚
items.forEach(item -> item.setActualWeight(new BigDecimal("-1")));

// 执行异步方法
try {
asynctestService.asyncBatchHandle(items).get();
} catch (ExecutionException e) {
System.out.println("异步异常捕获成功");
}

// 验证:数据库中数据没有被更新,事务回滚成功
List<InbdRecItem> dbItems = recItemMapper.selectBatchIds(items.stream().map(InbdRecItem::getId).toList());
dbItems.forEach(item -> {
assert item.getActualWeight().compareTo(new BigDecimal("-1")) != 0;
});
System.out.println("异步事务回滚测试通过");
}
}

02-Java21虚拟线程与事务管理
https://janycode.github.io/2026/05/03/02_编程语言/01_Java/06_Java21/02-Java21虚拟线程与事务管理/
作者
Jerry(姜源)
发布于
2026年5月3日
许可协议