3. 错误处理基础理论:错误类型分类、传播机制与补偿策略

在异步任务编排中,错误处理不是简单的 try-catch 就能覆盖的。由于任务之间存在依赖、并发、超时和重试,错误的类型、传播路径以及补偿方式必须从系统架构层面进行设计。本章从错误分类出发,逐步建立错误传播模型、超时与重试策略,以及幂等性设计原则,为后续的补偿机制和链路设计打下理论基础。

3.1 错误类型分类:可恢复与不可恢复

错误分类是补偿策略选择的第一步。根据错误是否可以通过重试或替代路径恢复,将错误分为两大类:

错误类型 定义 典型场景 补偿策略
可恢复错误 错误原因具有临时性,重试或等待后可能成功 网络超时、数据库连接池满、服务限流、CPU 负载高 指数退避重试、切换副本、降级调用
不可恢复错误 错误原因具有确定性,重试无法改变结果 参数校验失败、权限不足、数据格式错误、业务逻辑冲突 立即失败、触发补偿事务、人工介入
实践建议:在异步任务编排框架中,建议为每个任务显式声明 recoverable 属性。框架根据该属性决定是否自动触发重试,避免对不可恢复错误进行无意义的重复尝试。

错误分类的决策流程可以用以下 SVG 图表示:

错误类型分类决策流程 错误类型分类决策流程 捕获到异常 是否可恢复? 是 可恢复错误 重试 / 降级 / 切换 否 不可恢复错误 失败 / 补偿 / 人工 分类依据 临时性原因 → 可恢复;确定性原因 → 不可恢复。框架应支持自定义错误分类器。

3.2 错误传播机制

在异步任务链路中,错误不会自动向上冒泡。每个任务节点必须显式定义错误传播行为。常见的传播模式有三种:

  • 向上传播(Propagate Up):子任务失败后,将错误传递给父任务或编排器,由上层决定是否终止链路。
  • 向下传播(Propagate Down):父任务失败后,主动取消所有未执行的子任务,避免资源浪费。
  • 旁路传播(Side-channel):通过事件总线或回调队列将错误信息发送给监控系统或补偿处理器,主链路继续执行。
关键设计点:在 CompletableFuture 或 Go channel 模型中,错误传播需要显式处理 exceptionally() 或 select 分支。不要依赖隐式异常传递,否则会导致任务“静默失败”。

以下 SVG 图展示了三种传播模式在异步链路中的位置:

异步任务错误传播机制 异步任务错误传播机制 父任务 / 编排器 子任务 A 子任务 B 子任务 C 向上传播 向下传播(取消) 事件总线 / 补偿队列 旁路传播 传播策略选择 关键链路使用向上传播 + 终止;非关键链路使用旁路传播 + 异步补偿;父任务失败时使用向下传播取消子任务。

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. 传播路径要显式定义:不要让错误在异步链路中“迷路”。