当前位置: 代码网 > it编程>编程语言>Java > MyBatis-Plus saveBatch 批量插入方案

MyBatis-Plus saveBatch 批量插入方案

2026年08月27日 Java 我要评论
技术栈:spring boot 3 + mybatis-plus + mysql 8数据规模:5600万条兑换码、6036个活动、单次生成1800万+一、问题来了最近做一个营销活动平台,核心需求是这样

技术栈: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?

批次大小网络往返内存峰值风险
10018.86 万次极低太慢
50003772 次~10mb平衡点
50000378 次~100mbmysql binlog 峰值高
全量1次1 次~3.7gboom 风险

⑤ 尾批提交

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 分钟                              │
└─────────────────────────────────────────────┘
指标savebatchjdbc 批处理提升
网络往返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 批量插入内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com