blockingqueue 不能直接配 timer 做批量入库,因 timer 单线程执行且异常静默终止,易导致任务积压和 oom;应改用 scheduledthreadpoolexecutor,配限容队列与超时 poll 批量取数,并分段提交防错。

BlockingQueue 为什么不能直接配 Timer 做批量入库
因为 Timer 的任务执行是单线程的,一旦某个批量入库操作耗时稍长(比如网络抖动、DB 连接池等待),后续所有定时任务会排队阻塞,导致缓冲区持续积压甚至 OOM。更糟的是,Timer 遇到未捕获异常会静默终止,你根本不知道它已经“死”了。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 改用
ScheduledThreadPoolExecutor,至少设 corePoolSize=1,避免单点故障; -
BlockingQueue别用无界队列(如LinkedBlockingQueue不设容量),优先选ArrayBlockingQueue并明确指定容量,比如new ArrayBlockingQueue(1000); - 入库前先做「非空+大小判断」,避免提交空集合触发 JDBC 异常;
- 定时任务里别直接
drainTo到新 List——它可能返回 0,也可能返回满额,得用循环 + 超时 poll 配合,才能兼顾实时性与吞吐。
怎么安全地从 BlockingQueue 拿一批数据,又不丢不重
关键在「边界控制」:既要防止一次取太多拖慢响应,也要避免取太少放大调度开销。单纯靠 queue.drainTo(list, batchSize) 不可靠——它只保证最多取 batchSize,但不保证一定能取到,尤其在低流量时可能每次只拿到 1 条。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 用带超时的
poll()循环组装批次,例如:List<Record> batch = new ArrayList<>(); long deadline = System.currentTimeMillis() + 100; // 最多等 100ms while (batch.size() < 100 && System.currentTimeMillis() < deadline) { Record r = queue.poll(10, TimeUnit.MILLISECONDS); if (r != null) batch.add(r); } - 如果业务允许微小延迟,可加一个「最小等待时间」兜底,比如强制等够 50ms 再发,减少小包频次;
- 千万别在循环里调
queue.size()判断是否为空——它不是原子操作,且对ConcurrentLinkedQueue这类无锁队列成本高、不准。
批量入库时 PreparedStatement.executeBatch() 报错 BatchUpdateException 怎么办
这个异常本身不说明具体哪条出错,堆栈里往往只有「第 X 条语句失败」,但你根本不知道 X 对应的是原始 batch 中的哪个对象——因为 executeBatch() 返回的是 int[],而其中的 -3(EXECUTE_FAILED)位置和你 list 的索引并不总是一一对应(尤其开了 rewriteBatchedStatements=true 时)。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- 开发期打开 MySQL 的
rewriteBatchedStatements=true和continueBatchOnError=false,让失败立刻抛出,方便定位; - 上线后关闭
continueBatchOnError,改用分段提交:每 20 条 executeBatch 一次,缩小排查范围; - 记录日志时,不要只记
batch.size(),要记下这批数据的业务 ID 范围(比如 minId/maxId),比存整个对象轻量且可追溯; - 注意 Oracle 的
addBatch()对 null 参数敏感,MySQL 则对批量 insert 的字段数严格匹配,少一个字段就整批 fail。
并发写入时主键冲突或唯一索引重复怎么静默跳过
想用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE?小心——JDBC 默认把它们当普通 SQL 执行,executeBatch() 里混用会导致语法错误或行为不一致。而且不同数据库对「忽略」的定义不同:MySQL 认为重复是 warning,PostgreSQL 则必须显式写 ON CONFLICT DO NOTHING。
实操建议:
立即学习“Java免费学习笔记(深入)”;
- MySQL 场景下,统一用
INSERT INTO ... ON DUPLICATE KEY UPDATE id=id(空更新),它能被 batch 正确识别,且返回值中 -2 表示该行被 ignore; - PostgreSQL 必须用
addBatch()提交带ON CONFLICT的语句,且驱动版本不低于 42.2.0; - 别依赖
SQLException.getSQLState()判断冲突——它在不同驱动里返回值不一致,优先检查getErrorCode()(MySQL 是 1062,PG 是 23505); - 如果业务允许部分失败,就在 catch 块里把失败项单独 log,并放入重试队列,而不是整个 batch 回滚。
真正难的不是凑齐这些组件,而是当流量突增、DB 延迟毛刺、GC STW 同时发生时,你的 drain 策略、batch 大小、重试退避是否还稳得住——这些没法靠单测覆盖,得靠压测时看 queue.remainingCapacity() 和实际入库延迟的波动关系来调。










