线程池满了怎么办?用 RabbitMQ 做任务补偿不丢失

文章目录

在实际业务中,我们经常需要使用 线程池 执行异步任务,例如:

  • 生成 PDF 报告
  • 发送短信 / 邮件
  • 执行大批量数据库操作
  • 调外部接口

但线程池始终是有限资源

  • 核心线程数有限
  • 最大线程数有限
  • 队列容量有限

当系统高峰时流量过大,很容易出现:

线程池满了 → 拒绝策略触发 → 任务直接丢失

对于关键业务任务(如生成报告、订单后处理、补偿扣减),
不允许丢失

所以我们需要:

当线程池拒绝任务时,把任务丢到 RabbitMQ 中间缓冲,系统空闲后再补偿执行。


一、为什么会出现线程池拒绝任务?

先看线程池的本质:

复制代码
线程池 =(固定数量线程 + 任务队列)

设定:

  • corePoolSize = 2
  • maxPoolSize = 4
  • queueSize = 5

当系统瞬间执行 20 个任务时:

  • 前面 2 个 → 核心线程执行
  • 再来几个 → 放到队列
  • 队列满了 → 拿 maxPoolSize 顶上
  • maxPoolSize 线程也满了 → 拒绝策略触发

默认拒绝策略是:

  • AbortPolicy(抛异常)
  • DiscardPolicy(直接丢)

👉 任务丢了 = 数据不一致 = 事故


二、为什么用 RabbitMQ 做补偿?

RabbitMQ 可以承担:

  • 缓冲层:线程池执行不了的任务先存着
  • 持久化:服务器宕机、重启任务不会丢
  • 削峰填谷:系统高峰时 MQ 扛流量,低谷时慢慢消费
  • 异步重试:可支持死信队列、延迟重试

一句话:

RabbitMQ 能保证任务"不丢、能补偿、最终执行成功"。


三、整体架构图(重点)

复制代码
   ┌──────────────────┐
   │   Controller     │
   │ (创建任务)      │
   └───────┬──────────┘
           │
           ▼
   ┌──────────────────┐
   │  ThreadPool      │
   │ 执行 Runnable     │
   └───────┬──────────┘
           │(如果满)
           ▼
   ┌───────────────────────────┐
   │ RejectedExecutionHandler │
   │ (拒绝策略)               │
   │ ① 任务状态标记 REJECTED    │
   │ ② 发送消息到 MQ           │
   └─────────┬─────────────────┘
             │
             ▼
   ┌──────────────────┐
   │  RabbitMQ Queue  │
   │ (补偿队列)      │
   └─────────┬────────┘
             │
             ▼
   ┌───────────────────────────┐
   │  MQ Consumer              │
   │  收到消息 → 再投入线程池   │
   │  → 正常执行 processTask   │
   └───────────────────────────┘

核心思想:

线程池执行不了 → 先放 MQ
MQ 拉出来 → 再交给线程池执行
最终保证"任务必达"


四、Demo 业务场景

我们用"体检系统生成报告"为例:

  • 用户发起体检 → 写入 t_exam

  • 创建一条异步任务 → 写入 t_report_task

  • 任务交给线程池执行 PDF 生成

  • 线程池满 → 触发拒绝策略 →

    • t_report_task 标记为 REJECTED
    • msg 发到 RabbitMQ
  • MQ 消费者把任务拉出来 → 再让线程池执行

  • 最终 t_report_task 状态 → SUCCESS

这个场景真实且简单,不依赖外部业务。


五、关键代码

https://gitee.com/donglin-bit/pool-mq

1、定义 RabbitMQ 配置

java 复制代码
@Configuration
public class RabbitConfig {

    public static final String REPORT_COMP_EXCHANGE = "report.compensation.exchange";
    public static final String REPORT_COMP_QUEUE = "report.compensation.queue";
    public static final String REPORT_COMP_ROUTING = "report.compensation.routing";

    @Bean
    public DirectExchange reportCompensationExchange() {
        return new DirectExchange(REPORT_COMP_EXCHANGE);
    }

    @Bean
    public Queue reportCompensationQueue() {
        return new Queue(REPORT_COMP_QUEUE, true);
    }

    @Bean
    public Binding reportCompensationBinding() {
        return BindingBuilder
                .bind(reportCompensationQueue())
                .to(reportCompensationExchange())
                .with(REPORT_COMP_ROUTING);
    }
}

2、JSON 消息转换器(必须加)

java 复制代码
@Bean
public MessageConverter jacksonMessageConverter() {
    return new Jackson2JsonMessageConverter();
}

3、RabbitTemplate & Listener 使用 JSON

