1000 万短信 1 小时发完,怎么设计线程池?
设计一个能在一小时内稳定发送一千万条短信的线程池,绝不仅仅是设置几个参数那么简单。这是一个典型的高并发、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%)时,自动触发告警,甚至可以联动配置中心进行动态扩容。
- 开源工具: 可以引入业内成熟的动态线程池框架,如 Hippo4J 或 DynamicTp,它们开箱即用地提供了上述功能。
任务持久化与补偿
为了防止应用宕机导致内存队列中的任务丢失,必须有兜底方案。
- 本地持久化: 在任务提交到线程池之前,先在数据库或 Redis 中将该任务的状态标记为“发送中”。
- Ack 机制: 线程成功发送短信后,回调更新任务状态为“已完成”。
- 离线补偿: 部署一个定时任务,定期扫描数据库中状态为“发送中”且超过一定时间(如 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 等工具进行限流 |
保护下游依赖,遵守外部系统约束 |