1. 项目背景与测试目标
最近在重构一个高并发的订单处理系统时,遇到了MyBatis分页插件的性能瓶颈问题。恰逢JDK25发布,其中虚拟线程特性对IO密集型应用带来的性能提升让我产生了浓厚兴趣。于是决定对当前主流的三种MyBatis分页方案——g2rain-mybatis-extensions、PageHelper和MyBatis-Plus,在Spring Boot 4 + JDK25虚拟线程环境下进行全面的压力测试对比。
这次测试主要想验证几个关键问题:
- 虚拟线程对传统ORM框架的性能影响究竟有多大?
- 三种分页方案在高并发场景下的吞吐量差异
- 不同数据量级下各方案的响应时间表现
- 内存占用和GC情况的对比
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 测试环境搭建
2.1 硬件与基础软件配置
测试使用的是阿里云ecs.g7ne.4xlarge实例:
- 16核 vCPU
- 64GB内存
- 500GB ESSD云盘
- CentOS 7.9操作系统
- MySQL 8.0.28(innodb_buffer_pool_size=12G)
2.2 JDK25虚拟线程配置
在Spring Boot 4中启用虚拟线程需要做以下配置:
java复制@Configuration
public class ThreadConfig {
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
return protocolHandler -> {
protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
};
}
}
同时需要在application.properties中添加:
properties复制spring.threads.virtual.enabled=true
spring.datasource.hikari.thread-factory=org.springframework.boot.task.VirtualThreadTaskExecutorBuilder$VirtualThreadThreadFactory
2.3 测试数据准备
使用SysBench生成了4张测试表:
- small_table: 10万条记录
- medium_table: 500万条记录
- large_table: 2000万条记录
- wide_table: 100万条记录(包含30个字段)
每张表都建立了适当的索引,并确保数据分布均匀。
3. 三种分页方案实现对比
3.1 g2rain-mybatis-extensions实现
这个新兴的分页插件以其轻量级著称,配置方式如下:
java复制@Mapper
public interface UserMapper {
@Pagination
@Select("SELECT * FROM users WHERE status = #{status}")
List<User> findByStatus(@Param("status") int status);
}
它的特点是:
- 基于AOP实现,对原有SQL侵入小
- 支持多种数据库方言
- 分页参数通过ThreadLocal传递
3.2 PageHelper实现
最经典的分页插件,使用方式大家应该很熟悉:
java复制PageHelper.startPage(1, 10);
List<User> users = userMapper.selectAll();
PageInfo<User> pageInfo = new PageInfo<>(users);
需要注意的配置项:
properties复制pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true
3.3 MyBatis-Plus实现
MyBatis-Plus内置的分页功能需要先配置拦截器:
java复制@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
使用时的代码最简洁:
java复制Page<User> page = new Page<>(1, 10);
userMapper.selectPage(page, Wrappers.emptyWrapper());
4. 压力测试方案设计
4.1 测试工具与场景
使用JMeter 5.4.1设计了三类测试场景:
- 简单查询:主键查询+分页(模拟列表页)
- 复杂查询:多表关联+条件过滤+排序(模拟报表页)
- 混合场景:读写混合(30%写操作)
每个场景都设计了以下并发梯度:
- 100并发(基准测试)
- 500并发(常规压力)
- 1000并发(峰值压力)
- 2000并发(极限测试)
4.2 监控指标
除了常规的TPS、RT外,我们还特别关注:
- 虚拟线程创建数量
- 线程上下文切换次数
- 数据库连接池使用率
- JVM GC暂停时间
使用Arthas和Prometheus+Grafana搭建了实时监控系统。
5. 测试结果分析
5.1 吞吐量对比(TPS)
| 方案 | 100并发 | 500并发 | 1000并发 | 2000并发 |
|---|---|---|---|---|
| g2rain-mybatis-ext | 1256 | 3245 | 4987 | 6234 |
| PageHelper | 987 | 2543 | 3876 | 4123 |
| MyBatis-Plus | 1123 | 2987 | 4565 | 5210 |
注意:测试数据为简单查询场景下的平均值(单位:请求/秒)
从结果可以看出:
- g2rain在超高并发下表现最优,得益于其轻量级的实现
- PageHelper在中等并发下与MyBatis-Plus差距不大
- 所有方案在启用虚拟线程后,TPS都比传统线程池提升40%以上
5.2 响应时间对比(P99)
| 方案 | 100并发 | 500并发 | 1000并发 | 2000并发 |
|---|---|---|---|---|
| g2rain-mybatis-ext | 68ms | 125ms | 203ms | 356ms |
| PageHelper | 85ms | 187ms | 345ms | 567ms |
| MyBatis-Plus | 72ms | 156ms | 287ms | 498ms |
关键发现:
- 数据量超过500万时,PageHelper的响应时间波动明显增大
- g2rain的响应时间最稳定,特别是在2000并发时P99仍能控制在400ms以内
- MyBatis-Plus在复杂查询场景下会出现偶发的RT飙升
5.3 内存占用对比
通过JProfiler采集的内存数据:
| 方案 | 堆内存峰值 | 非堆内存 | 线程内存 |
|---|---|---|---|
| g2rain-mybatis-ext | 1.2GB | 350MB | 28MB |
| PageHelper | 1.8GB | 420MB | 45MB |
| MyBatis-Plus | 2.1GB | 500MB | 62MB |
虚拟线程的内存优势非常明显:
- 传统线程池在2000并发时需要预留2GB的线程栈内存
- 虚拟线程下只需要不到100MB的线程相关内存
6. 问题排查与优化建议
6.1 PageHelper的内存泄漏问题
在长时间压力测试后,发现PageHelper会出现内存缓慢增长的情况。通过内存dump分析,发现是ThreadLocal没有及时清理导致的。解决方案:
java复制try {
PageHelper.startPage(pageNum, pageSize);
// 业务代码
} finally {
PageHelper.clearPage(); // 必须手动清理
}
6.2 MyBatis-Plus的count查询优化
MyBatis-Plus的分页会先执行count查询,在大表场景下可能成为瓶颈。可以通过自定义SQL优化:
java复制@Select("SELECT * FROM user ${ew.customSqlSegment} LIMIT #{page.offset}, #{page.size}")
IPage<User> selectPageCustom(Page<User> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
6.3 g2rain的方言适配问题
测试中发现g2rain对Oracle的ROWNUM分页支持不够完善,需要手动指定方言:
java复制@Pagination(dialect = Dialect.ORACLE)
@Select("SELECT * FROM users")
List<User> findAll();
7. 虚拟线程的最佳实践
经过这次测试,总结出JDK25虚拟线程的几个使用要点:
- 连接池配置:建议将HikariCP的maximumPoolSize设置为物理核心数的2-3倍
properties复制spring.datasource.hikari.maximum-pool-size=32
- 事务管理:虚拟线程中要避免长时间持有数据库连接
java复制@Transactional(timeout = 5) // 设置合理超时
public void batchProcess() {
// ...
}
- 异常处理:虚拟线程的stacktrace较深,建议自定义异常处理器
java复制@RestControllerAdvice
public class VirtualThreadExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception e) {
// 简化stacktrace输出
return ResponseEntity.internalServerError()
.body(e.getMessage());
}
}
8. 最终选型建议
根据测试结果,给出不同场景下的推荐方案:
-
超高并发查询系统(如电商秒杀):
- 首选:g2rain-mybatis-extensions
- 理由:吞吐量最高,内存占用最小
-
常规业务系统(如CRM、ERP):
- 首选:MyBatis-Plus
- 理由:功能全面,开发效率高
-
遗留系统改造:
- 首选:PageHelper
- 理由:兼容性好,改造成本低
对于所有新项目,强烈建议基于Spring Boot 4 + JDK25虚拟线程进行架构。在实际部署时,我们还发现一个有意思的现象:使用虚拟线程后,原本需要10台4C8G实例支撑的流量,现在只需要6台同配置实例就能稳定运行,云成本直接降低了40%。
