
在后端项目中,线程池是处理异步任务的核心组件 —— 从订单支付、库存扣减到日志记录、数据统计,几乎都依赖线程池提升并发能力。但很多开发者会忽略一个关键细节:不同业务场景的线程池,拒绝策略必须差异化选择。
我曾在电商项目中踩过典型的坑:用默认的AbortPolicy(直接抛异常)处理订单支付线程池,导致大促高峰期大量 “支付成功但订单未处理” 的异常;又曾为日志线程池选了CallerRunsPolicy(调用方执行),结果日志队列满时阻塞用户浏览商品的主线程,页面卡顿投诉激增。
本文结合电商项目中两个核心线程池的实战经验,拆解 “拒绝策略不一致” 的底层逻辑,帮你掌握 “按业务选策略” 的方法论,而非盲目套用默认配置。
在讲项目实战前,先快速回顾线程池拒绝策略的核心类型 ——JDK 默认提供 4 种策略,外加自定义策略,不同策略的 “任务处理逻辑” 和 “业务影响” 差异极大:
拒绝策略类型 | 核心逻辑 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
AbortPolicy(默认) | 直接抛出RejectedExecutionException | 快速失败,提醒开发者有问题 | 任务直接丢失,可能导致业务中断 | 非核心任务,且需感知队列满的场景 |
CallerRunsPolicy | 由提交任务的调用方(如主线程)执行任务 | 任务不丢失,避免业务中断 | 可能阻塞调用方,影响主线程性能 | 核心任务,且任务执行耗时短的场景 |
DiscardPolicy | 默默丢弃无法处理的任务,不抛异常 | 不影响调用方,性能无损耗 | 任务丢失,无感知难排查 | 非核心任务,任务丢失可容忍的场景 |
DiscardOldestPolicy | 丢弃队列中最旧的任务(队列头部),再尝试提交当前任务 | 尽量处理新任务,减少最新任务丢失 | 可能丢失重要旧任务 | 任务有 “时效性”,新任务比旧任务重要的场景 |
自定义策略 | 按业务逻辑自定义(如持久化任务到 DB/MQ) | 完全适配业务需求 | 开发成本高,需考虑异常处理 | 核心任务,绝对不能丢失的场景(如支付订单) |
关键前提:线程池触发拒绝策略的条件是 “核心线程满 + 队列满 + 最大线程满”,所以策略选择必须结合 “线程池参数配置” 与 “业务需求”,不能孤立决策。
我负责的电商项目中,有两个高频使用的线程池:订单支付处理线程池(核心业务)和用户行为日志线程池(非核心业务)。两者的拒绝策略完全不同,背后是 “业务优先级、任务容忍度、调用方影响” 的三重考量。
@Configuration@EnableAsyncpublic class ThreadPoolConfig { // 订单支付处理线程池 @Bean("orderPayExecutor") public Executor orderPayExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(20); // 核心线程数:日常峰值足够处理 executor.setMaxPoolSize(50); // 最大线程数:高峰期扩容上限 executor.setQueueCapacity(100); // 队列容量:故意设小,避免任务堆积过多导致延迟 executor.setKeepAliveSeconds(60); // 空闲线程存活时间:60秒 executor.setThreadNamePrefix("OrderPay-"); // 线程名前缀:便于日志排查 // 拒绝策略:CallerRunsPolicy(调用方执行) executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; }}决策过程中排除了其他策略,核心原因如下:
双 11 高峰期,该线程池 QPS 从日常 500 飙升至 2000,触发拒绝策略后:
@Configurationpublic class ThreadPoolConfig { // 用户行为日志线程池 @Bean("userBehaviorLogExecutor") public Executor userBehaviorLogExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); // 核心线程数:日常足够 executor.setMaxPoolSize(20); // 最大线程数:高峰期扩容 executor.setQueueCapacity(1000); // 队列容量:设大,尽量缓存任务 executor.setKeepAliveSeconds(30); // 空闲线程存活时间:30秒 executor.setThreadNamePrefix("UserLog-"); // 线程名前缀 // 拒绝策略:DiscardOldestPolicy(丢弃队列最旧任务,尝试提交当前任务) executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); executor.initialize(); return executor; }}这是踩坑后优化的结果,最初选了CallerRunsPolicy,导致严重问题:
优化后双 11 高峰期:
项目中两个线程池的策略差异,本质是 “业务属性决定技术选型”。总结出 3 个维度的决策框架,帮你快速匹配适合的拒绝策略:
这是首要维度,直接决定 “是否能容忍任务丢失”:
当 JDK 默认策略无法满足需求时,需自定义拒绝策略。比如项目中的 “退款任务线程池”,要求 “任务不丢失、不阻塞调用方、失败后重试”,此时自定义策略是最佳选择。
// 1. 自定义拒绝策略类public class RefundRejectedPolicy implements RejectedExecutionHandler { @Autowired private RedisTemplate<String, String> redisTemplate; private static final String REFUND_TASK_KEY = "refund:task:queue"; @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 判断任务是否为退款任务(强转验证) if (r instanceof RefundTask) { RefundTask refundTask = (RefundTask) r; // 将任务ID存入Redis列表(后续定时任务重试) String taskJson = JSON.toJSONString(refundTask.getRefundId()); redisTemplate.opsForList().rightPush(REFUND_TASK_KEY, taskJson); log.info("退款任务线程池满,任务存入Redis重试,退款ID:{}", refundTask.getRefundId()); } else { // 非退款任务,按默认策略处理(抛异常) throw new RejectedExecutionException("不支持的任务类型:" + r.getClass().getName()); } }}// 2. 退款任务线程池配置@Bean("refundExecutor")public Executor refundExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(15); executor.setMaxPoolSize(30); executor.setQueueCapacity(200); executor.setThreadNamePrefix("Refund-"); // 配置自定义拒绝策略 executor.setRejectedExecutionHandler(new RefundRejectedPolicy()); executor.initialize(); return executor;}// 3. 定时任务重试Redis中的退款任务@Scheduled(fixedRate = 60000) // 每分钟执行一次public void retryRefundTask() { // 从Redis中取出未处理的退款任务ID String taskJson; while ((taskJson = redisTemplate.opsForList().leftPop(REFUND_TASK_KEY, 0, TimeUnit.SECONDS)) != null) { Long refundId = JSON.parseObject(taskJson, Long.class); // 提交任务到线程池(此时线程池可能已空闲) refundExecutor.execute(new RefundTask(refundId)); }}线程池拒绝策略的本质是 “业务风险与技术成本的平衡”—— 没有 “最好的策略”,只有 “最适合业务的策略”:
记住:线程池的每一个参数(核心线程数、队列容量、拒绝策略)都应服务于业务需求,而非盲目照搬网上的 “最优配置”。只有深入理解业务优先级和任务属性,才能做出正确的决策,让线程池成为提升系统性能的利器,而非引发故障的隐患。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。