⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Fight/zhijiewenmengquanMySQLyicipiliangcharuduoshaotiaoshujuxingnengzuijia/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

这道题面试官真正想筛什么

先看真实面试场景:

👨 面试官: 你们项目批量插入大概多少条一批?

👦 候选者: 嗯……5 万一批吧。

👨 面试官: 5 万确定吗?这条 SQL 多大、max_allowed_packet 够装吗?

👦 候选者: 嗯……我们之前一直这么用,没出过问题。

👨 面试官: 5 万这个数字怎么算出来的?为什么不是 500、不是 5000?资源、事务、锁——你考虑过哪个维度?

👦 候选者: 这……是之前一位资深工程师定的。

👨 面试官: 回去等通知吧。

这道题筛三件事:

  1. 数字背后的推导能力——能不能从硬件、记录大小、事务约束算出来一个数字
  2. 理解 InnoDB 的存储原理——页、redo log、事务边界、max_allowed_packet
  3. 会用 MyBatis 实现——<foreach> vs ExecutorType.BATCH、什么时候用哪个

下面三段就是 L1/L2/L3 标准答案——按段位评估自己卡在哪一档。

L1:30 秒答案——给一个安全的默认值

被问到能直接给一个生产可用的默认值:

500 - 1000 条一批——OLTP 业务(高频小事务)走这个范围,事务粒度可控、不会撑爆 max_allowed_packet、锁占用时间短。

5000 - 10000 条一批——离线导数/批处理场景,可以放大批量减少总往返开销。

一句话注释:业务系统选 500-1000,离线任务选 5000-10000,绝对不要超过 5 万。

⚠️ L1 的边界:背一个数字 = 30 分及格。如果面试官问”为什么是 500-1000?大了会怎样?”——你答不上来就只能停在 30 分。

L2:2 分钟答案——讲清”为什么这个数字”

L2 答法的核心:讲清三个限制因素——单批太大会触发的硬性约束。

限制 1:MySQL 的 max_allowed_packet 上限

每条 INSERT INTO t VALUES (...), (...), ... 是一个 SQL packet 发给 MySQL Server——有最大长度上限:

MySQL 版本 默认 max_allowed_packet
5.6 1 MB
5.7 4 MB
8.0+ 64 MB

超出会报错:Packet for query is too large。一行 100 字节的话,1MB packet 大约能装 1 万条——超过了就拆批。

限制 2:事务粒度——大事务 = 锁占用 + redo log 膨胀

InnoDB 写入会先写 redo log(WAL)+ 锁住相关行。单批越大:

  • redo log 膨胀——大事务可能触发 redo log file 切换甚至 checkpoint
  • 行锁/间隙锁占用时间长——其它读写都在等
  • 失败回滚成本高——10 万条插到 9.5 万崩了,前面 9.5 万全要 rollback

生产经验:单批超过 1 万条,回滚一次的代价就开始让人难受了。

限制 3:内存与 IO——批量太大反而拖慢

数据先进 buffer pool,再异步刷盘:

批量的甜区:

  • 太小 → 每条都要事务开销 + 网络往返,性能差
  • 甜区(500-5000) → 单事务内合并多行写、IO 次数和事务次数都最优
  • 太大 → buffer pool 被这一批占满 → 触发刷盘 → IO 飙升 → 影响其它查询

单条 vs 批量的差距——主要在事务开销:

场景 事务次数 网络往返 锁获取
1000 次单条 INSERT 1000 1000 1000
1 次 1000 行 batch 1 1 1

L2 的边界:能讲清三个限制因素 = 60 分及格。如果问”那具体怎么算?给我一个能用的公式”——L3 来了。

L3:5 分钟答案——一个能算出来的公式 + 实战代码

L3 给两件东西:一套可推导的公式 + MyBatis 实战代码——这一档是 90 分。

公式 1:单条记录大小估算

给定字段结构:

  • 整型 (int):4 字节
  • 变长字符串 (varchar):平均 50 字节,最大 255 字节
  • 日期 (date):3 字节
  • 浮点数 (float):4 字节

平均一条记录大小:

考虑 varchar 最大长度的极端情况:

公式 2:内存预算反推批量

8GB 内存、预留 20%(给 OS 和其它进程),可用 ≈ 6.4 GB:

按平均记录大小算,内存能撑下的最大记录数:

公式 3:硬盘容量校验

512 GB 硬盘能存的最大记录数:

这只是上限——实战还要预留磁盘 20-30% 给 binlog、redo log、临时空间,并考虑增长速度。

实战:MyBatis 三种姿势

姿势 1:<foreach> 拼大 SQL

最常见的做法——一条 SQL 多个 VALUES:

<insert id="insertMultiple" parameterType="list">
INSERT INTO orders (order_no, user_id, amount, create_time)
VALUES
<foreach collection="list" item="record" separator=",">
(#{record.orderNo}, #{record.userId}, #{record.amount}, #{record.createTime})
</foreach>
</insert>
  • ✅ 单次执行、性能高
  • ❌ 受 max_allowed_packet 限制——批量不能无限大

姿势 2:ExecutorType.BATCH 累积发送

让 MyBatis 帮你用 JDBC 批处理(addBatch / executeBatch)攒到一定数量再发——内存可控、批量大小可灵活配置:

SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
OrderMapper mapper = session.getMapper(OrderMapper.class);
int count = 0;
for (Order order : orders) {
mapper.insert(order);
// 每 1000 条 flush 一次,避免内存爆炸
if (++count % 1000 == 0) {
session.flushStatements();
}
}
session.commit();
} catch (Exception e) {
session.rollback();
throw e;
} finally {
session.close();
}
  • ✅ 内存可控、批量大小可灵活配置
  • ✅ DAO 层强制走 BATCH,能防住新人在 for 里写单条 insert 把性能玩崩

