1. 项目背景与问题定位
去年在金融级交易系统开发中,我们遇到了一个典型的数据持久化瓶颈:每日收盘后需要将约50万条市场行情数据批量入库。最初采用MyBatis单条插入方案,完整执行需要近5分钟,导致后续清算流程严重延迟。经过两周的工程化改造,最终将耗时稳定控制在3秒以内。这个优化过程涉及MyBatis批处理机制、JDBC驱动调优、事务控制等多维度技术方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心优化方案设计
2.1 批处理模式选择
原生MyBatis提供三种批处理方式:
- 简单循环插入:foreach标签拼接SQL
- BatchExecutor:配置ExecutorType.BATCH
- RewriteBatchedStatements:JDBC驱动层优化
实测对比结果(50万条数据):
| 方案 | 耗时 | 内存消耗 | 适用场景 |
|---|---|---|---|
| 简单循环 | 4分52秒 | 1.2GB | 小批量简单操作 |
| BatchExecutor | 28秒 | 800MB | 标准批处理场景 |
| RewriteBatched | 2.8秒 | 350MB | 大数据量插入 |
关键发现:MySQL驱动需要开启rewriteBatchedStatements=true参数才能真正生效批量优化,否则BatchExecutor仍会逐条发送SQL
2.2 JDBC参数调优
在jdbc.url中必须配置以下参数:
properties复制jdbc:mysql://localhost:3306/trade?rewriteBatchedStatements=true&cachePrepStmts=true&prepStmtCacheSize=500&prepStmtCacheSqlLimit=2048
参数说明:
- prepStmtCacheSize:预处理语句缓存数量
- prepStmtCacheSqlLimit:缓存SQL最大长度
- useServerPrepStmts:启用服务端预处理
2.3 事务边界控制
通过测试不同批次大小的事务控制效果:
java复制// 最佳实践代码示例
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
MarketDataMapper mapper = session.getMapper(MarketDataMapper.class);
for (int i = 0; i < totalCount; i++) {
mapper.insert(dataList.get(i));
if(i % 5000 == 0 || i == totalCount - 1) {
session.commit();
session.clearCache(); // 防止OOM
}
}
} finally {
session.close();
}
3. 深度优化实践
3.1 对象转换优化
原始POJO存在以下性能陷阱:
- 过多的字段校验注解
- 复杂的类型转换逻辑
- 冗余的日志打印
优化方案:
- 创建轻量级DataTransferObject
- 移除非必要校验
- 使用原生类型替代包装类
3.2 数据库端优化
配合DBA进行的MySQL服务端调整:
sql复制-- 临时调整参数
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
SET GLOBAL sync_binlog = 0;
SET GLOBAL max_allowed_packet = 256M;
-- 表结构优化
ALTER TABLE market_data
DISABLE KEYS,
ENGINE = InnoDB
ROW_FORMAT = COMPRESSED;
3.3 内存管理方案
处理百万级数据时的内存管理要点:
- 采用分页读取源数据
- 使用WeakHashMap缓存映射关系
- 每批处理完成后手动触发GC
java复制// 内存友好型处理
List<MarketData> batchList = new ArrayList<>(BATCH_SIZE);
for(SourceData source : dataStream) {
batchList.add(convertToDTO(source));
if(batchList.size() >= BATCH_SIZE) {
processBatch(batchList);
batchList.clear();
}
}
4. 性能对比与监控
4.1 优化前后指标对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 总耗时 | 292s | 2.8s |
| CPU平均使用率 | 85% | 63% |
| 网络IO | 12MB/s | 98MB/s |
| 数据库锁等待时间 | 47s | 0.3s |
4.2 监控埋点设计
在批处理关键节点添加监控:
java复制// 使用Micrometer实现
Timer.Sample sample = Timer.start(registry);
try {
// 批处理操作
} finally {
sample.stop(Timer.builder("mybatis.batch")
.tags("table", "market_data")
.register(registry));
}
5. 异常处理与容错
5.1 批量失败处理策略
开发了分段重试机制:
- 记录失败批次位置
- 自动拆分问题批次
- 异步重试失败记录
java复制// 智能重试逻辑
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.execute(context -> {
try {
return batchInsert(currentBatch);
} catch (PartialFailureException e) {
return handlePartialFailure(e);
}
});
5.2 常见问题排查指南
-
数据截断错误
- 检查max_allowed_packet设置
- 验证字段长度定义
-
连接超时
- 调整wait_timeout参数
- 配置合理的连接池超时
-
批处理不生效
- 确认rewriteBatchedStatements已开启
- 检查MySQL驱动版本(建议8.0.16+)
6. 工程化落地实践
6.1 自动化配置方案
创建Spring Boot自动配置类:
java复制@Configuration
@ConditionalOnClass(SqlSessionFactory.class)
public class MyBatisBatchAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate batchSqlSessionTemplate(
SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(
sqlSessionFactory,
ExecutorType.BATCH);
}
}
6.2 团队协作规范
制定的开发约束:
- 批量操作必须使用专用SqlSessionTemplate
- 单批次数据量控制在5000-10000条
- 必须添加批处理监控指标
- 禁止在批处理中混用查询操作
7. 扩展优化思路
7.1 多线程批处理
采用并行流处理时需注意:
- 每个线程使用独立的SqlSession
- 控制并发线程数(建议CPU核心数×2)
- 避免锁竞争热点
java复制List<CompletableFuture<Void>> futures = dataList.stream()
.parallel()
.map(data -> CompletableFuture.runAsync(
() -> threadLocalMapper.insert(data),
batchThreadPool))
.collect(Collectors.toList());
7.2 混合存储方案
针对超大规模数据:
- 先批量写入临时表
- 通过存储过程合并数据
- 使用LOAD DATA INFILE替代SQL插入
sql复制-- 最终合并方案
INSERT INTO target_table
SELECT * FROM temp_table
ON DUPLICATE KEY UPDATE ...;
在实际生产环境中,我们发现当单批次超过50万条时,采用临时表+存储过程的方案比纯JDBC批处理性能提升约40%。但需要注意这种方案会带来额外的磁盘IO开销,需要根据服务器配置权衡使用。
