任务特别多、队列满了又不能拒绝,怎么办?
结论先行:队列满说明"生产速度 > 消费速度",拒绝只是最后一道闸。不能拒绝时只有三条路:扩缓冲(治标)、降速/背压(治本)、溢出到外部存储(缓兵)。先判断任务能不能延后,再决定缓冲放在哪。
1. 六种方案
| 方案 | 做法 | 代价 | 适用 |
|---|---|---|---|
| 扩队列 | 容量调大 | 排队变长、延迟升高、可能超时 | 短时脉冲,能容忍延迟 |
| 扩线程 | 提高 maxPoolSize | 上下文切换、下游被打爆 | 下游有余量、任务 CPU 密集 |
| 背压 | CallerRunsPolicy 或阻塞提交 | 拖慢上游(甚至拖死 Tomcat 线程) | 上游可被拖慢的内部调用 |
| 溢出 MQ | 拒绝时投递消息队列 | 需消费者 + 幂等 + 补偿 | 任务可异步、可延后 |
| 落盘/入库 | 拒绝时写本地文件或表 | 复杂度高、要清理 | 不允许丢、量大且可延迟 |
| 降级 | 返回兜底结果 | 业务有损 | 非核心链路(推荐信、积分) |
2. 先想清楚:任务能不能延后
任务能延后处理吗?
├── 不能(用户正在等结果)→ 只能背压或降级:要么让用户等,要么给兜底
└── 能 → 缓冲放在哪里?
├── 内存够、脉冲短 → 扩队列 + 监控积压
├── 内存有限、量大 → 溢出到 MQ / 磁盘,事后补偿
└── 下游扛不住 → 限流 + 降级,把流量挡在入口
关键判断:队列长度 + 处理速度 = 最大可容忍延迟。队列 5000、每秒处理 100,最坏延迟就是 50 秒——多数业务根本等不起,此时"扩队列"等于"把超时藏起来"。
3. 背压:让上游自己慢下来
// CallerRunsPolicy:池满时由提交任务的线程自己执行
ThreadPoolExecutor pool = new ThreadPoolExecutor(
8, 8, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.CallerRunsPolicy());
它的效果是天然的限速器:提交者被自己的任务占住,自然就提交得慢了。但它有两个坑:
- 如果提交者是 Tomcat 的请求线程,会被业务任务占住,接口 RT 直接飙升;
- 如果任务里又往同一个池提交任务,可能死锁。
更温和的做法是提交前先检查水位:
public void submitSafely(Runnable task) {
if (pool.getQueue().remainingCapacity() < 50) {
// 水位告警:不拒绝,但先降级或延迟提交
metrics.counter("pool.high_watermark").increment();
fallback(task);
return;
}
pool.execute(task);
}
4. 溢出到 MQ:把内存缓冲换成磁盘缓冲
public class OverflowToMqPolicy implements RejectedExecutionHandler {
private final MessageProducer producer;
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (r instanceof OrderTask task) { // 可延后的任务
producer.send("order-async-topic", task.payload());
metrics.counter("pool.overflow.mq").increment();
} else { // 不可延后的任务必须暴露
throw new RejectedExecutionException("池已满且任务不可降级");
}
}
}
这套方案能成立的前提有三个,缺一不可:
| 前提 | 说明 |
|---|---|
| 任务可异步 | 调用方不依赖执行结果 |
| 消费端幂等 | 消息可能重复投递 |
| 有积压监控 | 否则 MQ 也会堆到磁盘满 |
5. 常见追问
追问一:为什么不能把队列设得很大? 三个后果:内存占用(每个任务对象都活着)、延迟不可控(排队 10 分钟才执行,用户早超时了)、故障被隐藏(队列是缓冲,不是解决产能不足的手段)。队列长度应按"可容忍延迟 × 处理速率"反推。
追问二:CallerRunsPolicy 会不会把 Tomcat 拖死?
会。它是"把压力反弹给调用方",在 Web 场景下等于用请求线程跑后台任务。所以核心链路通常配合入口限流使用,而不是单靠它兜底。
追问三:自定义拒绝策略有什么禁忌?
不能在 rejectedExecution 里做耗时操作(它运行在提交线程上),不能吞掉异常后假装成功,也不能再往同一个池提交任务。它应该是快速、可观测、有明确语义的。
追问四:怎么证明"不是队列太小,而是消费太慢"?
看三个指标:queue.size() 的趋势(持续上涨说明产能不足)、任务等待时间(从提交到开始执行)、处理速率。如果处理速率没变而队列涨,就是产能问题;如果处理速率下降,先查下游 RT 和 GC。
小结
"不能拒绝"不等于"无限缓冲"。正确顺序是:先限流(挡住不该进来的)→ 再背压(让上游慢下来)→ 再溢出(放到 MQ/磁盘)→ 最后才是扩队列。队列只是缓冲,产能不足的问题只能靠加机器、优化下游或降级来解决。