遇到的问题
工作中遇到一个真实问题:多表关联计算后写入临时表,最终数据量约 60 万行。现有代码存在两个瓶颈:
- 慢 SQL 问题:分页到数据行中段后,查询速度急剧下降。
- 频繁的 IO 错误:线程池最大线程数 100、最大队列 600,占用了大量数据库连接。
第一轮对话
问: “单表 60 万数据,线程池最大 10 并发。每个并发取 1 万条 ID,然后切换成 100 条分页处理。会不会出现 IO 报错问题?”
AI 回复:
- 不会直接报 IO 错误
- 数据量层面:60 万行对于现代关系型数据库(MySQL/PostgreSQL)属于小型数据集。全表扫描或索引扫描通常在 1~3 秒内完成,不会触发底层 socket 的 read/timeout。
- 分页层面:
LIMIT 100 OFFSET X虽然在大偏移量时效率下降,但 10 个并发各自只取 1 万条(100 次查询),总查询次数为 10 × 100 = 1000 次。这个 QPS 对数据库来说属于轻量级压力。
- 可能存在风险点
- 风险 1:长事务导致锁超时(Lock Wait Timeout)
- 如果在
@Transactional注解下循环处理这 1 万条数据(即一个事务包含 100 次分页查询和更新),事务持续时间可能长达几十秒。 - 后果:如果涉及
SELECT ... FOR UPDATE或更新操作,会长时间持有行锁或间隙锁。其他线程修改这些行时,会报错:Lock wait timeout exceeded; try restarting transaction。
- 如果在
- 风险 2:连接池耗尽(Connection Pool Exhaustion)
- 线程池最大并发为 10。如果每个线程在处理 1 万条数据时都占用一个数据库连接,且处理时间较长(假设 30 秒),这 10 个连接会被长期占用。
- 后果:如果应用还有其他业务接口需要查询数据库,会因为获取不到连接而报错:
HikariPool - Connection is not available, request timed out after 30000ms。
- 风险 3:结果集流式读取溢出(OutOfMemory)
- 如果直接用
List<Entity>接收select * from table where id in (...)的结果,且一次性返回 1 万条数据,这些数据会全部加载到应用内存。如果对象字段很大(如 text 类型),10 个线程并发加载可能导致 GC 开销过大或堆内存溢出(OOM)。
- 如果直接用
- 风险 1:长事务导致锁超时(Lock Wait Timeout)
- 优化建议(避免报错)
- 建议 1:缩小事务粒度(最关键)
- 不要在整个 1 万条数据的处理周期开启事务。改为每 100 条开启一个事务,处理完立即提交。
- 建议 2:使用游标(Cursor)替代分页(推荐)
- 如果数据库是 MySQL 或 PostgreSQL,
OFFSET在大偏移量(如OFFSET 500000)时效率极低。建议直接使用游标(流式查询):- MyBatis:使用
Cursor或ResultHandler,只查一次,逐条处理,不占用大量内存。
- MyBatis:使用
- 如果数据库是 MySQL 或 PostgreSQL,
- 建议 3:调整连接池配置
- 确保连接池的最大连接数
maximum-pool-size大于 10(业务线程)+ 其他业务连接数。通常建议设置为maximum-pool-size: 30,并设置合理的connection-timeout(如 30000ms)。
- 确保连接池的最大连接数
- 建议 4:使用
where id > ?代替OFFSET- 利用主键索引,避免深度分页导致的 IO 抖动。
- 建议 1:缩小事务粒度(最关键)
第二轮对话
问: “我先取 1 万条 ID,然后对这 1 万条 ID 再分页处理(100 条每页)。”
AI 回复:
1、潜在的风险点
| 风险点 | 场景 A(查询) | 场景 B(更新/删除) |
|---|---|---|
| 连接池耗尽 | ⚠️ 100 次查询,连接复用没问题 | ⚠️ 100 次更新,连接复用没问题 |
| 长事务 | ✅ 无事务或只读事务,安全 | ❌ 如果整个 1 万条在一个事务里,耗时可能超过 30s,触发事务超时 |
| 内存溢出 | ⚠️ 1 万条 ID 本身很小(~80KB),但如果后续查完整对象也积累,可能 OOM | ✅ 只批量更新,不积累数据 |
| 死锁 | ✅ 只读操作,无锁 | ⚠️ 如果更新顺序不一致或涉及多表,可能死锁 |
| 大事务锁范围 | ✅ 无 | ❌ 1 万条记录在同一个事务里,锁范围大,阻塞其他操作 |
2、总结
| 问题 | 结论 |
|---|---|
| 会报 IO 错误吗? | 不会。1 万条 ID 的查询是单次 IO,后续 100 批查询是重复 IO,但每次数据量很小(100 条),不会触发超时。 |
| 真正要注意什么? | 事务边界——千万别把整个 1 万条包在一个事务里。每 100 条开一个事务最安全。 |
| 内存够吗? | 1 万条 ID 约 80KB(Long 类型),完全没问题。但如果后续 selectByIds(100) 查完整对象,注意每批处理完及时释放引用。 |
| 性能如何? | 1 万条 ID 查一次 + 100 次 IN 查询,总耗时约 2~5 秒,完全可接受。 |
第三轮对话
问: “ID 是 32 位字符串,60 万 ID 大概占用多少内存?”
AI 回复:
| 组成部分 | 计算公式 | 单个占用 | 60 万个占用 |
|---|---|---|---|
| String 对象头 | 12~16 bytes | 16 bytes | 9.6 MB |
| char[] 数组 | 24 + 2 × 长度 bytes | 24 + 2 × 32 = 88 bytes | 52.8 MB |
| String 引用(List 中) | 8 bytes | 8 bytes | 4.8 MB |
| ArrayList 容量 | 10~12.5% 额外空间 | ~10 bytes/个 | 6 MB |
| 合计 | ~122 bytes/个 | ~73 MB |
一些思考
AI 会把所有可能性都罗列出来——风险点、优化建议等等。
但我们仍然需要结合实际(业务场景、项目框架、代码风格),选择适合自己的优化方案。工具给出选项,决策仍然在开发者手中。