应使用scheduledthreadpoolexecutor或带延迟队列的threadpoolexecutor控速,核心线程数=smtp连接池大小(如5),最大线程数=核心数,队列用linkedblockingqueue(容量1000)配callerrunspolicy,结合ratelimiter令牌桶限流(如每秒10封),失败任务异步投递delayqueue实现指数退避重试,任务内管理资源并捕获异常,配合监控熔断与黑名单机制。

用线程池控制发信速率
邮件批量发送必须避免瞬间打满 SMTP 服务器或触发反垃圾策略,核心是“控速”。推荐用 ScheduledThreadPoolExecutor 或带延迟队列的 ThreadPoolExecutor,而非无界线程池。
关键配置示例:
- 核心线程数 = SMTP 连接池大小(如 5),避免连接争抢
- 最大线程数 = 核心数(不扩容),防止突发压垮下游
- 使用 LinkedBlockingQueue 并设容量(如 1000),队列满时拒绝新任务(用 CallerRunsPolicy 让调用线程自执行,自然降速)
- 配合 RateLimiter(Guava) 在 execute 前做令牌桶限流,例如每秒最多 10 封:
RateLimiter limiter = RateLimiter.create(10.0);<br>if (limiter.tryAcquire()) {<br> executor.submit(() -> sendMail(mail));<br>} else {<br> // 加入延迟队列或记录限流日志<br>}
异步重试需解耦失败处理与主流程
重试不能阻塞主线程,也不能在 worker 线程里死循环重试。正确做法是:发送失败后,将任务封装为“可重试对象”,异步投递到重试调度器。
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
实现要点:
- 定义重试任务类,含原始邮件、已尝试次数、下次重试时间戳
- 用 DelayQueue 存储待重试任务(线程安全,支持按延迟时间排序)
- 单起一个守护线程轮询 DelayQueue.take(),取出到期任务后提交给同一业务线程池(或专用重试池)执行
- 重试策略建议指数退避:
nextDelay = base * Math.pow(2, attempt) + jitter,最大重试 3–5 次,超限转人工队列
线程池与邮件生命周期要绑定资源
每个发送任务应自行管理 SMTP 连接、模板渲染、附件流等,避免跨任务共享状态。特别注意:
- 不要在线程池外复用 MimeMessage 或 JavaMailSender 实例(它本身线程安全,但内部 session 可能有状态)
- 附件流(如 FileInputStream)必须在任务内打开+关闭,不可复用或提前关闭
- 使用 ThreadLocal
处理时间格式化,避免 SimpleDateFormat 非线程安全问题 - 任务 run() 方法末尾加 try-catch,捕获所有异常并交由重试模块处理,禁止向上抛出导致线程退出
监控与熔断让系统更可控
光靠线程池参数不够,需实时感知压力:
- 暴露线程池指标:活跃线程数、队列长度、拒绝数(可用 Micrometer + Prometheus)
- 当队列持续 >80% 容量且拒绝率上升,自动降低 RateLimiter QPS 或暂停新任务接入
- 对高频失败邮箱(如 5xx 响应)做临时黑名单,10 分钟内不再投递,避免无效重试
- 每封邮件生成唯一 traceId,贯穿日志、MQ、重试队列,便于排查卡点
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










