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 个核心状态,转换路径由正向执行和补偿回滚两条主线构成。下图展示了完整的转换拓扑:
实践建议: 状态转换必须通过数据库事务保证原子性。推荐使用“状态 + 版本号”的乐观锁更新,避免并发调度导致状态覆盖。例如:
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 协调器重启后,需要执行以下恢复步骤:
- 加载未完成实例: 查询
current_state IN ('EXECUTING','COMPENSATING')的 Saga 实例。 - 判断超时: 对比
updated_at与当前时间,若超过超时阈值(如 60s),则标记为超时。 - 恢复执行: 对于
EXECUTING实例,从step_log中找出最后一个成功步骤,然后继续正向执行或转入补偿。 - 幂等补偿: 对于
COMPENSATING实例,跳过已成功的补偿步骤,只执行未完成的补偿操作。 - 记录恢复日志: 将恢复动作写入
saga_alert表,便于事后审计。
生产经验: 故障恢复扫描频率不宜过高,建议每 5-10 秒执行一次批量扫描,避免对数据库造成压力。同时为
saga_instance.updated_at 建立索引,加速超时判定查询。