核心是控制并发粒度、隔离资源、避免冲突;需分批切片(200~500条/批)、固定线程池(8~16)、每批独占连接、批量SQL、防锁冲突、幂等重试。

直接用线程池跑大批量数据库写入,核心不是“开更多线程”,而是控制并发粒度、隔离资源、避免冲突。单靠增加线程数反而容易压垮数据库连接或触发锁争用,实际效果可能更差。
分批切片 + 固定线程池
把 10 万条数据按每批 200~500 条切分,用 Executors.newFixedThreadPool(8) 创建固定大小线程池(一般设为 CPU 核数的 1.5~2 倍)。每批作为一个独立任务提交,避免单个任务过长阻塞线程。
- 用 List.subList(start, Math.min(start + batchSize, list.size())) 安全切片,防止越界
- 线程池大小不建议盲目设成 50 或 100,需结合数据库连接池最大连接数(如 HikariCP 的
maximum-pool-size)做匹配,否则大量线程抢不到连接会排队等待 - 示例:若 DB 连接池上限是 20,线程池设为 12~16 更稳妥
每个线程独占数据库连接
DAO 层不能复用同一个 Connection 或 SqlSession 实例。Spring 环境下确保每次操作都走 事务模板 或 @Transactional 方法内新建 Session;纯 JDBC 需在线程任务内部获取新连接,用完立即 close。
- MyBatis/MyBatis-Plus 默认支持线程安全的 SqlSession,但若手动管理 SqlSession,必须保证 每个 Runnable 内 new 一个 SqlSession
- 共享 Mapper 接口实例没问题,但不能在多线程间共享未提交的事务上下文
- 避免静态变量、单例 Service 中缓存 ResultSet、Statement 等有状态对象
规避数据库锁与主键冲突
高并发 INSERT/UPDATE 容易触发行锁等待或唯一键冲突,尤其在自增主键、时间戳字段、业务唯一索引上。
- INSERT 场景优先用 批量插入语句(如 MyBatis 的
<foreach>+INSERT INTO ... VALUES (),(),()),比循环单条快 5~10 倍 - UPDATE 场景若依据非主键条件更新,考虑加 SELECT FOR UPDATE SKIP LOCKED 预占记录,防止多个线程同时读到同一批数据再竞相更新
- 主键由数据库生成时,避免多线程同时调用
NEXTVAL或 UUID.randomUUID() 后重复写入;可改用雪花 ID 或 DB 序列预分配段
结果聚合与异常兜底
主线程需知道所有子任务是否完成,并捕获具体哪一批失败。不能只靠 executor.shutdown() 就认为成功。
- 用 CountDownLatch 或 CompletableFuture.allOf() 等待全部任务结束
- 每个任务 try-catch 包裹,失败时记录日志 + 保存失败批次原始数据(如写入 error_log 表或本地文件),便于重试
- 关键业务场景建议加上 幂等标识(如 batch_id + md5(data)),重试时不重复处理

















