技术栈:spring boot 3 + mybatis-plus + mysql 8
数据规模:5600万条兑换码、6036个活动、单次生成1800万+
一、问题来了
最近做一个营销活动平台,核心需求是这样的:
运营创建一个活动,系统需要给每个活动批量生成大量唯一兑换码。
听起来不复杂?但看看数据量:
单次配置:1613 个活动
每个活动:平均 1.17 万条兑换码
总数据量:1886 万条
一开始我用 mybatis-plus 的 savebatch() 跑了一下,然后……就去吃了顿饭,回来还没跑完。
简单算一笔账:
mybatis-plus savebatch 本质:for 循环 + 单条 executeupdate
1886万条 × 每条约 30ms(sql解析 + 参数映射 + 网络往返)
≈ 565800秒 ≈ 14 小时
14 小时生成一批兑换码?运营和用户都会疯。
二、为什么 savebatch 慢
先看看 mybatis-plus savebatch() 到底做了什么:
// mybatis-plus savebatch 伪代码
public void savebatch(list<t> entitylist) {
for (t entity : entitylist) {
// ① 每次 new 一个对象(gc 压力)
// ② 每次解析一次 sql(cpu 开销)
// ③ 每次执行 executeupdate()(网络往返)
basemapper.insert(entity);
}
}
三个核心瓶颈:
| 瓶颈 | 原因 | 影响 |
|---|---|---|
| sql 重复解析 | 每条 insert 都要解析一次 sql 模板 | cpu 浪费 |
| 逐条网络往返 | 每条 insert 一次 execute → mysql → 返回 | 1886万次网络 io |
| 大量对象创建 | 每条 new entity() | gc 频繁,内存压力大 |
一句话:savebatch 是"伪批量",本质是 for 循环单条插入。
三、jdbc preparedstatement 真正的批量插入
核心代码
// ① sql 只预编译一次,? 占位符不参与 sql 解析(天然防 sql 注入)
private static final string insert_sql =
"insert into redeem_code (code, activity_id, serial_no, batch_no, "
+ "report_year, deleted, status, create_time, update_time) "
+ "values (?, ?, ?, ?, ?, 0, 1, ?, ?)";
private static final int batch_size = 5000;
public void batchgenerate(long activityid, int quantity,
int startserial, int year, string batchno) {
// ② 整批共享一个 timestamp 对象,避免 n 次对象创建
timestamp now = timestamp.valueof(localdatetime.now());
try (connection conn = datasource.getconnection();
preparedstatement ps = conn.preparestatement(insert_sql)) {
for (int i = 0; i < quantity; i++) {
int serialno = startserial + i;
string code = buildcode(activityid, serialno, year);
ps.setstring(1, code);
ps.setlong(2, activityid);
ps.setint(3, serialno);
ps.setstring(4, batchno);
ps.setint(5, year);
ps.settimestamp(6, now); // 复用同一个对象
ps.settimestamp(7, now);
ps.addbatch(); // ③ 攒入缓冲区,不发送
// ④ 每 5000 条打包发送一次
if ((i + 1) % batch_size == 0) {
ps.executebatch();
}
}
// ⑤ 提交剩余不足 5000 条的尾批
ps.executebatch();
} catch (exception e) {
throw new runtimeexception("批量生成失败", e);
}
}
五个优化点逐一拆解
① sql 只预编译一次
conn.preparestatement(insert_sql)
mysql 只解析一次 sql 模板,后续 5000 条只传参数。省掉 4999 次 sql 解析。
② timestamp 复用
timestamp now = timestamp.valueof(localdatetime.now()); // 5600万行共享同一个 now 对象 ps.settimestamp(6, now); // 第 1 行 ps.settimestamp(6, now); // 第 2 行 ... ps.settimestamp(6, now); // 第 5600万行
省掉 5600 万次 timestamp 对象创建。批量插入不要求每行时间精确到毫秒差异。
③ addbatch() 攒批
ps.addbatch(); // 只放入 jdbc 驱动的内存缓冲区,不发送
参数只在客户端内存暂存,不产生网络 io。
④ 每 5000 条 executebatch()
第 1~5000 条:攒批 → executebatch() → 1次网络io发送给mysql
第 5001~10000 条:攒批 → executebatch() → 1次网络io
...
1886 万条数据 ≈ 3772 次网络往返(而不是 1886 万次)。
为什么是 5000?
| 批次大小 | 网络往返 | 内存峰值 | 风险 |
|---|---|---|---|
| 100 | 18.86 万次 | 极低 | 太慢 |
| 5000 | 3772 次 | ~10mb | 平衡点 |
| 50000 | 378 次 | ~100mb | mysql binlog 峰值高 |
| 全量1次 | 1 次 | ~3.7gb | oom 风险 |
⑤ 尾批提交
ps.executebatch(); // 提交最后不足 5000 条的剩余
四、效果对比
┌─────────────────────────────────────────────┐ │ mybatis-plus savebatch │ │ │ │ java对象 ─→ sql解析 ─→ 网络io ─→ mysql │ │ java对象 ─→ sql解析 ─→ 网络io ─→ mysql │ │ java对象 ─→ sql解析 ─→ 网络io ─→ mysql │ │ ...(重复 1886 万次) │ │ │ │ 耗时:≈ 14 小时 │ └─────────────────────────────────────────────┘ ┌─────────────────────────────────────────────┐ │ jdbc preparedstatement 批处理 │ │ │ │ sql预编译(1次) │ │ 参数1 → addbatch ┐ │ │ 参数2 → addbatch ├→ executebatch → mysql │ │ ... │ (5000条/次) │ │ 参数5000 → addbatch ┘ │ │ │ │ 耗时:≈ 17 分钟 │ └─────────────────────────────────────────────┘
| 指标 | savebatch | jdbc 批处理 | 提升 |
|---|---|---|---|
| 网络往返 | 1886 万次 | 3772 次 | 5000 倍 |
| sql 解析 | 1886 万次 | 1 次 | 1886 万倍 |
| java 对象 | 1886 万个 | 0 个 | 无 gc 压力 |
| 总耗时 | ≈ 14 小时 | ≈ 17 分钟 | ≈ 50 倍 |
五、面试追问
q1:为什么不用 mybatis <foreach> 拼接批量 insert?
<!-- 这个方案有什么问题? -->
insert into redeem_code values
<foreach collection="list" item="item" separator=",">
(#{item.code}, #{item.activityid}, ...)
</foreach>两个致命问题:
- sql 长度超限:mysql max_allowed_packet 默认 4mb,5 万条就超限
- 内存膨胀:拼接出的 sql 字符串本身就占大量内存
preparedstatement.addbatch() 是 jdbc 协议级批处理,mysql 驱动将参数打包发送,不受 sql 长度限制。
q2:事务怎么控制?中途失败了怎么办?
@transactional 加在整个方法上,一个活动的所有兑换码要么全部成功,要么全部回滚。不会出现"只生成了一半"的数据。
q3:会不会有 sql 注入风险?
不会。preparedstatement 的 ? 占位符内容不会被当作 sql 语法解析,jdbc 驱动自动转义。而且编码是 buildcode() 纯计算生成的,不包含任何用户输入。
六、总结
| 场景 | 推荐方案 |
|---|---|
| < 1000 条 | savebatch() 够用,开发效率优先 |
| 1000 ~ 100 万条 | preparedstatement + addbatch() |
| > 100 万条 | preparedstatement + 分批提交(5000条/批) |
核心思路:减少网络往返 > 减少 sql 解析 > 减少 java 对象创建。
这不是什么高深的技术,但就是这种"回归 jdbc 本质"的做法,在批量场景下往往比任何 orm 的花式封装都管用。
到此这篇关于mybatis-plus savebatch 批量插入方案的文章就介绍到这了,更多相关mybatisplus savebatch 批量插入内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论