这是一个非常高级且符合生产环境实战需求的思考方向。
正如你所说,JDK 自带的策略要么太粗暴(抛异常、丢弃),要么有隐患(阻塞主线程)。将溢出的任务**“持久化到存储(Redis/DB)”,实现“削峰填谷”**,是高并发系统中保证数据不丢失的最佳实践之一。
下面我为你编写一个完整的 Demo,展示如何实现一个**“基于 Redis 持久化的自定义拒绝策略”**。
核心思路
- 定义任务接口: 任务必须携带业务数据(因为
Runnable本身很难序列化),以便我们将关键数据存入 Redis。 - 自定义 Handler: 实现
RejectedExecutionHandler接口,在rejectedExecution方法中捕获被拒绝的任务,提取业务数据,写入存储。 - 后续处理(补偿机制): 你需要另外写一个定时任务(Scheduled Task),在系统空闲时从 Redis 拉取这些数据,重新丢进线程池。
代码实现
1. 定义一个可持久化的任务接口
为了能把任务存到数据库,我们需要任务对象能提供它的“业务参数”。
import lombok.AllArgsConstructor;
import lombok.Data;
// 定义一个业务任务基类,包含业务ID和参数
@Data
@AllArgsConstructor
public abstract class PersistableTask implements Runnable {
private String businessId; // 比如 订单ID
private String taskType; // 任务类型,用于区分不同业务
private String payload; // 核心数据,比如 JSON 格式的订单详情
@Override
public abstract void run();
}
2. 模拟 Redis/数据库保存服务
在真实场景中,这里调用 Redis 的 lpush 或者 MySQL 的 insert。
class AsyncPersistenceService {
public void saveTaskToRedis(PersistableTask task) {
// 模拟存入 Redis
System.out.println("【降级处理】🚨 线程池已满!正在将任务持久化到 Redis...");
System.out.println(String.format(" >> Key: failed_tasks:%s, Value: {id:%s, payload:%s}",
task.getTaskType(), task.getBusinessId(), task.getPayload()));
// 真实代码示例:
// redisTemplate.opsForList().leftPush("failed_tasks:" + task.getTaskType(), JSON.toJSONString(task));
}
}
3. 核心:自定义拒绝策略 (PersistenceRejectionPolicy)
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
public class PersistenceRejectionPolicy implements RejectedExecutionHandler {
private final AsyncPersistenceService persistenceService;
public PersistenceRejectionPolicy(AsyncPersistenceService persistenceService) {
this.persistenceService = persistenceService;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 判断线程池是否正在关闭,如果关闭了就没办法了,只能放弃或打印日志
if (executor.isShutdown()) {
System.err.println("线程池已关闭,任务直接丢弃");
return;
}
// 2. 关键点:尝试将 Runnable 还原为我们定义的 PersistableTask
if (r instanceof PersistableTask) {
PersistableTask task = (PersistableTask) r;
try {
// 3. 执行持久化逻辑 (写入 Redis/MySQL)
persistenceService.saveTaskToRedis(task);
} catch (Exception e) {
// 如果连 Redis 都挂了,那就是彻底的灾难了,记录本地日志
System.err.println("【严重错误】持久化失败,任务丢失: " + task.getBusinessId());
e.printStackTrace();
}
} else {
// 如果提交的不是我们封装的任务类型,只能打印日志或根据情况处理
System.err.println("【警告】未知任务类型被拒绝,无法持久化: " + r.toString());
}
}
}
4. 实战演示:将它们串联起来
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolDemo {
public static void main(String[] args) {
// 1. 初始化持久化服务
AsyncPersistenceService persistenceService = new AsyncPersistenceService();
// 2. 创建自定义线程池
// 为了演示效果,我们将核心设为1,最大设为1,队列设为1。
// 这意味着第3个任务就会触发拒绝策略。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
1,
1,
0L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1), // 队列容量 1
new ThreadPoolExecutor.DefaultThreadFactory(),
new PersistenceRejectionPolicy(persistenceService) // 👉 使用我们自定义的策略
);
// 3. 模拟提交 5 个任务
for (int i = 1; i <= 5; i++) {
String orderId = "ORDER_" + i;
String payload = "{amount: 100}";
// 创建任务
PersistableTask task = new PersistableTask(orderId, "ORDER_PROCESS", payload) {
@Override
public void run() {
try {
System.out.println("✅ 正在处理业务: " + getBusinessId());
Thread.sleep(1000); // 模拟耗时,堵塞线程池
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
System.out.println("提交任务: " + orderId);
executor.execute(task); // 注意:这里用 execute 而不是 submit
}
executor.shutdown();
}
}
运行结果预期
你会看到如下输出:
提交任务: ORDER_1
✅ 正在处理业务: ORDER_1 <-- 线程池拿走执行
提交任务: ORDER_2 <-- 进入队列等待
提交任务: ORDER_3 <-- 线程池满+队列满,触发拒绝策略!
【降级处理】🚨 线程池已满!正在将任务持久化到 Redis...
>> Key: failed_tasks:ORDER_PROCESS, Value: {id:ORDER_3, payload:{amount: 100}}
提交任务: ORDER_4
【降级处理】🚨 线程池已满!正在将任务持久化到 Redis...
>> Key: failed_tasks:ORDER_PROCESS, Value: {id:ORDER_4, payload:{amount: 100}}
...
💡 两个关键的技术细节
1. 为什么推荐用 execute() 而不是 submit()?
这是一个大坑。
submit(Runnable task)会把你的Runnable包装成一个FutureTask对象。- 如果使用
submit,当你自定义的拒绝策略拿到Runnable r时,它是FutureTask类型,你无法强制转换成你的PersistableTask,也就拿不到里面的businessId和payload。 - 解决办法: 如果非要用
submit,你需要通过反射去扒FutureTask里的原始对象,或者继承ThreadPoolExecutor重写beforeExecute,但这太复杂了。对于这种异步丢进池子就不管的场景,直接用execute是最简单的。
2. “亡羊补牢”的定时任务
持久化只是第一步,别忘了“补救”。你需要写一个定时任务(比如每 10 分钟运行一次):
// 伪代码:补偿任务
@Scheduled(cron = "0 0/10 * * * ?")
public void retryFailedTasks() {
// 1. 检查当前线程池是否空闲(队列大小 < 阈值)
if (threadPool.getQueue().size() > 50) return;
// 2. 从 Redis 弹出一个任务
String json = redis.rpop("failed_tasks:ORDER_PROCESS");
// 3. 重新提交
if (json != null) {
PersistableTask task = JSON.parseObject(json, MyOrderTask.class);
threadPool.execute(task);
}
}
总结
这套方案通过**“自定义拒绝策略 + Redis 缓冲 + 定时补偿”**,完美解决了 JDK 默认策略的缺陷。这正是大厂处理高并发订单、日志、消息推送时的标准模式。
171万+

被折叠的 条评论
为什么被折叠?