java 复制代码
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory,
                                     MessageConverter jacksonMessageConverter) {
    RabbitTemplate template = new RabbitTemplate(connectionFactory);
    template.setMessageConverter(jacksonMessageConverter);
    return template;
}

4、自定义拒绝策略(关键)

java 复制代码
@Slf4j
public class ReportRejectedHandler implements RejectedExecutionHandler {

    private final ReportTaskService reportTaskService;
    private final RabbitTemplate rabbitTemplate;

    public ReportRejectedHandler(ReportTaskService reportTaskService,
                                 RabbitTemplate rabbitTemplate) {
        this.reportTaskService = reportTaskService;
        this.rabbitTemplate = rabbitTemplate;
    }

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        if (r instanceof ReportTaskRunnable runnable) {

            // ① 标记任务为 REJECTED(数据库存证)
            reportTaskService.markRejected(
                    runnable.getTaskId(),
                    "Thread pool rejected, send to MQ"
            );

            // ② 发 MQ 补偿
            ReportTaskMessage msg = new ReportTaskMessage();
            msg.setTaskId(runnable.getTaskId());
            msg.setPatientId(runnable.getPatientId());
            msg.setExamId(runnable.getExamId());

            rabbitTemplate.convertAndSend(
                    RabbitConfig.REPORT_COMP_EXCHANGE,
                    RabbitConfig.REPORT_COMP_ROUTING,
                    msg
            );

            log.warn("任务被线程池拒绝,已发到 MQ taskId={}", runnable.getTaskId());
        }
    }
}

5、MQ 消费者补偿执行任务

java 复制代码
@Component
@RequiredArgsConstructor
@Slf4j
public class ReportCompensationConsumer {

    private final ThreadPoolExecutor reportExecutor;
    private final ReportTaskService reportTaskService;

    /**
     * 从补偿队列拿到任务,再交给线程池处理
     */
    @RabbitListener(queues = REPORT_COMP_QUEUE)
    public void onMessage(ReportTaskMessage msg) {
        log.info("收到报告补偿消息: {}", msg);

        ReportTaskRunnable runnable = new ReportTaskRunnable(
                msg.getTaskId(),
                msg.getPatientId(),
                msg.getExamId(),
                reportTaskService
        );

        try {
            reportExecutor.execute(runnable);
        } catch (RejectedExecutionException e) {
            // 极端情况下,补偿线程池也满了
            log.error("补偿线程池仍然满载,taskId={}", msg.getTaskId(), e);
            // 这里简单让异常抛出,Spring AMQP 默认会重试 / requeue
            throw e;
        }
    }
}

6、ThreadPool 配置

java 复制代码
@Configuration
public class ThreadPoolConfig {

    @Bean
    public ThreadPoolExecutor reportExecutor(ReportTaskService reportTaskService,
                                             RabbitTemplate rabbitTemplate) {
        int core = 2;
        int max = 4;
        long keepAlive = 60L;
        BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(5); // 小一点便于触发拒绝

        return new ThreadPoolExecutor(
                core,
                max,
                keepAlive,
                TimeUnit.SECONDS,
                queue,
                Executors.defaultThreadFactory(),
                new ReportRejectedHandler(reportTaskService, rabbitTemplate)
        );
    }
}

7、最终效果

数据库里:

  • 有的任务 INIT → SUCCESS
  • 有的任务 INIT → REJECTED → SUCCESS(通过 MQ 补偿)

这证明补偿流程完全闭环。


六、总结

线程池拒绝任务 → 自定义拒绝策略存证 + 投递 MQ
MQ 消费者重新提交线程池 → 最终任务一定被执行成功。

这是最常用的异步任务高可用方案


相关推荐
坊钰7 小时前
【Rabbit MQ】Rabbit MQ 介绍
java·rabbitmq
難釋懷7 小时前
分布式锁-redission锁的MutiLock原理
分布式
小北方城市网8 小时前
RabbitMQ 生产级实战:可靠性投递、高并发优化与问题排查
开发语言·分布式·python·缓存·性能优化·rabbitmq·ruby
乾元8 小时前
拒绝服务的进化:AI 调度下的分布式协同攻击策略
人工智能·分布式
听麟10 小时前
HarmonyOS 6.0+ PC端多设备文件拖拽协同开发实战:手眼同行增强与分布式软总线深度应用
分布式·华为·harmonyos
前端世界11 小时前
鸿蒙分布式网络性能优化实战:从通信建连到多设备协同
网络·分布式·harmonyos
雪碧聊技术11 小时前
什么是Zookeeper?
分布式·zookeeper
李白你好11 小时前
基于腾讯云函数 (SCF) 的分布式 IP 代理池.
分布式·tcp/ip·腾讯云
鱼跃鹰飞11 小时前
大厂面试真题-说说Kafka消息的不重复和不丢失
java·分布式·kafka