Saga补偿事务超时与连接池水位故障排查 - 第4节

4.1 Saga 执行状态的存储模型

Saga 状态机需要将运行时状态持久化到外部存储,才能在参与者宕机、网络中断或进程重启后恢复执行。常见的存储后端包括关系数据库(MySQL、PostgreSQL)、NoSQL(Redis、MongoDB)以及分布式 KV 存储(etcd、ZooKeeper)。

存储维度字段示例说明
Saga 实例元数据saga_id, name, version, created_at标识一次 Saga 执行的全生命周期
当前状态current_state, step_index状态机当前所处的节点位置
参与者执行记录step_id, action, status, retry_count每个步骤的补偿/正向操作执行结果
上下文载荷payload (JSON/Blob)跨步骤传递的业务数据
异常堆栈error_code, error_message, stack_trace记录失败原因,用于人工介入
关键设计原则: 持久化记录必须支持幂等写入,避免重复补偿或重复正向提交。建议使用乐观锁(version 字段)或数据库唯一约束来保证同一 Saga 实例的状态转换只生效一次。

4.2 状态机转换图

标准 Saga 状态机包含 6 个核心状态,转换路径由正向执行和补偿回滚两条主线构成。下图展示了完整的转换拓扑:

Saga 状态机转换图 INIT (初始) EXECUTING (执行中) COMPLETED (已完成) COMPENSATING (补偿中) FAILED (失败) ABORTED (已终止) 状态转换规则: • 正向路径:INIT → EXECUTING → COMPLETED • 异常路径:EXECUTING → COMPENSATING → FAILED 或 ABORTED • 补偿完成后进入 FAILED(可重试)或 ABORTED(人工介入)
实践建议: 状态转换必须通过数据库事务保证原子性。推荐使用“状态 + 版本号”的乐观锁更新,避免并发调度导致状态覆盖。例如:UPDATE saga_instance SET current_state='COMPENSATING', version=version+1 WHERE saga_id=? AND version=?

4.3 异常状态处理策略

当 Saga 执行过程中出现超时、业务异常或系统崩溃时,状态机需要进入异常处理分支。以下是三种典型异常场景及其处理方式:

  • 步骤超时: 正向操作或补偿操作超过配置的超时阈值(如 30s),状态机将当前步骤标记为 TIMEOUT,并立即触发补偿流程。超时记录需写入持久化表,用于后续重试或告警。
  • 业务异常: 参与者返回明确的业务错误码(如余额不足、库存锁定失败),状态机直接进入 COMPENSATING 状态,按逆序执行已成功步骤的补偿操作。
  • 系统崩溃: Saga 协调器进程异常退出,重启后通过 saga_id 从持久化存储加载状态。若发现状态为 EXECUTING 且超时未完成,则自动转入 COMPENSATING;若为 COMPENSATING 则继续执行未完成的补偿步骤。
异常类型检测方式状态转换持久化动作
步骤超时调度器定时扫描 + 超时阈值EXECUTING → COMPENSATING写入 timeout_record 表
业务异常参与者返回错误码EXECUTING → COMPENSATING记录 error_code 和 error_message
系统崩溃重启后加载未完成实例EXECUTING/COMPENSATING → 恢复执行读取 saga_instance 和 step_log
避坑提醒: 补偿操作本身也可能超时或失败。必须为补偿步骤设置独立的重试策略和最大重试次数(通常 3 次)。超过最大重试次数后,状态机应进入 ABORTED 状态,并触发人工告警。切勿让补偿无限重试,否则会耗尽连接池资源。

4.4 持久化表结构设计示例

以下是一个基于 MySQL 的 Saga 持久化表设计,覆盖实例元数据、步骤日志和异常记录三个核心维度:

-- Saga 实例主表
CREATE TABLE saga_instance (
    saga_id         VARCHAR(64) PRIMARY KEY,
    name            VARCHAR(128) NOT NULL,
    version         INT NOT NULL DEFAULT 0,
    current_state   VARCHAR(32) NOT NULL,
    payload         JSON,
    created_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Saga 步骤执行日志
CREATE TABLE saga_step_log (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    saga_id         VARCHAR(64) NOT NULL,
    step_index      INT NOT NULL,
    action          VARCHAR(64) NOT NULL,   -- 'forward' 或 'compensate'
    status          VARCHAR(32) NOT NULL,   -- 'PENDING','SUCCESS','FAILED','TIMEOUT'
    retry_count     INT NOT NULL DEFAULT 0,
    error_message   TEXT,
    started_at      DATETIME,
    finished_at     DATETIME,
    INDEX idx_saga_id (saga_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- Saga 异常告警记录
CREATE TABLE saga_alert (
    id              BIGINT AUTO_INCREMENT PRIMARY KEY,
    saga_id         VARCHAR(64) NOT NULL,
    alert_type      VARCHAR(32) NOT NULL,   -- 'TIMEOUT','MAX_RETRY','ABORTED'
    detail          JSON,
    created_at      DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
索引优化: 在 saga_step_log 的 saga_id + step_index 上建立联合唯一索引,防止重复插入相同步骤记录。在 saga_instance 的 current_state 上建立索引,加速故障恢复时的扫描查询。

4.5 故障恢复流程

当 Saga 协调器重启后,需要执行以下恢复步骤:

  1. 加载未完成实例: 查询 current_state IN ('EXECUTING','COMPENSATING') 的 Saga 实例。
  2. 判断超时: 对比 updated_at 与当前时间,若超过超时阈值(如 60s),则标记为超时。
  3. 恢复执行: 对于 EXECUTING 实例,从 step_log 中找出最后一个成功步骤,然后继续正向执行或转入补偿。
  4. 幂等补偿: 对于 COMPENSATING 实例,跳过已成功的补偿步骤,只执行未完成的补偿操作。
  5. 记录恢复日志: 将恢复动作写入 saga_alert 表,便于事后审计。
故障恢复流程 重启加载 超时判定 恢复执行 记录审计 恢复执行分支逻辑: • 若当前状态为 EXECUTING 且未超时 → 继续正向执行 • 若当前状态为 EXECUTING 且已超时 → 转入 COMPENSATING • 若当前状态为 COMPENSATING → 继续执行未完成的补偿步骤 • 所有恢复操作必须保证幂等性,避免重复补偿
生产经验: 故障恢复扫描频率不宜过高,建议每 5-10 秒执行一次批量扫描,避免对数据库造成压力。同时为 saga_instance.updated_at 建立索引,加速超时判定查询。