线程池是实现生产者-消费者模型最自然、最稳定的工程载体,用于日志异步入库:生产者提交日志至阻塞队列,消费者线程批量执行jdbc入库,共享队列需有界,线程池须自定义配置,拒绝策略推荐callerrunspolicy,并引入批量插入、连接池、字段裁剪、优雅关闭、错误重试与监控告警等实战措施。

线程池是实现生产者-消费者模型最自然、最稳定的工程载体。处理海量日志异步入库时,不能靠“每个日志启一个线程”,而应通过线程池统一调度任务,让生产者(日志采集端)只负责提交,消费者(入库执行端)专注执行,中间由阻塞队列缓冲和解耦。
明确角色分工与核心组件
在日志异步入库场景中:
-
生产者:业务线程或AOP切面,调用
executor.submit()或offer()将日志对象(如LogEntry)放入队列,不等待结果 -
消费者:线程池中的工作线程,从队列中
take()或poll()取出日志,批量组装SQL并执行JDBC入库 -
共享队列:选用
LinkedBlockingQueue(有界)、ArrayBlockingQueue(固定容量)或SynchronousQueue(零缓冲,适合高吞吐+强控制),禁用无界LinkedBlockingQueue(易OOM) -
线程池:使用
ThreadPoolExecutor自定义构造,避免Executors.newFixedThreadPool等隐藏风险的工厂方法
关键参数配置与避坑要点
针对日志入库这类典型IO密集型任务:
- 核心线程数建议设为
2 × CPU核数 + 2,兼顾数据库连接池并发上限与系统负载 - 最大线程数可略高于核心数(如+2~4),用于应对突发流量,但不宜过大(避免上下文切换开销反超收益)
- 队列容量需结合日志峰值QPS与单次入库耗时估算:例如每秒1万条日志、单次批量插入100条耗时200ms,则缓冲5秒需约5000个槽位
- 拒绝策略必须自定义:不选
AbortPolicy(直接抛异常中断业务),推荐CallerRunsPolicy(由提交线程自己执行,自然限流)或记录告警后丢弃(如日志非关键)
提升入库效率的实战技巧
单纯“一条日志一个INSERT”会严重拖慢吞吐,必须引入批量逻辑:
- 消费者线程不单条处理,而是循环
poll(timeout)收集N条或等待M毫秒,凑够一批再执行JdbcTemplate.batchUpdate() - 使用
PreparedStatement预编译+批量addBatch,避免SQL解析开销 - 考虑接入连接池(如HikariCP)并设置
maximumPoolSize匹配消费者线程数,防止DB连接争抢 - 对日志字段做必要裁剪(如截断过长message、过滤敏感字段),减少网络与磁盘IO压力
保障可靠性与可观测性
生产环境必须应对中断、堆积、失败等现实问题:
- 程序关闭时,调用
shutdown()→awaitTermination()→shutdownNow()三级退出,确保队列清空或持久化未完成任务 - 捕获
SQLException并重试(带退避);对不可恢复错误(如表结构变更)记录到死信队列或本地文件,供人工补录 - 暴露队列剩余容量、活跃线程数、平均处理延迟等指标,接入Prometheus+Grafana监控告警
- 为每批日志添加唯一traceId,便于问题定位与链路追踪










