【线程池】优化等待队列和拒绝策略
- 【一】问题描述
- 【二】核心原则
- 【三】优化方案
-
- [【1】轻量级方案:Redis 持久化 + 自定义队列](#【1】轻量级方案:Redis 持久化 + 自定义队列)
-
- (1)定义可序列化任务基类
- [(2)自定义「有界内存 + Redis 持久化」等待队列](#(2)自定义「有界内存 + Redis 持久化」等待队列)
- (3)自定义拒绝策略(永不丢弃任务)
- [(4)SpringBoot 线程池配置(核心)](#(4)SpringBoot 线程池配置(核心))
- [(5)定时补偿任务(空闲时执行 Redis 溢出任务)](#(5)定时补偿任务(空闲时执行 Redis 溢出任务))
- (6)使用示例(业务任务)
- [【2】高并发方案:MQ 中间件解耦(生产核心业务首选)](#【2】高并发方案:MQ 中间件解耦(生产核心业务首选))
- 【3】数据库持久化方案
【一】问题描述
SpringBoot 项目中原生线程池等待队列存在二选一痛点:
(1)用无界队列(LinkedBlockingQueue):任务无限堆积→OOM 内存溢出
(2)用有界队列(ArrayBlockingQueue):队列满→触发拒绝策略→任务丢失
要实现任务不丢失 + 绝不 OOM,核心改造思路是:【有界内存队列(防 OOM) + 持久化兜底队列(防丢失) + 自定义拒绝策略 + 定时补偿执行】,兼顾内存安全和任务可靠性。
【二】核心原则
(1)内存队列必须有界:严格限制大小,杜绝无限堆积导致 OOM
(2)溢出任务持久化:内存队列满了,任务不落内存,直接落盘(Redis/DB/ 文件)
(3)拒绝策略重写:不抛异常、不丢弃任务,转而存入持久化队列
(4)空闲补偿执行:线程池空闲时,把持久化任务回刷到内存队列执行
(5)任务幂等:补偿执行可能重复,需保证任务幂等性
【三】优化方案
【1】轻量级方案:Redis 持久化 + 自定义队列
适合中小并发、简单业务,基于 SpringBoot+Redis 实现,无需额外部署中间件,完美兼顾 OOM 防护 + 任务不丢失。
(1)定义可序列化任务基类
所有线程池任务实现该接口,保证 Redis 可存储:
java
import java.io.Serializable;
/**
* 可持久化任务基类(必须序列化)
*/
public interface PersistableTask extends Runnable, Serializable {
// 任务唯一标识(用于去重)
String getTaskId();
}
(2)自定义「有界内存 + Redis 持久化」等待队列
重写队列逻辑:内存满→存 Redis,内存空→从 Redis 拉取,从根源防 OOM、防丢失:
java
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义线程池等待队列:有界内存队列 + Redis持久化兜底
* 核心:内存队列有界防OOM,Redis存溢出任务防丢失
*/
public class PersistableTaskQueue extends AbstractExecutorService {
// 内存有界队列(核心:限制大小,防OOM,根据服务器内存调,建议100~1000)
private final BlockingQueue<Runnable> memoryQueue;
// Redis持久化队列(溢出任务存这里)
private final RedisTemplate<String, Object> redisTemplate;
// 持久化队列Key
private static final String REDIS_QUEUE_KEY = "threadpool:overflow:queue";
// 内存队列最大容量
private static final int MEMORY_QUEUE_SIZE = 500;
public PersistableTaskQueue(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
// 内存队列:有界ArrayBlockingQueue,杜绝OOM
this.memoryQueue = new ArrayBlockingQueue<>(MEMORY_QUEUE_SIZE);
}
// 任务入队:内存能放下→入内存;放不下→入Redis
public boolean offer(Runnable task) {
if (memoryQueue.offer(task)) {
return true;
}
// 内存队列满,溢出任务存入Redis(持久化,不丢失)
if (task instanceof PersistableTask) {
redisTemplate.opsForList().leftPush(REDIS_QUEUE_KEY, task);
return true;
}
return false;
}
// 任务出队:先拿内存,内存空→从Redis拉取
public Runnable take() throws InterruptedException {
Runnable task = memoryQueue.poll();
if (task == null) {
// 内存空,从Redis拉取溢出任务
task = (Runnable) redisTemplate.opsForList().rightPop(REDIS_QUEUE_KEY);
if (task == null) {
// 无任务,阻塞等待
task = memoryQueue.take();
}
}
return task;
}
// 省略其他重写方法(直接实现即可)
@Override
public void shutdown() {}
@Override
public List<Runnable> shutdownNow() {return null;}
@Override
public boolean isShutdown() {return false;}
@Override
public boolean isTerminated() {return false;}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) {return false;}
@Override
public void execute(Runnable command) {offer(command);}
}
(3)自定义拒绝策略(永不丢弃任务)
原生拒绝策略会丢任务,重写后溢出任务直接入 Redis 持久化:
java
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 自定义拒绝策略:任务溢出→存入Redis,不丢失、不抛异常
*/
public class PersistableRejectHandler implements RejectedExecutionHandler {
private final RedisTemplate<String, Object> redisTemplate;
private static final String REDIS_QUEUE_KEY = "threadpool:overflow:queue";
public PersistableRejectHandler(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 触发拒绝策略=线程池满载,直接把任务存入Redis
if (r instanceof PersistableTask) {
redisTemplate.opsForList().leftPush(REDIS_QUEUE_KEY, r);
}
}
}
(4)SpringBoot 线程池配置(核心)
配置线程池参数,绑定自定义队列和拒绝策略:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
public class ThreadPoolConfig {
// 核心线程数(IO密集型:CPU核心数*2;CPU密集型:CPU核心数+1)
private static final int CORE_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 2;
// 最大线程数(不要过大,避免线程切换开销)
private static final int MAX_POOL_SIZE = Runtime.getRuntime().availableProcessors() * 4;
// 空闲线程存活时间
private static final long KEEP_ALIVE_TIME = 60L;
@Bean
public ThreadPoolExecutor persistableThreadPool(RedisTemplate<String, Object> redisTemplate) {
// 自定义持久化队列
PersistableTaskQueue taskQueue = new PersistableTaskQueue(redisTemplate);
// 自定义拒绝策略
PersistableRejectHandler rejectHandler = new PersistableRejectHandler(redisTemplate);
return new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
taskQueue, // 绑定自定义队列
new ThreadPoolExecutor.CallerRunsPolicy(), // 备用拒绝策略
rejectHandler // 核心拒绝策略
);
}
}
(5)定时补偿任务(空闲时执行 Redis 溢出任务)
保证 Redis 中的溢出任务最终被执行,无遗漏:
java
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 定时补偿:将Redis溢出任务回刷到线程池执行
*/
@Component
public class TaskCompensateScheduler {
private final ThreadPoolExecutor threadPool;
private final RedisTemplate<String, Object> redisTemplate;
private static final String REDIS_QUEUE_KEY = "threadpool:overflow:queue";
public TaskCompensateScheduler(ThreadPoolExecutor threadPool, RedisTemplate<String, Object> redisTemplate) {
this.threadPool = threadPool;
this.redisTemplate = redisTemplate;
}
// 每5秒补偿一次(根据业务调频率)
@Scheduled(fixedRate = 5000)
public void compensateTask() {
// 仅当线程池空闲时,拉取Redis任务执行
if (threadPool.getQueue().size() < 100) {
Object task = redisTemplate.opsForList().rightPop(REDIS_QUEUE_KEY);
if (task != null) {
threadPool.execute((Runnable) task);
}
}
}
}
(6)使用示例(业务任务)
java
/**
* 业务任务(实现PersistableTask,支持持久化)
*/
public class BizTask implements PersistableTask {
private String taskId;
private String bizData;
public BizTask(String taskId, String bizData) {
this.taskId = taskId;
this.bizData = bizData;
}
@Override
public String getTaskId() {
return taskId;
}
@Override
public void run() {
// 业务逻辑:保证幂等性
System.out.println("执行任务:" + bizData);
}
}
// 业务中使用
@Service
public class BizService {
@Resource
private ThreadPoolExecutor threadPool;
public void submitTask() {
// 提交任务,无论并发多大,都不会OOM、不会丢失
threadPool.execute(new BizTask("task_001", "测试业务数据"));
}
}
【2】高并发方案:MQ 中间件解耦(生产核心业务首选)
适合高并发、核心链路业务,彻底把线程池和任务缓冲解耦,100% 防 OOM、防丢失:
(1)线程池只配置小容量有界队列(如 200),杜绝 OOM
(2)任务直接发送到RabbitMQ/RocketMQ/Kafka(消息持久化,不丢失)
(3)MQ 消费者监听消息,提交到线程池执行
(4)消费失败→MQ 重试 / 死信队列,保证任务最终执行
核心优势
(1)线程池无堆积压力→永不 OOM
(2)MQ 消息持久化→任务绝对不丢失
(3)削峰填谷,适配流量波动
【3】数据库持久化方案
(1)使用场景
对实时性要求不高,例如大数据平台计算T-1数据,先创建待运行的任务实例,然后通过定时任务批量执行任务实例。因为这些任务在初始化创建的时候是先创建到数据库实现持久化的,所以不会出现任务丢失的情况;使用有界队列可以保证不会出现队列OOM的问题
(2)设计思路
1-首先,创建n条任务信息到数据库,每条任务有状态、重试次数、重试次数上限等等关键信息
2-然后,初始化线程池,队列初始化有界队列,拒绝策略选择丢弃任务
ThreadPoolExecutor.DiscardPolicy(丢弃新任务策略): 不处理新任务,直接丢弃掉
ThreadPoolExecutor.DiscardOldestPolicy(丢弃最早未处理): 此策略将丢弃最早的未处理的任务请求。
3-xxljob定时调度任务,先到数据库查询状态为【未完成】或状态为【失败】但重试次数没有超过上限的任务n条,遍历到线程池中处理,如果没有被拒绝,状态重置为【运行中】,如果执行成功就修改状态为【成功】,失败就修改状态为【失败】;如果等待队列满了被丢弃就保持原状态,下次定时任务还会捞取满足条件的非成功的实例