⚠️ 关于”网络往返次数”的常见误解:

  • BATCH 模式不是永远只 1 次往返——你 flushStatements() 多少次,就分多少批发送
  • MySQL 驱动默认仍是逐条 INSERT 发包(只是用了 JDBC batch 协议);只有加 URL 参数 rewriteBatchedStatements=true 时,驱动才会把多条 INSERT 重写成单条多 VALUES 的大 SQL——这时才接近 <foreach> 的效率
  • 重写后的大 SQL 仍受 max_allowed_packet 限制——所以即使开了 rewrite,单批不能无限大,建议保持 1000-5000 这个甜区

姿势 3:避免频繁 commit

每条都 commit = 每次都触发刷盘 + checkpoint——批量插入最忌讳:

// ❌ 错的姿势:循环里 commit
for (Order o : orders) {
insert(o);
commit(); // 性能爆炸
}

// ✅ 对的姿势:循环里只 add,全部完了再 commit
for (Order o : orders) {
insert(o);
}
commit();

直接掉分的几种答法

答法 为什么扣分
“5 万 / 10 万条一批”(凭感觉给数字) 大概率超 max_allowed_packet——4MB 默认值,100 字节/行只能装 4 万;100 字节单行 + 索引列稍多就爆
“越多越好“ 错——批量大反而拖慢,不是单调上升
“我们项目就是这么定的” 不会推导 = 30 分顶天
不知道 max_allowed_packet 基础不牢——这是 SQL 协议层的硬性约束
不知道 ExecutorType.BATCH MyBatis 都没玩透——<foreach> 不是唯一姿势

高频追问怎么接

追问 1:<foreach> 和 ExecutorType.BATCH 选哪个?

维度 <foreach> ExecutorType.BATCH
SQL 形态 一条 INSERT VALUES (…),(…) N 条独立 INSERT,走 JDBC addBatch 协议攒发
网络往返 1 次 每次 flushStatements() 一批——你 flush 几次就发几次
max_allowed_packet 限制 严格——超了直接报错 默认逐条发不会超;开 rewriteBatchedStatements=true 重写为大 SQL 后仍会触发限制
可读性 XML 中拼 SQL,调 SQL 直观 Java 代码控制,SQL 一致
推荐场景 知道大小、≤ 1000 条 大批量(5000+)、不确定大小

追问 2:批量插入和分库分表怎么协调?

核心问题:分库分表后,一批 10000 条数据可能分散到 16 个库,每个库实际只有 ~625 条——不能用一条 SQL 解决。

两种方案:

  • 按分片键分组:先按分片规则把 10000 条分组到 16 个库,对每个库分别走批量
  • 借助 ShardingSphere / MyCAT:让中间件帮你做这个分组,业务层依然像单库一样写

追问 3:MySQL 自身有什么参数能调?

  • bulk_insert_buffer_size:MyISAM 专用——InnoDB 不用看
  • innodb_flush_log_at_trx_commit:默认 1 = 每事务刷盘,改成 2 可以批量插入快很多(但崩了可能丢 1 秒数据)
  • sync_binlog:默认 1 = 每事务刷 binlog,同上 trade-off

生产里别乱改——这些参数影响数据安全,DBA 没点头之前别动。

一句话收口

回到开头那个面试场景——候选者答错的不是”5 万”这个数字,而是讲不出选这个数字的理由。

成为优秀工程师的关键:不是知道更多技巧,而是对每个数字都讲得出来源。

文章目录
  1. 1. 这道题面试官真正想筛什么
  2. 2. L1:30 秒答案——给一个安全的默认值
  3. 3. L2:2 分钟答案——讲清”为什么这个数字”
    1. 3.0.1. 限制 1:MySQL 的 max_allowed_packet 上限
    2. 3.0.2. 限制 2:事务粒度——大事务 = 锁占用 + redo log 膨胀
    3. 3.0.3. 限制 3:内存与 IO——批量太大反而拖慢
  • 4. L3:5 分钟答案——一个能算出来的公式 + 实战代码
    1. 4.0.1. 公式 1:单条记录大小估算
    2. 4.0.2. 公式 2:内存预算反推批量
    3. 4.0.3. 公式 3:硬盘容量校验
    4. 4.0.4. 实战:MyBatis 三种姿势
      1. 4.0.4.1. 姿势 1:<foreach> 拼大 SQL
      2. 4.0.4.2. 姿势 2:ExecutorType.BATCH 累积发送
      3. 4.0.4.3. 姿势 3:避免频繁 commit
  • 5. 直接掉分的几种答法
  • 6. 高频追问怎么接
    1. 6.0.1. 追问 1:<foreach> 和 ExecutorType.BATCH 选哪个?
    2. 6.0.2. 追问 2:批量插入和分库分表怎么协调?
    3. 6.0.3. 追问 3:MySQL 自身有什么参数能调?
  • 7. 一句话收口