批量操作与性能优化
在现代 Web 系统中,数据库往往是性能瓶颈,而批量操作(Batch) 是最直接、最有效的优化手段之一。
本文将在已经完成:
- tio-boot + jOOQ 整合
- AOP 注入 DSLContext
- 手动 / AOP 事务管理
的基础上,深入讲解:
- 如何使用 jOOQ 进行高性能批量操作
- 如何避免常见性能陷阱
- 如何提升 PostgreSQL 写入吞吐
- 如何设计更合理的数据访问策略
目标是让你的系统从“能跑”升级到“高并发可扩展”。
一、为什么必须掌握批量操作
很多系统慢,并不是 SQL 写得差,而是犯了一个经典错误:
❌ 循环 insert
for (User u : users) {
dsl.insertInto(USER)
.set(USER.NAME, u.getName())
.execute();
}
看起来只是一个循环,但实际发生的是:
每次执行都要:
- 获取连接
- 发送 SQL
- 等待数据库响应
- 提交事务
如果插入 1000 条数据,就会产生:
1000 次网络往返 + 1000 次 SQL 执行
性能会呈指数级下降。
二、jOOQ 的三种批量模式(非常重要)
理解这三种模式,是性能优化的关键。
⭐ 模式一:Batch(推荐)
jOOQ 会使用 JDBC Batch:
dsl.batch(
dsl.insertInto(USER).set(USER.NAME, "A"),
dsl.insertInto(USER).set(USER.NAME, "B")
).execute();
特点:
- 单次发送到数据库
- 极大减少网络 IO
- 性能提升通常 5~50 倍
这是最常用的方式。
⭐⭐ 模式二:Batch Bind(更高性能)
当 SQL 模板相同时,推荐使用:
dsl.batch(
dsl.insertInto(USER, USER.NAME)
.values((String) null)
).bind("A")
.bind("B")
.bind("C")
.execute();
优势:
- SQL 只编译一次
- 数据库执行计划可复用
- CPU 更低
在超大批量写入时优势明显。
⭐⭐⭐ 模式三:Multi-row Insert(PostgreSQL 极强)
PostgreSQL 对这种语法优化极好:
INSERT INTO user(name)
VALUES ('A'),('B'),('C');
jOOQ 写法:
dsl.insertInto(USER, USER.NAME)
.values("A")
.values("B")
.values("C")
.execute();
在 PG 中:
通常是最快的写入方式
甚至可能比 JDBC Batch 更快。
三、在事务中执行批量(强烈建议)
无论哪种批量:
✅ 必须放进事务中!
否则:
- 每条 SQL 自动提交
- WAL 写入暴涨
- IO 飙升
推荐写法:
txManager.tx(() -> {
userDao.batchInsert(users);
});
不要在 DAO 内开启事务。
事务边界始终放在 Service 层。
四、实战:高性能 Batch Insert DAO
示例:SystemAdminDao
public int[] batchInsert(List<SystemAdmin> admins) {
DSLContext ctx = TransactionContext.get();
InsertValuesStep2<Record, Object, Object> step = ctx.insertInto(DSL.table("system_admin"),
//
DSL.field("login_name"), DSL.field("password"))
//
.values((String) null, null);
BatchBindStep batch = ctx.batch(step);
for (SystemAdmin a : admins) {
batch.bind(a.getLoginName(), a.getPassword());
}
return batch.execute();
}
性能通常可提升:
10 倍以上
五、超大批量写入策略(百万级)
当数据量非常大时,不要一次提交!
❌ 错误示例
一次插入 1,000,000 条
可能导致:
- OOM
- 事务日志暴涨
- 锁时间过长
✅ 推荐策略:分块提交(Chunking)
int batchSize = 1000;
for (int i = 0; i < list.size(); i += batchSize) {
List<SystemAdmin> chunk =
list.subList(i, Math.min(i + batchSize, list.size()));
txManager.tx(() -> {
dao.batchInsert(chunk);
});
}
经验值:
| 数据库 | 推荐 chunk |
|---|---|
| PostgreSQL | 500~2000 |
| MySQL | 500~1000 |
六、批量更新优化
不要循环 update:
❌
for (...) {
update where id=?
}
✅ CASE WHEN 批量更新
PostgreSQL 非常擅长:
UPDATE system_admin
SET password =
CASE id
WHEN 1 THEN 'a'
WHEN 2 THEN 'b'
END
WHERE id IN (1,2);
jOOQ 可以构建这种 SQL。
优点:
- 一次扫描表
- 一次锁
- 极低 IO
七、读取性能优化(很多人忽略)
写入只是半边天。
读取同样关键。
⭐ 只查需要的字段
不要:
select *
而要:
dsl.select(USER.ID, USER.NAME)
减少:
- 网络传输
- JVM 反序列化
- GC 压力
⭐ 使用 fetchSize(超大查询)
dsl.fetchSize(1000);
效果:
- 避免一次加载百万行到内存
- 使用游标流式读取
非常适合:
- 导出数据
- 数据迁移
八、连接池优化建议(很多人忽略)
默认配置通常偏保守。
建议调整 Druid:
dataSource.setInitialSize(5);
dataSource.setMaxActive(30);
dataSource.setMinIdle(5);
dataSource.setMaxWait(60000);
核心原则:
连接数 ≈ CPU核心数 × 2~4
不要盲目开到 200。
数据库会先崩。
九、jOOQ 性能隐藏技巧(高手常用)
⭐ 使用 PreparedStatement(默认开启)
jOOQ 已经帮你做好了。
不要关闭。
⭐ 避免频繁创建 DSLContext
正确:
- DSLContext 单例
- 事务内用 Connection 创建子 DSL
错误:
每次 new DSL.using()
⭐ 善用 PostgreSQL UPSERT
INSERT ...
ON CONFLICT DO UPDATE
避免:
- 先 select
- 再 insert/update
直接减少一次 IO。
十、什么时候不该用 Batch?
少量数据:
- < 50 条
收益不明显。
但:
超过 200 条就建议批量。
十一、性能优化优先级(建议收藏)
如果只能做 3 件事:
第一优先级
✅ 批量写入 ✅ 放入事务
立刻提升 5~20 倍。
第二优先级
✅ 分块提交 ✅ 避免 select *
再提升 30~50%。
第三优先级
✅ 合理连接池 ✅ 使用 UPSERT
系统稳定性大幅提升。
十二、总结
掌握 jOOQ 批量操作,本质是在掌控三件事:
- 网络 IO
- 事务提交频率
- SQL 执行次数
当你做到:
- 批量替代循环
- 事务包裹批量
- 合理分块
你的数据库性能通常会出现“跃迁式”提升。
一句话总结:
不会 Batch 的系统,很难支撑高并发。
