一、遇到的问题
工作中遇到一个真实问题:多表关联计算后写入临时表,最终数据量约 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` 接收 `select from table where id in (...)` 的结果,且一次性返回 1 万条数据,这些数据会全部加载到应用内存。如果对象字段很大(如 text 类型),10 个线程并发加载可能导致 GC 开销过大或堆内存溢出(OOM)。
优化建议(避免报错)
建议 1:缩小事务粒度(最关键)
不要在整个 1 万条数据的处理周期开启事务。改为每 100 条开启一个事务,处理完立即提交。
建议 2:使用游标(Cursor)替代分页(推荐)
如果数据库是 MySQL 或 PostgreSQL,`OFFSET` 在大偏移量(如 `OFFSET 500000`)时效率极低。建议直接使用游标(流式查询):
MyBatis:使用 `Cursor` 或 `ResultHandler`,只查一次,逐条处理,不占用大量内存。
建议 3:调整连接池配置
确保连接池的最大连接数 `maximumpoolsize` 大于 10(业务线程)+ 其他业务连接数。通常建议设置为 `maximumpoolsize: 30`,并设置合理的 `connectiontimeout`(如 30000ms)。
建议 4:使用 `where id > ?` 代替 `OFFSET`
利用主键索引,避免深度分页导致的 IO 抖动。
三、第二轮对话
问: "我先取 1 万条 ID,然后对这 1 万条 ID 再分页处理(100 条每页)。"
AI 回复:
1、潜在的风险点

2、总结

四、第三轮对话
问: "ID 是 32 位字符串,60 万 ID 大概占用多少内存?"
AI 回复:

五、一些思考
AI 会把所有可能性都罗列出来——风险点、优化建议等等。
但我们仍然需要结合实际(业务场景、项目框架、代码风格),选择适合自己的优化方案。工具给出选项,决策仍然在开发者手中。
夜雨聆风