记录一次 AI 工具辅助下的开发落地案例

遇到的问题

工作中遇到一个真实问题:多表关联计算后写入临时表,最终数据量约 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:缩小事务粒度(最关键)
      • 不要在整个 1 万条数据的处理周期开启事务。改为每 100 条开启一个事务,处理完立即提交。
    • 建议 2:使用游标(Cursor)替代分页(推荐)
      • 如果数据库是 MySQL 或 PostgreSQL,OFFSET 在大偏移量(如 OFFSET 500000)时效率极低。建议直接使用游标(流式查询):
        • MyBatis:使用 CursorResultHandler,只查一次,逐条处理,不占用大量内存。
    • 建议 3:调整连接池配置
      • 确保连接池的最大连接数 maximum-pool-size 大于 10(业务线程)+ 其他业务连接数。通常建议设置为 maximum-pool-size: 30,并设置合理的 connection-timeout(如 30000ms)。
    • 建议 4:使用 where id > ? 代替 OFFSET
      • 利用主键索引,避免深度分页导致的 IO 抖动。

第二轮对话

问: “我先取 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 会把所有可能性都罗列出来——风险点、优化建议等等。
但我们仍然需要结合实际(业务场景、项目框架、代码风格),选择适合自己的优化方案。工具给出选项,决策仍然在开发者手中。