分布式重试中不能直接用jdk原生discardpolicy,因其无日志、无监控、不可追溯;应实现safediscardpolicy,满足可追溯(记录任务类型/业务id/重试次数)、可度量(上报prometheus指标)、可收敛(采样日志+动态开关)三大要求。

分布式重试机制中,当任务队列已满、线程池饱和或下游服务持续不可用时,新来的重试任务不能无限制堆积——这时需要一个可控的丢弃策略。DiscardPolicy 是 JDK 线程池内置的一种拒绝策略,它什么也不做,直接丢弃任务。但“默默丢弃”在分布式场景下风险极高:没日志、没告警、没补偿,等于让业务“静默失败”。所以,我们真正要写的,不是原样照搬 ThreadPoolExecutor.DiscardPolicy,而是一个有上下文感知、可审计、带轻量监控的“安全丢弃器”。
为什么不能直接用原生 DiscardPolicy?
原生 DiscardPolicy 的实现只有一行:return;。它:
- 不记录被丢弃的任务内容(比如重试的接口名、参数 ID、重试次数)
- 不触发任何可观测行为(无日志、无指标、无告警)
- 无法区分是“合理背压”还是“异常雪崩”,运维完全无感
- 在分布式重试中,可能丢掉的是用户支付回调、订单超时补单等关键任务
一个靠谱的分布式 DiscardPolicy 长什么样?
它得满足三个基本要求:可追溯、可度量、可收敛。不是“丢”,而是“有据可依地丢”。建议结构如下:
-
记录关键元数据:任务类型(如
OrderTimeoutRetryTask)、业务 ID(如order_123456)、当前重试次数、丢弃时间戳、所属线程池名 - 异步打点 + 采样日志:高频丢弃时避免日志刷爆磁盘,按固定比例(如 1%)打印完整 DEBUG 日志,其余仅上报指标
-
暴露丢弃计数器:接入 Micrometer / Prometheus,暴露
retry_task_discarded_total{policy="safe_discard", pool="retry-pool-1"} - 支持动态降级开关(可选):通过配置中心控制是否启用丢弃(比如故障期间临时切为阻塞或降级为本地延时队列)
手写 SafeDiscardPolicy 实战代码
以下是一个 Spring Boot 项目中可用的轻量实现(基于 RejectedExecutionHandler 接口):
public class SafeDiscardPolicy implements RejectedExecutionHandler {
private static final Logger log = LoggerFactory.getLogger(SafeDiscardPolicy.class);
private final MeterRegistry meterRegistry;
private final String poolName;
private final int sampleRate; // 每 100 次丢弃打 1 条完整日志
public SafeDiscardPolicy(MeterRegistry meterRegistry, String poolName) {
this(meterRegistry, poolName, 100);
}
public SafeDiscardPolicy(MeterRegistry meterRegistry, String poolName, int sampleRate) {
this.meterRegistry = meterRegistry;
this.poolName = poolName;
this.sampleRate = sampleRate;
}
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 1. 上报指标
Counter.builder("retry.task.discarded")
.tag("policy", "safe_discard")
.tag("pool", poolName)
.register(meterRegistry)
.increment();
// 2. 尝试提取业务上下文(假设 Runnable 是自定义重试任务)
if (r instanceof RetryTask task) {
String bizId = task.getBizId();
String taskType = task.getClass().getSimpleName();
int retryCount = task.getRetryCount();
// 3. 采样日志
if (ThreadLocalRandom.current().nextInt(sampleRate) == 0) {
log.warn("[DISCARD] pool={} type={} bizId={} retryCount={} queueSize={} active={} max={}",
poolName, taskType, bizId, retryCount,
executor.getQueue().size(), executor.getActiveCount(), executor.getMaximumPoolSize());
}
} else {
// 非标准任务,至少记个类名
if (ThreadLocalRandom.current().nextInt(sampleRate) == 0) {
log.warn("[DISCARD] pool={} unknown task: {}", poolName, r.getClass().getName());
}
}
}
}
使用时,在构建重试线程池时传入即可:
new ThreadPoolExecutor(
core, max, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue(queueCapacity),
new CustomThreadFactory("retry-pool"),
new SafeDiscardPolicy(meterRegistry, "retry-pool-1")
);
配套建议:别只靠丢弃,要形成闭环
丢弃只是最后一道防线。搭配以下措施才真正可靠:
- 前置限流:在任务入队前,用 Sentinel 或 Redis 计数器对相同 bizId 的重试请求限频
- 分级重试:核心任务走高优先级线程池 + 持久化队列(如 RocketMQ),非核心才走内存池 + SafeDiscardPolicy
-
丢弃后补偿钩子(进阶):在
rejectedExecution中触发异步补偿,例如把丢弃任务写入死信表,供人工核查或定时重投 -
监控看板:将
retry_task_discarded_total与下游成功率、RT、线程池活跃度放在同一面板,设置突增告警(如 5 分钟内 > 100 次)
丢弃本身不难,难的是让丢弃变得“可信任”。写一个 SafeDiscardPolicy,本质是在混沌中划出一条清晰的止损边界——它不解决问题,但它让你知道问题在哪,以及还能撑多久。










