任务特别多、队列满了又不能拒绝,怎么办?

#Java#面试八股

任务特别多、队列满了又不能拒绝,怎么办?

结论先行:队列满说明"生产速度 > 消费速度",拒绝只是最后一道闸。不能拒绝时只有三条路:扩缓冲(治标)、降速/背压(治本)、溢出到外部存储(缓兵)。先判断任务能不能延后,再决定缓冲放在哪。

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());

它的效果是天然的限速器:提交者被自己的任务占住,自然就提交得慢了。但它有两个坑:

  1. 如果提交者是 Tomcat 的请求线程,会被业务任务占住,接口 RT 直接飙升;
  2. 如果任务里又往同一个池提交任务,可能死锁。

更温和的做法是提交前先检查水位:

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/磁盘)→ 最后才是扩队列。队列只是缓冲,产能不足的问题只能靠加机器、优化下游或降级来解决。

笔记加载中…