不同的业务场景对任务丢失的容忍度、响应延迟的要求、系统保护的需求各不相同。下面通过 6 个典型场景,分析如何选择合适的拒绝策略,并给出代码示例和注意事项。
场景1:电商订单支付(核心交易链路)
业务特点:
- 每一笔支付请求都必须处理,不能丢失。
- 支付操作涉及数据库更新、第三方网关调用、消息发送等。
- 要求高一致性,失败需要明确感知并触发重试或补偿。
压力情况:大促时瞬间流量激增,线程池可能饱和。
选择策略 :AbortPolicy + 上层统一捕获异常,进行异步重试或放入死信队列。
理由:
- 支付任务绝对不能静默丢弃(
DiscardPolicy不可用)。 - 不能让调用者线程执行支付任务(
CallerRunsPolicy会阻塞 Tomcat 线程,导致整个服务响应变慢)。 - 抛出异常是最明确的失败信号,由调用方(通常是 Controller 或 Service)捕获后,可以将任务转储到消息队列或数据库,稍后重试。
代码示例:
java
// 线程池配置
@Bean("paymentExecutor")
public Executor paymentExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
20, 50, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new NamedThreadFactory("payment"),
new ThreadPoolExecutor.AbortPolicy() // 显式抛出异常
);
return executor;
}
// 业务调用处
@Service
public class PaymentService {
@Autowired
private ThreadPoolExecutor paymentExecutor;
public void processPayment(PaymentRequest request) {
try {
paymentExecutor.execute(() -> doPayment(request));
} catch (RejectedExecutionException e) {
// 线程池饱和,将任务写入重试队列(如 RocketMQ、Redis 等)
saveToRetryQueue(request);
log.warn("Payment task rejected, saved to retry queue. requestId={}", request.getId());
// 可选:向调用方返回"系统繁忙,稍后重试"的提示
}
}
}
监控指标:拒绝次数必须为 0,一旦出现立即告警并扩容。
场景2:秒杀扣库存(瞬时高并发,允许快速失败)
业务特点:
- 请求量瞬间爆炸,但真正能成功秒杀到的用户只有一小部分。
- 要求极低的响应延迟,不能排队等待。
- 超出处理能力的请求应该被快速拒绝,返回"已售罄"或"系统繁忙"。
压力情况:QPS 从几百瞬间飙升到几十万。
选择策略 :AbortPolicy 或 DiscardPolicy + 前端友好提示。
理由:
- 使用
SynchronousQueue+ 有限最大线程数(如 200),任何超出并发能力的请求立即触发拒绝。 - 用
AbortPolicy抛异常,或者用DiscardPolicy静默丢弃,但都需要在业务层捕获并返回统一的失败响应。 - 不能使用
CallerRunsPolicy,因为调用者(Tomcat 工作线程)执行秒杀任务会严重拖垮整个 Web 容器。
代码示例:
java
// 秒杀专用线程池
int maxConcurrency = 200; // 根据压测得出系统能承受的最大并发扣库存操作
ExecutorService seckillExecutor = new ThreadPoolExecutor(
0, maxConcurrency, 30L, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new NamedThreadFactory("seckill"),
new ThreadPoolExecutor.AbortPolicy()
);
// 秒杀接口
@PostMapping("/seckill")
public Result seckill(Long goodsId, Long userId) {
try {
seckillExecutor.execute(() -> {
// 扣库存、创建订单等核心操作
inventoryService.decr(goodsId);
orderService.create(userId, goodsId);
});
return Result.success("抢购中,请稍后查看订单");
} catch (RejectedExecutionException e) {
// 线程池满,直接返回失败
return Result.error("很遗憾,您没抢到,下次加油");
}
}
优化点:可以在拒绝策略中直接记录指标,但无需重试,因为秒杀失败就是最终结果。
场景3:异步发送短信/邮件(非关键通知,允许少量丢失)
业务特点:
- 用户注册、下单后发送确认短信/邮件。
- 即使少量消息发送失败,也不影响核心业务(用户可以通过其他渠道重试)。
- 不希望因消息发送阻塞主流程。
压力情况:业务高峰时消息量较大,但系统可以接受一定程度的丢弃。
选择策略 :DiscardOldestPolicy 或 DiscardPolicy + 日志记录。
理由:
- 消息堆积过久反而不如丢弃旧消息,保证新消息能及时发出(
DiscardOldestPolicy)。 - 如果消息完全可丢弃(如运营推广短信),直接用
DiscardPolicy。 - 不能使用
CallerRunsPolicy,因为主线程(如订单完成后的异步通知)不应被阻塞。
代码示例:
java
// 通知线程池
ThreadPoolExecutor notifyExecutor = new ThreadPoolExecutor(
5, 20, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
new NamedThreadFactory("notify"),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
// 发送短信
public void sendSms(String phone, String content) {
notifyExecutor.execute(() -> {
try {
smsClient.send(phone, content);
} catch (Exception e) {
log.error("Send sms failed, phone={}", phone, e);
// 可选:记录失败到数据库,由定时任务补偿
}
});
}
监控:可以统计丢弃数量,如果丢弃率过高(如 >1%),考虑扩容或优化短信通道。
场景4:日志/审计记录(海量低价值,可丢弃)
业务特点:
- 每条请求都需要记录访问日志、用户行为日志。
- 数据量极大(每秒数万条),对实时性要求低。
- 偶尔丢失几条日志对业务无影响。
压力情况:持续高吞吐,磁盘或网络可能成为瓶颈。
选择策略 :DiscardPolicy(静默丢弃)。
理由:
- 日志系统不应该拖垮主业务。如果线程池满了,说明下游(如日志服务器、ES)已经处理不过来,再排队只会加剧问题。
- 直接丢弃是最简单有效的自我保护。
- 也可以自定义策略,将丢弃的日志采样记录(用于分析丢失率)。
代码示例:
java
ThreadPoolExecutor logExecutor = new ThreadPoolExecutor(
2, 10, 10L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("access-log"),
new ThreadPoolExecutor.DiscardPolicy()
);
// 记录访问日志
public void logAccess(HttpServletRequest request) {
logExecutor.execute(() -> {
// 构建日志对象,发送到 Kafka 或写入本地文件
accessLogService.save(parseLog(request));
});
}
进阶:可以结合采样,在丢弃时随机记录 1% 的丢弃事件用于监控。
场景5:批量数据导入(任务重,不允许丢失,可接受延迟)
业务特点:
- 从文件、数据库批量导入数据,每个任务执行时间较长(秒级到分钟级)。
- 任务数量固定,不允许丢失。
- 可以接受导入速度变慢,但不能失败。
压力情况:任务提交可能短时间集中,但总任务量可控。
选择策略 :CallerRunsPolicy。
理由:
- 当线程池饱和时,由调用者线程(例如主线程或定时任务线程)直接执行导入任务,这样不会丢失任务,同时会自然降低新任务的提交速度。
- 因为导入任务本身是重量级操作,调用者执行虽然会阻塞,但总比丢弃好。
- 配合有界队列,防止内存溢出。
代码示例:
java
ThreadPoolExecutor importExecutor = new ThreadPoolExecutor(
4, 8, 5L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(10), // 小队列,让拒绝策略尽快生效
new NamedThreadFactory("data-import"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 批量提交导入任务
public void importLargeFiles(List<File> files) {
for (File file : files) {
importExecutor.execute(() -> importOneFile(file));
}
importExecutor.shutdown();
importExecutor.awaitTermination(1, TimeUnit.HOURS);
}
注意 :CallerRunsPolicy 可能会导致调用者线程长时间阻塞,如果调用者是定时任务线程,可能影响其他定时任务。可以将调用者线程池也设置得足够健壮。
场景6:与消息队列结合(最终一致性,高可靠性)
业务特点:
- 任务必须处理,但不能阻塞当前线程。
- 希望削峰填谷,利用消息队列的持久化能力。
选择策略:自定义拒绝策略,将任务转发到 RocketMQ、Kafka 等。
理由:
- 线程池只处理实时部分,当线程池饱和时,将任务写入消息队列,由消费者异步处理。
- 这样既保护了系统,又保证了任务不丢失。
代码示例:
java
public class MQBackedRejectedHandler implements RejectedExecutionHandler {
private final RocketMQTemplate mqTemplate;
private final String topic;
public MQBackedRejectedHandler(RocketMQTemplate mqTemplate, String topic) {
this.mqTemplate = mqTemplate;
this.topic = topic;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (executor.isShutdown()) {
return;
}
// 将任务序列化后发送到 MQ
if (r instanceof SerializableTask) {
mqTemplate.syncSend(topic, ((SerializableTask) r).getPayload());
} else {
// 兜底:记录到数据库
saveToDatabase(r);
}
}
}
// 线程池配置
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 50, 60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new NamedThreadFactory("worker"),
new MQBackedRejectedHandler(mqTemplate, "rejected-task-topic")
);
注意:发送 MQ 本身也可能失败,需要做好重试和监控。
场景总结表
| 业务场景 | 推荐拒绝策略 | 理由 | 风险提示 |
|---|---|---|---|
| 支付/下单(核心交易) | AbortPolicy + 上层重试 |
必须明确失败,不能静默丢弃 | 调用方需处理异常 |
| 秒杀/抢购(快速失败) | AbortPolicy / DiscardPolicy |
追求低延迟,超出直接拒绝 | 丢弃率可能较高,前端需友好提示 |
| 异步通知(短信/邮件) | DiscardOldestPolicy |
保证新消息优先,可少量丢失 | 旧消息可能丢失 |
| 日志/审计 | DiscardPolicy |
海量数据,可丢失 | 监控丢弃率,避免过高的丢失 |
| 批量导入(不允许丢) | CallerRunsPolicy |
由调用者执行,不丢失 | 调用者可能阻塞 |
| 高可靠异步任务 | 自定义(转 MQ/DB) | 削峰填谷,保证最终执行 | 增加系统复杂度 |
最佳实践建议
- 默认不要使用
AbortPolicy而毫无处理 :至少要在业务代码中捕获RejectedExecutionException,记录日志或触发降级。 - 非核心业务优先使用
DiscardPolicy并记录丢弃次数:用于容量规划。 - 所有拒绝策略都应该有监控 :通过 Micrometer、Prometheus 暴露
rejected.count指标。 CallerRunsPolicy要谨慎评估调用者线程:如果调用者是 Web 请求线程,可能导致请求超时堆积。- 自定义拒绝策略时不要执行过于耗时的操作(如写数据库、发 MQ),否则会加剧线程池的阻塞。
通过结合具体业务场景选择合适的拒绝策略,可以平衡系统稳定性 、任务可靠性 和响应延迟三者之间的关系,构建高可用的并发系统。