1000 万短信 1 小时发完,怎么设计线程池?

📅 2026-05-04 00:00:00阅读时间: 8分钟

设计一个能在一小时内稳定发送一千万条短信的线程池,绝不仅仅是设置几个参数那么简单。这是一个典型的高并发、IO密集型任务,需要从架构层面进行系统性设计,以确保高性能、高可靠和系统稳定。

以下是完整的设计方案:

🎯 核心目标与约束

首先,明确我们的目标:

  • 总量: 10,000,000 条短信
  • 时限: 1 小时 (3600 秒)
  • 平均速率: 10,000,000 / 3600 ≈ 2778 条/秒

这意味着我们的系统需要稳定地维持近 2800 QPS 的发送能力。

🛠️ 线程池核心配置

在生产环境中,严禁使用 Executors.newFixedThreadPool() 等方式创建线程池,因为它们使用无界队列,在海量任务下极易导致内存溢出(OOM)。我们必须手动创建 ThreadPoolExecutor 并进行精细化配置。

1. 确定线程数量

短信发送是典型的 IO 密集型 任务(主要耗时在网络调用),而非 CPU 密集型。对于 IO 密集型任务,可以使用以下经验公式来估算初始线程数:

Nthreads = Ncpu * Ucpu * (1 + W/C)

  • Ncpu: CPU 核心数
  • Ucpu: 目标 CPU 利用率 (通常设为 1)
  • W/C: 等待时间与计算时间的比值。对于网络请求,W远大于C,因此这个值很大。

在实际工程中,一个常用的简化策略是将线程数设置为 CPU 核心数的 2 倍作为起点。例如,一台 8 核的机器,可以从 16 个核心线程开始。但这只是一个初始值,最终需要通过压力测试来确定最优配置。

2. 选择有界队列

必须使用有界队列(如 LinkedBlockingQueue 并指定容量)来限制等待任务的数量,这是防止 OOM 的关键防线。队列的大小需要根据可用内存和单个任务占用的内存来估算。

3. 设置合理的拒绝策略

当线程池和队列都满了之后,新提交的任务如何处理?默认的 AbortPolicy 会直接抛出异常,导致任务丢失,这在我们的场景中是不可接受的。

强烈推荐使用 CallerRunsPolicy

  • 工作原理: 当任务被拒绝时,由提交任务的线程(例如主线程)自己去执行该任务。
  • 核心优势: 这在离线批量处理场景中形成了一种 “天然背压 (Backpressure)” 机制。当生产者(从数据库拉取任务的线程)被迫自己处理任务时,它就没法继续从数据库拉取新任务,从而自动减缓了任务的注入速度,给线程池喘息的机会,有效避免了系统因过载而崩溃。

🛡️ 生产级可靠性保障

仅有线程池是不够的,必须构建一套完整的可靠性保障体系。

动态调优与监控

线上流量是变化的,硬编码的参数无法应对所有情况。

  • 参数动态化: 将线程池的核心参数(corePoolSize, maxPoolSize, queueCapacity)配置在 Apollo、Nacos 等配置中心,支持运行时动态调整,无需重启服务。
  • 全链路监控: 实时监控线程池的活跃度、队列剩余容量等关键指标。当队列使用率超过阈值(如 80%)时,自动触发告警,甚至可以联动配置中心进行动态扩容。
  • 开源工具: 可以引入业内成熟的动态线程池框架,如 Hippo4JDynamicTp,它们开箱即用地提供了上述功能。

任务持久化与补偿

为了防止应用宕机导致内存队列中的任务丢失,必须有兜底方案。

  1. 本地持久化: 在任务提交到线程池之前,先在数据库或 Redis 中将该任务的状态标记为“发送中”。
  2. Ack 机制: 线程成功发送短信后,回调更新任务状态为“已完成”。
  3. 离线补偿: 部署一个定时任务,定期扫描数据库中状态为“发送中”且超过一定时间(如 10 分钟)的记录,将它们重新投递到消息队列或线程池中,确保任务不遗漏。

⚙️ 外部限流与网关协同

你的线程池再强大,也必须考虑下游短信网关的承受能力。如果网关有 QPS 限制(例如每秒最多接收 3000 条),那么你的发送速率就不能超过这个限制。

此时,可以在任务执行逻辑中引入限流器,如 Guava 的 RateLimiter

java 复制代码
// 创建一个每秒放行 2800 个令牌的限流器
final RateLimiter rateLimiter = RateLimiter.create(2800.0);

// 在线程池的任务中
public void sendSmsTask() {
    // 获取令牌,如果速率超限则会阻塞等待
    rateLimiter.acquire(); 
    // 执行真正的短信发送逻辑
    smsGateway.send(...);
}

通过这种方式,可以确保发送给网关的流量是平滑且受控的,避免因瞬时流量过大而被网关拒绝。

📝 总结

综上所述,一个健壮的千万级短信推送方案应该是多层次的:

层级 策略 目的
基础层 手动创建 ThreadPoolExecutor,使用有界队列和 CallerRunsPolicy 避免 OOM,实现背压,保证单机稳定性
业务层 基于 IO 密集型公式设定初始线程数,并通过压测调优 最大化资源利用率和吞吐能力
保障层 任务状态持久化 + 离线补偿任务 确保任务在任何情况下都不丢失
治理层 动态线程池 + 全链路监控 实时感知系统状态,灵活应对流量变化
协同层 使用 RateLimiter 等工具进行限流 保护下游依赖,遵守外部系统约束