3. 错误处理基础理论:错误类型分类、传播机制与补偿策略
在异步任务编排中,错误处理不是简单的 try-catch 就能覆盖的。由于任务之间存在依赖、并发、超时和重试,错误的类型、传播路径以及补偿方式必须从系统架构层面进行设计。本章从错误分类出发,逐步建立错误传播模型、超时与重试策略,以及幂等性设计原则,为后续的补偿机制和链路设计打下理论基础。
3.1 错误类型分类:可恢复与不可恢复
错误分类是补偿策略选择的第一步。根据错误是否可以通过重试或替代路径恢复,将错误分为两大类:
| 错误类型 | 定义 | 典型场景 | 补偿策略 |
|---|---|---|---|
| 可恢复错误 | 错误原因具有临时性,重试或等待后可能成功 | 网络超时、数据库连接池满、服务限流、CPU 负载高 | 指数退避重试、切换副本、降级调用 |
| 不可恢复错误 | 错误原因具有确定性,重试无法改变结果 | 参数校验失败、权限不足、数据格式错误、业务逻辑冲突 | 立即失败、触发补偿事务、人工介入 |
实践建议:在异步任务编排框架中,建议为每个任务显式声明
recoverable 属性。框架根据该属性决定是否自动触发重试,避免对不可恢复错误进行无意义的重复尝试。
错误分类的决策流程可以用以下 SVG 图表示:
3.2 错误传播机制
在异步任务链路中,错误不会自动向上冒泡。每个任务节点必须显式定义错误传播行为。常见的传播模式有三种:
- 向上传播(Propagate Up):子任务失败后,将错误传递给父任务或编排器,由上层决定是否终止链路。
- 向下传播(Propagate Down):父任务失败后,主动取消所有未执行的子任务,避免资源浪费。
- 旁路传播(Side-channel):通过事件总线或回调队列将错误信息发送给监控系统或补偿处理器,主链路继续执行。
关键设计点:在 CompletableFuture 或 Go channel 模型中,错误传播需要显式处理
exceptionally() 或 select 分支。不要依赖隐式异常传递,否则会导致任务“静默失败”。
以下 SVG 图展示了三种传播模式在异步链路中的位置:
3.3 超时与重试策略
超时和重试是处理可恢复错误的核心手段。设计时需考虑三个维度:超时粒度、重试间隔和重试次数上限。
3.3.1 超时策略
- 任务级超时:为每个异步任务设置独立的超时时间,超时后触发
TimeoutException。 - 链路级超时:为整个编排链路设置总超时时间,超过后终止所有未完成任务。
- 动态超时:根据历史 P99 延迟动态调整超时阈值,适用于网络波动较大的场景。
3.3.2 重试策略
| 策略名称 | 间隔计算方式 | 适用场景 | 风险 |
|---|---|---|---|
| 固定间隔 | 每次重试等待固定时间(如 1s) | 内部服务调用,延迟稳定 | 可能造成资源争抢 |
| 指数退避 | 间隔 = base × 2^n,n 为重试次数 | 外部 API、网络请求 | 等待时间可能过长 |
| 抖动退避 | 间隔 = base × 2^n + random(0, jitter) | 高并发分布式系统 | 实现稍复杂 |
| 立即重试 | 间隔 = 0 | 连接池满、瞬态错误 | 可能加剧系统压力 |
重试风暴警告:当大量任务同时失败并触发重试时,可能对下游系统造成二次冲击。建议在重试策略中加入“熔断”机制:当错误率达到阈值时,暂停重试并快速失败。
以下代码示例展示了 Java 中结合 CompletableFuture 的超时与重试实现:
// Java 示例:带超时和重试的异步任务
public CompletableFuture<Result> executeWithRetry(
Task task, int maxRetries, Duration timeout) {
CompletableFuture<Result> future =
CompletableFuture.supplyAsync(() -> task.execute())
.orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS);
for (int i = 0; i < maxRetries; i++) {
future = future.exceptionally(throwable -> {
if (isRecoverable(throwable)) {
// 指数退避等待
long delay = (long) Math.pow(2, i) * 100;
sleep(delay);
return task.execute();
}
throw new CompletionException(throwable);
}).orTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS);
}
return future;
}
3.4 幂等性设计原则
幂等性是重试策略能够安全实施的前提。如果一个操作被多次执行与单次执行产生相同的结果,则该操作是幂等的。在异步任务编排中,幂等性设计需要覆盖以下层面:
- 接口层幂等:通过幂等令牌(Idempotency Key)保证同一请求不被重复处理。
- 数据层幂等:使用唯一约束、乐观锁或去重表防止重复写入。
- 业务层幂等:业务逻辑本身支持重复执行(如“设置状态为已支付”天然幂等)。
| 实现方式 | 原理 | 适用场景 | 注意事项 |
|---|---|---|---|
| 幂等令牌 | 客户端生成唯一 ID,服务端缓存处理结果 | 支付、订单创建 | 令牌需全局唯一,缓存需设置 TTL |
| 数据库唯一约束 | 利用主键或唯一索引防止重复插入 | 消息去重、日志记录 | 需处理唯一冲突异常 |
| 乐观锁(版本号) | 更新时检查版本号,版本不匹配则失败 | 库存扣减、状态变更 | 高并发下可能频繁重试 |
| 状态机校验 | 只允许特定状态转换,重复请求被忽略 | 工作流引擎、审批流程 | 状态转换图需完整定义 |
幂等性检查清单:
✅ 每个可重试的操作是否都有幂等键?
✅ 幂等键的生成是否依赖客户端输入?
✅ 幂等键的存储是否与业务操作在同一事务中?
✅ 幂等键的过期时间是否大于最大重试窗口?
✅ 每个可重试的操作是否都有幂等键?
✅ 幂等键的生成是否依赖客户端输入?
✅ 幂等键的存储是否与业务操作在同一事务中?
✅ 幂等键的过期时间是否大于最大重试窗口?
幂等性在重试链路中的位置可以用以下 SVG 图表示:
3.5 错误处理策略组合决策
在实际系统中,错误类型、传播机制、超时重试和幂等性需要组合使用。以下决策表可以帮助架构师快速选择策略:
| 错误类型 | 传播方式 | 重试策略 | 幂等要求 | 典型场景 |
|---|---|---|---|---|
| 可恢复 + 瞬时 | 向上传播 | 抖动退避,最多 3 次 | 必须 | 网络超时、数据库死锁 |
| 可恢复 + 持续 | 旁路传播 | 指数退避,最多 5 次 | 必须 | 服务限流、队列积压 |
| 不可恢复 | 向上传播 + 终止 | 不重试 | 不要求 | 参数错误、权限不足 |
| 不可恢复 + 业务补偿 | 旁路传播 | 不重试 | 不要求 | 库存不足、余额不够 |
设计原则总结:
1. 先分类,再决策:根据错误类型选择传播和重试策略。
2. 重试必须有幂等保护:没有幂等性的重试是危险的。
3. 超时是最后的防线:链路级超时防止无限等待。
4. 传播路径要显式定义:不要让错误在异步链路中“迷路”。
1. 先分类,再决策:根据错误类型选择传播和重试策略。
2. 重试必须有幂等保护:没有幂等性的重试是危险的。
3. 超时是最后的防线:链路级超时防止无限等待。
4. 传播路径要显式定义:不要让错误在异步链路中“迷路”。