分布式事务监控与手动恢复平台设计

概述

系列定位说明

本文是"分布式事务工程实践"系列的第七篇。前六篇我们沿着分布式事务的技术演进路径,从 XA/JTA 的强一致性与 In-Doubt 困境出发,深入 Seata AT 的自动补偿与全局锁机制,再到 TCC 的防悬挂与幂等设计,继而覆盖 Saga 的补偿状态机,最后落地于 可靠消息最终一致性 的本地消息表与 CDC 事务发件箱。至此,"如何实现"分布式事务的技术图谱已完整呈现。

然而,在生产环境中,"实现"仅仅是起点。一个分布式事务系统的成熟度,并不取决于它能处理多少正常流量,而取决于它面对异常时能否 快速发现、精准定位、安全恢复。正是这三个能力,构成了分布式事务从"能用"到"敢用"的关键一跃。本文将围绕这一主线,系统构建分布式事务的监控与手动恢复体系------从指标体系设计、可观测性面板构建、告警分级规则,到统一日志表设计、手动恢复平台功能、Micrometer 埋点方案,为每一种前文详述的方案提供生产级运维保障。

总结性引言

回想单数据库时代,事务的故障模式相对单纯:死锁、锁超时、长事务。而在分布式事务的世界,故障模式以组合爆炸的方式增长:一个 TCC 的悬挂可能因网络延迟而潜伏数小时,直到对账才发现数据偏差;一条死信消息可能堆积如山,阻塞整个订单状态流转链路;一个 XA 的 In-Doubt Transaction 死死抱住数据库锁,导致所有相关写入操作挂起,而业务层毫不知情。更令人不安的是,这些故障往往不会立即暴露------它们像暗流一样在系统深处积累,直到用户投诉、业务对账异常、或数据库连接池耗尽时才被察觉。

没有监控,分布式事务就是"盲飞"没有手动恢复平台,运维人员就只能硬着头皮直接改库------这种操作不仅风险极高,也无法追溯审计。监控与恢复平台,是分布式事务的"免疫系统":它持续感知异常信号,按严重程度分级响应,并在必要时提供受控的干预手段。

本文将以前六篇为基础,为每一种分布式事务方案构建专属的监控指标------XA 的 In-Doubt Transaction 数与事务日志大小、AT 的全局锁竞争与 undo log 膨胀、TCC 的悬挂事务数、Saga 的补偿失败次数、消息方案的死信堆积量、CDC 的 Binlog 消费延迟。随后,基于 Prometheus + Grafana 打造三层可观测性面板(L1 全局概览 → L2 方案细分 → L3 单事务追踪),设计 P0/P1/P2 三级告警规则,并构建 Admin 后台提供事务查询、手动重试、死信跳过、全局锁释放等恢复操作,所有操作记录到审计日志。当 Grafana 的 P99 延迟曲线突然飙升,当 PagerDuty 的电话因 In-Doubt >0 而响起,当运维人员在 Admin 后台镇定地释放残留的全局锁------这背后,是一套完整的生产运维保障体系在发挥作用。

核心要点

  • 统一监控指标体系:涵盖 7 大类指标------全局事务成功率、事务耗时与 P99 延迟、悬挂事务数量、死信堆积量、补偿失败次数、In-Doubt Transaction 数量、全局锁等待超时次数、CDC 消费延迟。每类指标均给出具体的 Prometheus 数据类型(Counter / Timer / Gauge)和采集方法。
  • Grafana 三层面板 :L1 全局概览面板提供分布式事务整体健康度的"仪表盘",L2 方案细分面板为六种方案各自建立独立监控视图,L3 单事务追踪面板通过 xid 下钻到全局事务完整时间线与各分支辅助表详情。
  • 告警分级规则:P0 级(立即响应,5 分钟)应对业务中断类故障,P1 级(紧急处理,15 分钟)应对故障积累类异常,P2 级(关注,1 小时)应对性能劣化类预警。配合告警抑制与聚合策略,避免告警风暴。
  • 手动恢复平台 :提供事务多条件查询、全局时间线详情展示、五种核心手动操作(重试 Confirm/Cancel、回滚全局事务、跳过死信、释放全局锁、批量处理悬挂),所有变更操作写入审计日志表 recovery_audit_log
  • transaction_log 统一日志表 :以 xid 为主键,记录全局事务的完整生命周期(状态变迁、时间戳、错误信息),并通过 branches JSON 字段和 payload JSON 字段存储分支事务详情与业务上下文。与各方案辅助表形成星型关联。
  • Micrometer 埋点方案 :在 TM 侧通过 @GlobalTransactional AOP 拦截器记录事务耗时与结果;在 RM 侧记录 Phase 1/Phase 2 耗时;在 TC 侧暴露活跃事务数、In-Doubt 数等 Gauge 指标;在消息消费者侧记录消费耗时与失败计数。

文章组织架构图

flowchart TD subgraph 感知层 1["1. 统一监控指标体系
全局指标 + XA/AT/TCC/Saga/消息/CDC 专属指标"] end subgraph 可视化层 2["2. Grafana 三层面板
L1 全局概览 / L2 方案细分 / L3 单事务追踪"] end subgraph 响应层 3["3. 告警分级规则
P0 / P1 / P2 阈值、通知渠道、抑制策略"] end subgraph 数据层 4["4. transaction_log 统一日志表
DDL / 写入时机 / 关联查询 / 清理策略"] end subgraph 恢复层 5["5. 手动恢复平台
查询 / 详情 / 五种操作 / 审计日志"] end subgraph 实现层 6["6. Micrometer 埋点方案
TM / RM / TC 代码示例"] end subgraph 巩固层 7["7. 面试高频专题
12 道题含系统设计题"] end 1 --> 2 2 --> 3 3 --> 4 4 --> 5 5 --> 6 6 --> 7

架构图说明

  • 总览说明:全文 7 个模块构成一个完整的运维保障闭环------从指标采集(感知层)到面板展示(可视化层),再到告警触发(响应层),以统一日志表(数据层)为数据基础,以手动恢复平台(恢复层)为干预手段,以 Micrometer 埋点(实现层)为代码落地,最终以面试题(巩固层)收尾强化理解。
  • 逐模块说明 :模块 1 定义了 7 类监控指标,其中通用指标 3 类(成功率/耗时/活跃事务数),方案专属指标覆盖六种方案的各自痛点;模块 2 将指标组织为三个递进层级的 Grafana 面板;模块 3 设计三级告警的触发条件、通知方式与抑制策略;模块 4 设计 transaction_log 统一日志表,作为 L3 追踪和手动恢复的数据枢纽;模块 5 基于日志表构建 Admin 后台的查询、详情与手动恢复功能;模块 6 给出 TM/RM/TC 各节点的 Micrometer 埋点代码;模块 7 以 12 道面试题深化理解。
  • 关键结论 :分布式事务的生产运维保障是一个系统工程,需要从感知(指标)→可视化(面板)→告警(分级通知)→数据(统一日志)→恢复(手动平台)五个维度协同构建。transaction_log 统一日志表是整个体系的数据枢纽------它向上支撑 Grafana L3 单事务追踪,向下为手动恢复提供决策依据,向后为故障复盘保留完整记录。

1. 统一监控指标体系:覆盖 XA/AT/TCC/Saga/消息/CDC 的专属指标

分布式事务监控指标的设计不能追求"大而全",而应精准锚定各方案最脆弱的环节。本章将指标划分为通用指标 (所有方案均需关注)和方案专属指标(针对特定方案的痛点),并给出每个指标的 Prometheus 数据类型、采集方法与告警意义。

1.1 通用指标:全局事务健康度基线

通用指标反映分布式事务整体的运行状态,无论底层采用何种实现方案,这三个指标都是必须监控的。

1.1.1 全局事务成功率

定义:在统计窗口内,全局事务成功提交的比例。

Prometheus 数据类型:Counter

计算公式

ini 复制代码
transaction_success_rate = 
  sum(rate(transaction_committed_total[5m])) / 
  (sum(rate(transaction_committed_total[5m])) + sum(rate(transaction_rollbacked_total[5m])))

细分要求 :必须区分 COMMIT_FAILURE(提交失败,系统已自动回滚)和 ROLLBACK_FAILURE(回滚失败,需要人工介入)。前者反映业务异常(如库存不足),后者反映系统故障(如网络中断导致回滚未完成)。两者的告警阈值完全不同------ROLLBACK_FAILURE 即使只有 1 次也需要排查,而 COMMIT_FAILURE 在合理比例内是可接受的。

指标设计

  • transaction_committed_total{type="AT|XA|TCC|SAGA|MQ"}:成功提交的全局事务数
  • transaction_rollbacked_total{type="AT|XA|TCC|SAGA|MQ", cause="business|system"}:已回滚的事务数,按业务异常或系统异常区分
  • transaction_rollback_failed_total{type="AT|XA|TCC|SAGA|MQ"}:回滚失败的事务数(绝对异常)

告警意义 :当 rollback_failed 计数器增长或成功率跌破阈值时,说明系统的自动恢复机制已失效,需要人工介入。

1.1.2 事务耗时与 P99 延迟

定义 :全局事务从 BEGIN 到最终状态(COMMITTEDROLLBACKED)的端到端耗时分布。

Prometheus 数据类型 :Histogram(transaction_duration_seconds

技术基线与告警阈值

方案 典型 P99 告警阈值 原因分析
XA 100-500ms >1s 多次网络往返 + 磁盘刷盘(Write-Ahead Log),跨数据库实例时网络延迟叠加明显
AT 20-50ms >100ms Phase 1 执行本地事务 + 保存 undo log,Phase 2 异步删除 undo log,整体轻量
TCC 5-20ms >50ms 本地事务 + 轻量 RPC 调用,无额外持久化开销
Saga 可变 按业务基线 取决于状态机步数,通常每步 10-100ms
消息 1-5s >10s 消息投递有定时扫描延迟(1-3s),CDC 方案可降到 <100ms

指标设计

ini 复制代码
histogram_quantile(0.99, rate(transaction_duration_seconds_bucket{type="AT"}[5m]))

告警意义 :P99 突然飙升通常是数据库连接池耗尽、网络抖动或热点数据锁竞争的早期信号。不同方案的基线差异很大,切不可用同一阈值衡量所有方案

1.1.3 活跃事务数

定义 :当前时刻处于中间状态(BEGINCOMMITTINGROLLBACKING)尚未完结的全局事务数量。

Prometheus 数据类型 :Gauge(active_transactions_gauge

告警意义:活跃事务数异常升高通常意味着有事务"卡住"------可能是 TC 宕机、网络分区、或某个分支事务长时间未响应。正常情况下,活跃事务数应保持在一个稳定区间(取决于业务 TPS 和平均事务耗时)。

估算公式 (利特尔法则):平均活跃事务数 ≈ TPS × 平均事务耗时。例如 TPS=100,平均耗时 50ms,则活跃事务数约 5。

1.2 XA 专属指标

XA 协议的核心风险在于 2PC 过程中 Coordinator(TM)崩溃导致的 In-Doubt Transaction,以及事务日志体积膨胀对恢复性能的影响。

1.2.1 In-Doubt Transaction 数量

定义 :TM 发出 Prepare 指令后崩溃,导致参与者既未收到 Commit 也未收到 Rollback 的事务。这些事务处于"悬而未决"状态,持有数据库行级锁或表级意向锁,会阻塞所有相关写操作。

Prometheus 数据类型 :Gauge(xa_indoubt_transactions_total

采集方法

方式一:直连数据库查询 Atomikos 的 tm_log 表:

sql 复制代码
SELECT COUNT(*) FROM tm_log 
WHERE status = 1  -- 1 表示 PREPARED 状态
  AND commit_time IS NULL;

方式二:通过 JMX 暴露。Atomikos 提供了 UserTransactionService 的 MBean,可调用 getDefaultTimeout() 等方法。也可以通过 Seata XA 模式的 TC 端监控------Seata Server 在检测到 xa_commitxa_rollback 请求超时时,会将这些事务标记为 In-Doubt。

方式三:通过 Micrometer 自定义 Gauge,在应用层定期查询 tm_log 表:

java 复制代码
@Bean
public MeterBinder xaInDoubtMetrics(JdbcTemplate jdbcTemplate) {
    return registry -> Gauge.builder("xa.indoubt.transactions", () -> {
        Integer count = jdbcTemplate.queryForObject(
            "SELECT COUNT(*) FROM tm_log WHERE status = 1", Integer.class);
        return count != null ? count : 0;
    }).register(registry);
}

告警意义In-Doubt > 0 即为 P0 级告警。原因:

  1. In-Doubt 事务持有数据库锁,会导致所有涉及相同数据行的写操作被阻塞;
  2. 长时间不处理可能导致锁等待超时,引发连锁故障;
  3. 恢复时需要人工判断是提交还是回滚,决策错误可能造成数据不一致。

关联前文(第 1 篇 XA):第 1 篇详细剖析了 XA 的 2PC 流程,In-Doubt 产生于 TM 在 Prepare 完成后、发送 Commit 前的任意时刻崩溃。TM 重启后会读取 tm_log 进行恢复------尝试 commit() 所有 Prepared 事务。但如果参与者节点也发生过重启,或网络恢复后数据库已重置连接,恢复可能失败,事务便残留为 In-Doubt。

1.2.2 tmlog 文件或表大小

定义 :事务日志的物理存储量。对于 Atomikos 文件模式是 .log 文件大小,对于数据库模式是 tm_log 表的空间占用。

Prometheus 数据类型 :Gauge(xa_tmlog_size_bytes

告警意义tm_log 膨胀可能由大量长事务或未及时清理的历史记录引起。当日志过大时,TM 重启扫描恢复的时间会显著增加(可能需要几分钟甚至更长),延长故障恢复窗口。

1.3 AT 专属指标

Seata AT 模式的核心机制是自动回滚 (依赖 undo_log 表的数据快照)和全局锁lock_table 防止脏写)。因此监控重心在于这两个辅助表。

1.3.1 全局锁等待超时次数

定义 :RM 在尝试获取某个数据行的全局锁时,等待时间超过 lockRetryTimeout(默认 10s)而抛出 LockConflictException 的次数。

Prometheus 数据类型 :Counter(seata_lock_timeout_total

采集方法 :在应用层捕获 LockConflictException 并增加 Counter:

java 复制代码
@ExceptionHandler(LockConflictException.class)
public ResponseEntity<?> handleLockConflict(LockConflictException e) {
    meterRegistry.counter("seata.lock.timeout",
        "resource", e.getResourceId(),
        "xid", e.getXid()
    ).increment();
    return ResponseEntity.status(409).body("全局锁等待超时,请重试");
}

告警意义 :全局锁超时次数激增通常意味着热点数据竞争激烈------例如秒杀场景下多个事务同时操作同一库存记录。这不仅导致当前事务回滚,还会消耗 RM 的线程资源和 TC 的锁管理开销。当 30 分钟内超时次数 >20 时触发 P2 告警。

关联前文(第 2 篇 AT):第 2 篇详细解释了全局锁的工作机制------Phase 1 本地事务提交前,RM 会向 TC 注册分支并申请全局锁;Phase 2 提交完成后释放锁。锁竞争发生在 Phase 1 注册阶段,而非本地事务执行期间。

1.3.2 undo_log 表行数与磁盘占用

定义undo_log 表中未删除的记录数(即全局事务尚未完成 Phase 2 的分支回滚日志)及其磁盘空间占用。

Prometheus 数据类型 :Gauge(seata_undo_log_rowsseata_undo_log_size_bytes

采集方法

java 复制代码
@Bean
public MeterBinder undoLogMetrics(JdbcTemplate jdbcTemplate) {
    return registry -> {
        Gauge.builder("seata.undo.log.rows", () -> 
            jdbcTemplate.queryForObject("SELECT COUNT(*) FROM undo_log", Long.class)
        ).register(registry);
        Gauge.builder("seata.undo.log.size", () -> 
            jdbcTemplate.queryForObject(
                "SELECT COALESCE(SUM(DATA_LENGTH + INDEX_LENGTH), 0) FROM information_schema.tables WHERE table_name='undo_log'", 
                Long.class)
        ).register(registry);
    };
}

告警意义 :Phase 2 删除 undo_log异步执行 的。如果 TC 通知 RM 删除 undo log 的过程出现延迟(如网络抖动、RM 繁忙),undo_log 会堆积。极端情况下可能耗尽磁盘空间。正常的 undo_log 行数应与活跃事务数 × 平均分支数成正比。

1.3.3 Phase 1 与 Phase 2 耗时分布

定义

  • Phase 1 耗时:从 RM 开始执行本地事务(包括业务 SQL 和插入 undo_log)到向 TC 注册分支成功的时间。
  • Phase 2 耗时:从 TC 发起全局提交/回滚通知到 RM 完成 undo_log 删除的时间。

Prometheus 数据类型 :Timer(seata_at_phase1_duration_secondsseata_at_phase2_duration_seconds

告警意义:Phase 1 耗时突增通常是数据库性能问题(如慢 SQL、锁等待);Phase 2 耗时突增通常是 TC 与 RM 之间的网络问题或 RM 端的删除操作阻塞。

1.4 TCC 专属指标

TCC 的核心风险是悬挂事务 (Cancel 比 Try 先到达,或 Cancel 到达时 Try 已超时回滚)和空回滚 (Cancel 到达时 Try 从未被执行)。这些异常模式依赖于 tcc_fence_log 防悬挂表来检测和记录。

1.4.1 悬挂事务数量

定义tcc_fence_logstatus = 'CANCELLED' 但无对应 Try 记录,或 Try 记录 status = 'SUSPENDED' 的异常记录数。

Prometheus 数据类型 :Gauge(tcc_hanging_transactions_total

采集SQL

sql 复制代码
-- 查询悬挂事务:Cancel 已执行但 Try 从未到达
SELECT COUNT(*) FROM tcc_fence_log 
WHERE status = 'CANCELLED' 
  AND xid NOT IN (
    SELECT xid FROM tcc_fence_log WHERE status = 'TRYING'
  );

-- 或者查询被标记为 SUSPENDED 的记录
SELECT COUNT(*) FROM tcc_fence_log WHERE status = 'SUSPENDED';

告警意义 :悬挂数量 >50 触发 P1 告警。悬挂事务意味着业务侧的 Cancel 操作在空跑------可能扣减了已扣减的额度、解冻了未冻结的资金。虽然空回滚在 TCC 设计上是可以接受的(Cancel 需幂等,空回滚返回成功),但大量悬挂说明系统存在严重的时序或超时配置问题,需要排查根因。

关联前文(第 3 篇 TCC):第 3 篇详细设计了 tcc_fence_log 表,并解释了防悬挂机制------Try 执行前会检查是否存在 Cancel 记录(悬挂检测),Cancel 执行前会检查 Try 是否执行过(空回滚允许)。悬挂发生时,Cancel 将 tcc_fence_log 中该 xid 的记录状态设为 SUSPENDED

1.4.2 各阶段耗时分布

定义:Try、Confirm、Cancel 三个阶段的平均耗时与 P99 延迟。

Prometheus 数据类型 :Timer(tcc_try_duration_secondstcc_confirm_duration_secondstcc_cancel_duration_seconds

采集方法 :在 TCC 的 Try/Confirm/Cancel 方法上使用 AOP 或手动 Timer.Sample 埋点。

告警意义

  • Try 阶段耗时突增:资源预占逻辑可能存在问题(如库存扣减慢 SQL);
  • Confirm 阶段耗时突增:可能业务处理逻辑过重,建议将复杂逻辑移到 Try 阶段,Confirm 只做状态确认;
  • Cancel 阶段耗时突增:回滚逻辑可能有性能瓶颈,需优化。

1.5 Saga 专属指标

Saga 模式通过状态机驱动正向事务链,失败时执行补偿链。其核心风险是补偿失败------即正向事务已部分提交,但补偿操作无法执行或执行失败。

1.5.1 补偿失败次数与重试耗尽次数

定义

  • saga_compensation_failed_totalsaga_transaction_logstatus = 'COMPENSATION_FAILED' 的记录数;
  • saga_retry_exhausted_total:补偿重试次数达到上限(retry_count >= maxRetry)且最终状态为 FAILED 的事务数。

Prometheus 数据类型 :Counter(saga_compensation_failed_totalsaga_retry_exhausted_total

告警意义:补偿失败 10 分钟内 >10 次触发 P1 告警。补偿重试耗尽意味着 Saga 状态机已无法自动恢复,需要人工决策是向前重试补偿还是向前恢复。

关联前文(第 4 篇 Saga):第 4 篇实现了 Saga 状态机,每一步事务执行结果记录到 saga_transaction_log,补偿重试由状态机引擎自动触发。当重试次数达到上限后,事务标记为 FAILED 并停止自动处理。

1.5.2 状态机执行耗时

定义:从 Saga 状态机启动到执行完毕(成功或失败)的端到端耗时。

Prometheus 数据类型 :Timer(saga_state_machine_duration_seconds

告警意义:Saga 的事务链可能包含多个服务调用,总耗时随步数线性增长。如果单步耗时正常但总耗时过长,可能是步数过多需要合并;如果单步耗时突增,则需要排查对应服务。

1.6 消息最终一致性专属指标

消息方案(本地消息表 / 事务发件箱)的核心链路是:业务事务写入 outbox 表 → 定时任务或 CDC 读取并发送到 MQ → 消费者消费。其风险点在于消息堆积死信

1.6.1 死信堆积量(应用层 + MQ 层)

定义

  • 应用层死信outbox 表中 status = 'DEAD' 的记录数。这些消息已经过最大重试次数,定时任务不再处理。
  • MQ 层死信 :RocketMQ %DLQ% 主题的消息堆积数,或 Kafka 死信 Topic 的消息 Lag。

Prometheus 数据类型 :Gauge(dead_message_total),由两个来源相加:

java 复制代码
Gauge.builder("dead.message.total", () -> {
    int outboxDead = jdbcTemplate.queryForObject(
        "SELECT COUNT(*) FROM outbox WHERE status = 'DEAD'", Integer.class);
    int mqDead = rocketMQAdmin.queryDlqDepth(); // 通过 RocketMQ Admin API 获取
    return outboxDead + mqDead;
}).register(registry);

告警意义:死信堆积 >100 触发 P1 告警。每一条死信都代表一个业务事件未成功投递------可能是订单状态未更新、积分未发放、通知未推送。死信堆积会直接转化为业务延迟或数据不一致。

关联前文(第 5 篇消息):第 5 篇设计了 outbox 表的状态流转:PENDING → SENDING → SENT,失败重试达到上限后变为 DEAD。定时扫描任务跳过 DEAD 状态的消息。

1.6.2 outbox 表 PENDING 堆积

定义outbox 表中 status = 'PENDING' 的消息数量。

Prometheus 数据类型 :Gauge(outbox_pending_total

告警意义:正常情况下 PENDING 消息会被定时任务快速处理(CDC 方案下延迟 <100ms)。如果 PENDING > 1000 且持续增长,说明定时扫描任务可能故障、MQ 生产端阻塞、或 CDC Connector 停止工作。

1.6.3 消费 Lag

定义:MQ 消费者落后于生产者的消息数量。

Prometheus 数据类型 :Gauge(mq_consumer_lag

  • RocketMQ:(brokerOffset - consumerOffset) 或通过 ConsumerRunningInfo 获取 Diff 指标
  • Kafka:kafka_consumer_fetch_manager_records_lag_max

告警意义:消费 Lag 持续增长说明消费者处理能力不足或消费者宕机,最终可能导致消息积压触发 MQ 存储告警。

1.7 CDC 发件箱专属指标

CDC 发件箱依赖 Debezium 监听 Binlog 并实时投递到 Kafka。其核心风险是消费延迟偏移量丢失

1.7.1 MilliSecondsBehindSource

定义:Debezium 当前处理 Binlog 的位置与源库最新 Binlog 位置的时间差(毫秒)。该指标通过 Debezium 的 JMX Bean 暴露。

Prometheus 数据类型 :Gauge(debezium_milli_seconds_behind_source

采集方法

yaml 复制代码
# prometheus.yml 配置 JMX Exporter 采集 Debezium 指标
- job_name: 'debezium'
  static_configs:
    - targets: ['debezium-connector:8083']
  metrics_path: '/metrics'

或使用 Debezium 自带的 Prometheus JMX Exporter:

ini 复制代码
debezium_metrics_MilliSecondsBehindSource{context="streaming"}

告警意义:延迟 >5s 触发 P2 告警。延迟升高的常见原因:源库有大量写入或 DDL 操作、Kafka 集群负载过高、Debezium 任务触发错误重试、网络带宽不足。

关联前文(第 6 篇 CDC):第 6 篇详细描述了 Debezium 的部署配置和 Outbox Event Router 的路由逻辑。CDC 延迟直接决定了消息投递的端到端延迟------延迟 <100ms 是正常态,>5s 说明链路出现阻塞。

1.7.2 偏移量提交失败次数

定义 :Debezium 将 Binlog 偏移量提交到 Kafka 内部 Topic(connect-offsets)时失败的次数。

Prometheus 数据类型 :Counter(debezium_offset_commit_failed_total

告警意义:偏移量提交失败可能导致 Connector 重启后从旧位置重复消费,造成消息重复投递。

1.8 分布式事务监控指标体系全景图

flowchart TB subgraph 通用指标 direction LR G1["全局事务成功率
Counter: transaction_committed_total
+ transaction_rollbacked_total
区分 COMMIT_FAILURE / ROLLBACK_FAILURE"] G2["事务耗时与P99延迟
Histogram: transaction_duration_seconds
按 type 标签分组
各方案基线不同"] G3["活跃事务数
Gauge: active_transactions_gauge
≈ TPS × 平均耗时
异常升高表示事务卡住"] end subgraph XA专属 X1["In-Doubt Transaction数
Gauge: xa_indoubt_transactions_total
来源: tm_log 表状态查询
P0告警: >0立即响应"] X2["tmlog大小
Gauge: xa_tmlog_size_bytes
膨胀影响TM重启恢复时间"] end subgraph AT专属 A1["全局锁等待超时
Counter: seata_lock_timeout_total
异常: LockConflictException
P2告警: 30分钟>20次"] A2["undo_log表行数
Gauge: seata_undo_log_rows
Phase 2 异步删除
堆积可能耗尽磁盘"] A3["Phase1/Phase2耗时
Timer: seata_at_phase1_duration
+ phase2_duration
定位慢SQL或网络延迟"] end subgraph TCC专属 T1["悬挂事务数量
Gauge: tcc_hanging_transactions_total
来源: tcc_fence_log异常记录
P1告警: >50"] T2["Try/Confirm/Cancel耗时
Timer: tcc_try_duration
+ tcc_confirm_duration
+ tcc_cancel_duration"] end subgraph Saga专属 S1["补偿失败次数
Counter: saga_compensation_failed_total
P1告警: 10分钟>10次"] S2["重试耗尽次数
Counter: saga_retry_exhausted_total
标记FAILED,需人工介入"] S3["状态机执行耗时
Timer: saga_state_machine_duration"] end subgraph 消息专属 M1["死信堆积量
Gauge: dead_message_total
= outbox DEAD + MQ DLQ
P1告警: >100"] M2["outbox PENDING堆积
Gauge: outbox_pending_total
P2告警: >1000
可能定时任务故障"] M3["消费Lag
Gauge: mq_consumer_lag
消费者处理能力不足的信号"] end subgraph CDC专属 C1["Binlog消费延迟
Gauge: debezier_milli_seconds_behind_source
P2告警: >5s
定位源库写入/Kafka/网络"] C2["偏移量提交失败
Counter: debezium_offset_commit_failed_total
可能导致重复消费"] end G1 --- G2 --- G3 X1 --- X2 A1 --- A2 --- A3 T1 --- T2 S1 --- S2 --- S3 M1 --- M2 --- M3 C1 --- C2

图 1-1 分布式事务监控指标体系全景图

  • 整体说明:该图将监控指标划分为一个通用指标组和六个方案专属指标组,每个指标都标注了 Prometheus 数据类型、告警级别和核心采集来源,形成完整的指标体系地图。
  • 逐组件说明
    • 通用指标组:事务成功率、耗时、活跃事务数是所有方案都必须关注的基础指标,其中成功率需区分业务回滚和系统回滚失败;
    • XA 专属:聚焦 2PC 的致命缺陷------In-Doubt Transaction 持有锁不释放,这是 P0 级告警的核心触发条件;
    • AT 专属:聚焦自动补偿机制的两个依赖------全局锁保证隔离性、undo_log 保证可回滚,两者出问题直接威胁数据一致性;
    • TCC 专属:聚焦防悬挂机制------悬挂事务数量是 TCC 幂等设计失效的直接证据;
    • Saga 专属:聚焦补偿失败------这是 Saga 长事务链中最脆弱的环节,重试耗尽后必须人工介入;
    • 消息专属:聚焦死信与堆积------消息方案是异步的,死信意味着"最终一致性"永远无法达到;
    • CDC 专属:聚焦 Binlog 消费延迟------这是 CDC 发件箱相比定时扫描的核心优势(<100ms),一旦延迟升高则退化为定时扫描方案。
  • 关联说明 :每个方案专属指标都直接溯源到前六篇的实现细节------In-Doubt 映射第 1 篇的 tm_log 恢复流程;全局锁映射第 2 篇的 lock_table 竞争机制;悬挂事务映射第 3 篇的 tcc_fence_log 防悬挂表;补偿失败映射第 4 篇的 saga_transaction_log;DEAD 状态映射第 5 篇的 outbox 状态流转;CDC 延迟映射第 6 篇的 Debezium 部署。
  • 关键结论:这套指标体系是后续 Grafana 面板、告警规则和手动恢复操作的数据基础,每个指标都有明确的采集方式、告警阈值和处理指引。

2. Prometheus + Grafana 三层监控面板设计(L1/L2/L3)

监控指标通过 Micrometer 暴露为 Prometheus 端点后,需要通过 Grafana 面板进行可视化呈现。本节设计三层递进式面板架构,遵循"宏观感知 → 组件定位 → 事务下钻"的故障排查路径。

2.1 L1:全局概览面板

目标用户 :值班 SRE、技术 Lead、团队全员
核心问题:分布式事务系统整体是否健康?

面板布局(一行三个图表,共两行):

第一行:核心健康度

图表 类型 PromQL 示例 说明
全局事务成功率 Stat + 时序折线 sum(rate(transaction_committed_total[1m])) / (sum(rate(transaction_committed_total[1m])) + sum(rate(transaction_rollbacked_total[1m]))) * 100 绿色大数字显示当前成功率,下方折线图展示 6 小时趋势。低于 99.9% 时数字变红
活跃事务数 时序折线 active_transactions_gauge 展示最近 1 小时的活跃事务数变化。叠加一条虚线表示理论上限(TPS × P99)
P99 延迟(多线) 时序折线 `histogram_quantile(0.99, rate(transaction_duration_seconds_bucket{type=~"AT XA

第二行:异常定位

图表 类型 PromQL 示例 说明
回滚失败次数 时序折线 rate(transaction_rollback_failed_total[5m]) 监控系统自动恢复机制的失效情况,只要有值就需要关注
失败事务 Top 10 服务 Bar Gauge topk(10, sum(rate(transaction_rollbacked_total[5m])) by (service)) 按服务名聚合,快速定位故障集中的微服务
In-Doubt / 死信 / 悬挂 总数 Stat(三列) xa_indoubt_transactions_totaldead_message_totaltcc_hanging_transactions_total 三个关键异常指标的大数字显示,任何一个 >0 都需警觉

L1 面板的核心设计理念:一眼定全局。值班人员扫一眼 L1 面板就能判断"现在有没有问题"。成功率数字、P99 曲线趋势、三个异常计数是最关键的信息。

2.2 L2:方案细分面板

目标用户 :分布式事务组件负责人、开发工程师
核心问题:具体是哪种分布式事务方案出了问题?

L2 面板为六种方案各创建一个独立 Row(可折叠),每个 Row 包含该方案的专属指标图表。

XA Row

  • In-Doubt 数量(Stat + 时序):实时数字 + 24 小时趋势
  • tmlog 大小(时序):观察日志增长速度
  • 2PC 各阶段耗时(Heatmap):X 轴时间,Y 轴延迟区间,颜色深浅表示数量

AT Row

  • 全局锁超时次数(时序 + 累计值)
  • undo_log 行数(时序):正常应保持在低位
  • Phase 1 vs Phase 2 耗时对比(双 Y 轴折线):观察是否 Phase 1 耗时主导还是 Phase 2 耗时主导

TCC Row

  • 悬挂事务数(时序 + 累计值)
  • 防悬挂表总行数(时序):评估清理策略是否有效
  • Try / Confirm / Cancel 耗时 P50/P99(多线图)

Saga Row

  • 补偿失败次数(时序 + 累计值)
  • 状态机执行耗时分布(Histogram Heatmap)
  • saga_transaction_log 表行数(时序)

消息 Row

  • DEAD 数量(Stat):outbox DEAD + MQ DLQ
  • PENDING 堆积趋势(时序)
  • 消费 Lag(时序):与消费者实例数叠加展示

CDC Row

  • MilliSecondsBehindSource(时序):标注 5s 告警线
  • Kafka 消费 Lag(时序):发件箱消息的端到端延迟
  • Connector 任务状态(Status Panel):RUNNING / FAILED / PAUSED

2.3 L3:单事务追踪面板

目标用户 :排查具体故障的开发工程师
核心问题xid = xxx 的这个事务究竟经历了什么?

这是三层架构中最能体现"可观测性深度"的一层。单事务追踪依赖 transaction_log 统一日志表(详见第 4 节)和辅助表数据。

面板设计

顶部:事务检索区

  • 输入框:xidbusinessId
  • 可选过滤器:transactionTypestatus、时间范围
  • Grafana 通过 Variable 实现动态查询:SELECT xid FROM transaction_log WHERE $__timeFilter(start_time) ORDER BY start_time DESC LIMIT 100

中部:全局事务时间线(甘特图或瀑布图)

使用 Grafana 的 State TimelineBar Chart 面板,以时间为横轴,展示事务从 BEGIN 到最终状态的每个状态变化:

scss 复制代码
BEGIN(10:00:00.000) 
  → Branch1-TRYING(10:00:00.015, 耗时45ms) → CONFIRMED(10:00:00.060)
  → Branch2-TRYING(10:00:00.020, 耗时120ms) → ROLLBACKED(10:00:00.140)
→ ROLLBACKED(10:00:00.150)

数据来源:transaction_log.branches JSON 字段解析。

底部:辅助表数据联动

通过 MySQL 数据源执行关联查询(SQL 见第 4 节),展示:

  • ATundo_logrollback_info(回滚镜像数据)、lock_table 的锁持有情况
  • TCCtcc_fence_log 的完整记录(Try / Confirm / Cancel 的状态和时间)
  • Sagasaga_transaction_log 的每一步状态机执行记录
  • 消息outbox 的消息内容、重试次数、当前状态
  • 错误堆栈transaction_log.error_message 字段内容

Data Link 跳转 :配置 Grafana 的 Data Link,点击 xid 可以跳转到 ELK 或 Loki 中搜索该 xid 的所有日志。

2.4 Grafana 三层监控面板架构图

flowchart LR subgraph L1["L1 全局概览面板 (Global Overview)"] direction LR L1A["事务成功率
Stat + 时序折线
6小时趋势"] L1B["活跃事务数
时序折线
标注理论上限"] L1C["P99延迟(多线)
按type分组
对数Y轴"] L1D["回滚失败次数
时序折线"] L1E["失败Top10服务
Bar Gauge"] L1F["关键异常计数
In-Doubt/死信/悬挂"] end subgraph L2["L2 方案细分面板 (Per-Solution Detail)"] direction LR L2XA["XA Row
In-Doubt数量
tmlog大小
2PC耗时分布"] L2AT["AT Row
锁超时次数
undo_log行数
Phase1/2耗时对比"] L2TCC["TCC Row
悬挂事务数
防悬挂表行数
Try/Confirm/Cancel耗时"] L2SAGA["Saga Row
补偿失败次数
状态机耗时分布
日志表行数"] L2MSG["消息 Row
DEAD堆积
PENDING趋势
消费Lag"] L2CDC["CDC Row
Binlog延迟
Kafka Lag
Connector状态"] end subgraph L3["L3 单事务追踪面板 (Per-Transaction Trace)"] direction LR L3A["事务检索区
xid/businessId输入
类型/状态过滤器"] L3B["全局事务时间线
甘特图/瀑布图
BEGIN→Branch1→Branch2→ROLLBACKED"] L3C["辅助表数据
undo_log / outbox
tcc_fence_log / saga_log
错误堆栈"] L3D["跳转链接
ELK/Loki日志
xid关联查询"] end L1 --> L2 L2 --> L3 L1 -.->|"下钻: 点击异常指标"| L2 L2 -.->|"下钻: 点击具体事务"| L3

图 2-1 Grafana 三层监控面板架构图

  • 整体说明:三层面板遵循"总-分-详"的信息递进原则------L1 提供全局健康度的"驾驶舱视图",L2 提供各方案组件的"仪表盘阵列",L3 提供单事务实例的"X 光透视"。三层之间通过 Grafana 的 Data Link 和 Dashboard Variable 实现联动下钻。
  • 逐层说明
    • L1 全局概览:以 6 小时为默认时间窗口,展示系统级聚合指标。设计要点:关键数字要大(Stat 面板)、趋势要连续(时序折线)、异常要醒目(阈值线 + 颜色变化)。
    • L2 方案细分:每种方案独立 Row,可折叠以减少信息过载。每个 Row 包含该方案最核心的 3-4 个指标,布局采用"计数 + 趋势 + 分布"的组合(如:Stat + 时序 + Histogram)。
    • L3 单事务追踪 :以 xid 为查询主键,从 transaction_log 表拉取全局事务的时间线和分支详情,并联动查询辅助表。这是 Grafana 直接通过 MySQL Data Source 执行 SQL 的地方。
  • 关联说明 :L1 到 L2 的下钻通过 Grafana 的 Panel Link 实现------例如点击成功率下降的时间点,跳转到 L2 面板并自动过滤对应时间范围。L2 到 L3 的下钻通过 Data Link 实现------例如点击某个悬挂事务计数,跳转到 L3 面板并传入 xid 变量。transaction_log 表(第 4 节)是 L3 面板的数据核心。
  • 关键结论 :三层架构的本质是将故障定位时间从"小时级"压缩到"分钟级"。L1 让故障"可感知",L2 让故障"可定位",L3 让故障"可解释"。没有这套分层设计,运维人员面对 Prometheus 的海量指标将无所适从。

3. 告警分级规则:P0/P1/P2 的阈值、通知渠道与抑制策略

告警是连接"监控发现"与"人工响应"的关键环节。告警设计不当会产生两个极端:告警太多导致"狼来了"效应(alert fatigue),或告警太少导致故障漏报。本章设计三级告警体系,每一级都明确定义触发条件、通知渠道、响应时间和处理指引。

3.1 告警分级原则

级别 定义 响应时间 典型场景 通知渠道
P0 影响核心业务可用性或存在数据永久不一致风险 5 分钟 In-Doubt Transaction 持有锁、成功率跌破阈值 电话 + 企业微信 + PagerDuty
P1 存在明显故障积累,但暂未导致完全不可用 15 分钟 死信堆积、悬挂事务、补偿失败 企业微信 + 邮件
P2 指标偏离基线但尚未产生直接业务影响 1 小时 P99 延迟升高、锁超时增多、CDC 延迟 邮件 + Jira 工单

3.2 P0 级:立即响应(5 分钟内)

P0 级告警意味着生产事故已经发生或即将发生,必须立即介入。

规则1:In-Doubt Transaction 检测

yaml 复制代码
- alert: InDoubtTransactionDetected
  expr: xa_indoubt_transactions_total > 0
  for: 1m
  labels:
    severity: P0
    component: xa_transaction
  annotations:
    summary: "XA In-Doubt Transaction 检测到 {{ $value }} 个未决事务"
    description: >
      数据库中存在 {{ $value }} 个 In-Doubt Transaction,
      这些事务持有数据库锁,会阻塞所有相关表的写操作。
      请立即检查 tm_log 表,确认是提交还是回滚。
      应急处理:通过 Admin 手动恢复平台强制回滚或提交 In-Doubt 事务。

为什么是 P0? In-Doubt 事务持有数据库锁(行级锁或表级意向锁),所有涉及这些行的写操作都会被阻塞。如果是热点数据行,可能导致整个业务模块的写入瘫痪。

规则2:全局事务成功率跌破阈值

yaml 复制代码
- alert: TransactionSuccessRateBelowThreshold
  expr: |
    (
      sum(rate(transaction_committed_total[5m]))
      /
      (sum(rate(transaction_committed_total[5m])) + sum(rate(transaction_rollbacked_total[5m])))
    ) < 0.999
  for: 3m
  labels:
    severity: P0
    component: global_transaction
  annotations:
    summary: "全局事务成功率低于 99.9%,当前 {{ $value | humanizePercentage }}"
    description: >
      过去 5 分钟全局事务成功率降至 {{ $value | humanizePercentage }}。
      请检查 L1 面板的失败 Top 10 服务排行,定位故障服务。

阈值设定依据:对于核心交易链路,99.9% 是行业基准。假设 TPS=1000,0.1% 的失败率意味着每分钟 60 次失败,这已经超出了可接受的业务波动范围。

规则3:事务 P99 延迟超过业务容忍上限

yaml 复制代码
- alert: TransactionP99ExceedLimit
  expr: |
    histogram_quantile(0.99, rate(transaction_duration_seconds_bucket[5m])) > 10
  for: 3m
  labels:
    severity: P0
    component: global_transaction
  annotations:
    summary: "事务 P99 延迟超过 10 秒,当前 {{ $value }}s"
    description: >
      事务端到端延迟 P99 已超过 10 秒,可能导致上游服务超时、
      连接池耗尽、用户请求失败。请检查慢事务分布和数据库性能。

阈值设定依据:10 秒是大多数 RPC 框架和网关的默认超时上限。超过 10 秒的事务会引发连锁超时,导致线程池耗尽和雪崩效应。

3.3 P1 级:紧急处理(15 分钟内)

P1 级告警意味着故障正在积累,如果不处理会在未来升级为 P0 事故。

规则4:死信消息堆积

yaml 复制代码
- alert: DeadMessagePilingUp
  expr: dead_message_total > 100
  for: 5m
  labels:
    severity: P1
    component: message
  annotations:
    summary: "死信消息堆积超过 100,当前 {{ $value }} 条"
    description: >
      死信消息总数已达 {{ $value }} 条,这些消息对应的业务事件未成功投递。
      请通过 Admin 手动恢复平台检查死信列表,按业务影响程度选择重投或跳过。

规则5:TCC 悬挂事务异常增多

yaml 复制代码
- alert: TCCHangingTransactionsExceeded
  expr: tcc_hanging_transactions_total > 50
  for: 5m
  labels:
    severity: P1
    component: tcc
  annotations:
    summary: "TCC 悬挂事务超过 50,当前 {{ $value }} 个"
    description: >
      悬挂事务表示 Cancel 比 Try 先到达或 Try 已超时回滚。
      请检查 Try 接口的响应时间是否过长导致超时,或网络是否存在延迟。
      可通过 Admin 手动恢复平台批量处理悬挂事务。

规则6:Saga 补偿失败频发

yaml 复制代码
- alert: SagaCompensationFrequentFailure
  expr: rate(saga_compensation_failed_total[10m]) > 1  # 10分钟内超过10次,即速率>1/分钟
  for: 5m
  labels:
    severity: P1
    component: saga
  annotations:
    summary: "Saga 补偿失败频发,10 分钟内 {{ $value | humanize }} 次"
    description: >
      Saga 状态机的补偿操作频繁失败,可能造成数据不一致。
      请检查 saga_transaction_log 表,确认哪些事务补偿失败并手动重试。

3.4 P2 级:关注(1 小时内)

P2 级告警是预警信号,表明系统正在偏离正常状态,需要安排排查。

规则7:Seata AT 全局锁竞争频繁

yaml 复制代码
- alert: SeataLockTimeoutFrequent
  expr: rate(seata_lock_timeout_total[30m]) > 0.66  # 30分钟超过20次
  for: 10m
  labels:
    severity: P2
    component: seata_at
  annotations:
    summary: "Seata AT 全局锁超时频繁,30 分钟内 {{ $value | humanize }} 次"
    description: >
      全局锁等待超时频繁发生,表明存在热点数据竞争。
      建议排查:哪些表的哪些行竞争激烈?是否可以引入缓存或队列削峰?

规则8:CDC Binlog 消费延迟

yaml 复制代码
- alert: CDCBinlogDelayHigh
  expr: debezium_milli_seconds_behind_source > 5000
  for: 5m
  labels:
    severity: P2
    component: cdc
  annotations:
    summary: "CDC Binlog 消费延迟超过 5 秒,当前 {{ $value }}ms"
    description: >
      Debezium 消费 Binlog 延迟升高,消息投递延迟增大。
      排查方向:源库写入量是否突增?Kafka 集群负载是否过高?Debezium 任务是否有错误?

规则9:outbox PENDING 消息堆积

yaml 复制代码
- alert: OutboxPendingPilingUp
  expr: outbox_pending_total > 1000
  for: 10m
  labels:
    severity: P2
    component: message
  annotations:
    summary: "outbox 表 PENDING 消息超过 1000,当前 {{ $value }} 条"
    description: >
      待发送消息堆积,可能是定时扫描任务故障或 MQ 生产端阻塞。
      请检查消息发送任务的健康状态和 MQ 集群的可用性。

3.5 告警抑制与聚合策略

告警抑制是防止"告警风暴"的关键机制。一条网络抖动可能导致几十个事务同时失败,如果不加抑制,运维人员会收到几十条重复告警。

Prometheus Alertmanager 配置示例

yaml 复制代码
# alertmanager.yml
route:
  receiver: 'default'
  group_by: ['alertname', 'severity']
  group_wait: 30s       # 第一次告警到达后等待30s,聚合同一组的其他告警
  group_interval: 5m    # 同组告警的发送间隔
  repeat_interval: 4h   # 未恢复告警的重复发送间隔

  routes:
    - match:
        severity: P0
      receiver: 'p0-oncall'
      group_wait: 10s    # P0告警等待时间更短
      repeat_interval: 5m # P0告警每5分钟重复一次直到确认
    - match:
        severity: P1
      receiver: 'p1-team'
    - match:
        severity: P2
      receiver: 'p2-jira'

inhibit_rules:
  - source_match:
      severity: 'P0'
    target_match:
      severity: 'P1'
    equal: ['component', 'instance']
  # 含义:同一组件同一实例上如果已有P0告警,则抑制P1告警

应用层自定义抑制(Micrometer 层面):

java 复制代码
// 同一 xid 的重复告警 5 分钟内只发一次
private final Map<String, Instant> xidAlertCache = new ConcurrentHashMap<>();

public void alertIfNeeded(String xid, String alertType) {
    String key = xid + ":" + alertType;
    Instant lastAlert = xidAlertCache.get(key);
    if (lastAlert == null || Duration.between(lastAlert, Instant.now()).toMinutes() >= 5) {
        xidAlertCache.put(key, Instant.now());
        // 发送告警
        alertingService.send(alertType, xid);
    }
}

3.6 告警分级规则决策树图

flowchart TD Start["Prometheus 告警触发"] --> Q1{"条件: In-Doubt > 0
或 成功率 < 99.9%
或 P99 > 10s"} Q1 -->|是| P0_Action["P0 立即响应
─────────────────
通知: 电话 + 企业微信 + PagerDuty
响应时限: 5分钟
处理人: On-Call SRE
─────────────────
典型操作:
• 检查 tm_log 表
• 强制回滚 In-Doubt 事务
• 回滚故障服务的全局事务"] Q1 -->|否| Q2{"条件: 死信 > 100
或 悬挂 > 50
或 补偿失败速率 > 1/min"} Q2 -->|是| P1_Action["P1 紧急处理
─────────────────
通知: 企业微信 + 邮件
响应时限: 15分钟
处理人: 组件负责人
─────────────────
典型操作:
• 检查死信列表并重投
• 批量处理悬挂事务
• 手动重试补偿"] Q2 -->|否| Q3{"条件: 锁超时速率 > 0.66/min
或 CDC延迟 > 5s
或 PENDING > 1000
或 P99 > 基线2倍"} Q3 -->|是| P2_Action["P2 关注
─────────────────
通知: 邮件 + Jira工单
响应时限: 1小时
处理人: 开发工程师
─────────────────
典型操作:
• 分析热点数据
• 排查 CDC 链路
• 优化慢事务"] Q3 -->|否| Normal["系统正常运行
无需告警"] P0_Action -.->|"告警抑制: 同一组件P0
抑制同级P1"| P1_Action P1_Action -.->|"告警聚合: 同一 xid
5分钟聚合"| P2_Action style P0_Action fill:#ff4444,stroke:#333,color:#fff style P1_Action fill:#ff8800,stroke:#333,color:#fff style P2_Action fill:#ffcc00,stroke:#333,color:#000 style Normal fill:#44cc44,stroke:#333,color:#fff

图 3-1 告警分级规则决策树图

  • 整体说明:该决策树展示了从 Prometheus 告警触发到分级响应的完整流程。每个节点都对应明确的条件判断,每条分支都导向具体的响应动作。
  • 逐条件说明
    • P0 条件:三个条件任意满足一个即触发。In-Doubt 意味着锁持有(直接影响写操作),成功率 <99.9% 意味着核心业务受损,P99>10s 意味着超时连锁反应。
    • P1 条件:死信/悬挂/补偿失败都是"积累型"故障,单条不影响整体但堆积后会造成业务延迟。
    • P2 条件:锁超时/CDC 延迟/PENDING 堆积/P99 偏高都是"性能劣化"信号,给予相对充裕的响应时间。
  • 关联说明
    • 告警抑制:当同一组件已触发 P0 告警时,同一组件的 P1 告警被抑制(因为 P0 的响应自然会覆盖 P1 的处理)。
    • 告警聚合:同一 xid 的多次告警在 5 分钟内合并为一条通知,减少告警噪声。
    • 告警静默:计划内维护期间通过 Alertmanager Silence API 批量静默。
  • 关键结论 :分级告警体系的核心在于匹配故障严重度与响应资源。P0 占用的 On-Call 资源昂贵,必须精准触发;P1 和 P2 提供了足够的缓冲时间,让团队在正常工作节奏中处理问题,而非被告警追赶。

4. transaction_log 统一日志表设计:DDL、写入时机、清理策略

transaction_log 表是整个监控与恢复体系的数据枢纽------它是 L3 单事务追踪的数据源,是手动恢复平台的检索基础,也是故障复盘的完整记录。其设计需兼顾查询性能、存储效率和扩展性。

4.1 表结构 DDL

sql 复制代码
CREATE TABLE `transaction_log` (
  -- ==================== 主键与业务标识 ====================
  `xid` VARCHAR(128) NOT NULL COMMENT '全局事务ID,由Seata TC生成或自定义ID生成器生成,全系统唯一',
  `business_id` VARCHAR(128) DEFAULT NULL COMMENT '业务标识,如订单号/用户ID/交易流水号。非事务系统字段,而是业务层面的人类可读标识,便于业务方检索',
  
  -- ==================== 事务类型与状态 ====================
  `transaction_type` ENUM('XA','AT','TCC','SAGA','MQ') NOT NULL COMMENT '分布式事务方案类型。用于区分不同方案的监控和恢复策略',
  `status` ENUM('BEGIN','COMMITTING','COMMITTED','ROLLBACKING','ROLLBACKED','FAILED','TIMEOUT') NOT NULL COMMENT '
    全局事务状态流转:
    BEGIN → COMMITTING → COMMITTED  (正常提交流程)
    BEGIN → ROLLBACKING → ROLLBACKED (正常回滚流程)
    BEGIN → TIMEOUT                   (超时未完成)
    ROLLBACKING → FAILED             (回滚失败,需人工介入)
  ',
  
  -- ==================== 时间维度 ====================
  `start_time` DATETIME(3) NOT NULL COMMENT '事务开始时间,精确到毫秒。由TM在调用TC begin()成功后记录',
  `end_time` DATETIME(3) DEFAULT NULL COMMENT '事务结束时间。COMMITTED/ROLLBACKED/FAILED/TIMEOUT时更新',
  `timeout` INT DEFAULT 60000 COMMENT '事务超时时间(毫秒),默认60秒。用于判断事务是否超时',
  
  -- ==================== 重试与错误 ====================
  `retry_count` INT DEFAULT 0 COMMENT '重试次数。TCC的Confirm/Cancel重试、Saga的补偿重试均递增此字段',
  `error_message` TEXT DEFAULT NULL COMMENT '错误信息或异常堆栈。记录事务失败时的完整异常信息,便于故障分析',
  
  -- ==================== JSON 扩展字段 ====================
  `branches` JSON DEFAULT NULL COMMENT '
    分支事务数组,JSON格式。每个元素包含:
    {
      "branchId": "service-order:20230101-001",  -- 分支事务ID
      "resourceId": "jdbc:mysql://order-db:3306/order",  -- 资源标识
      "serviceName": "order-service",             -- 服务名
      "status": "CONFIRMED",                      -- 分支状态:TRYING/CONFIRMED/CANCELLED/FAILED
      "startTime": "2025-03-10T10:01:00.000",     -- 分支开始时间
      "endTime": "2025-03-10T10:01:02.150",       -- 分支结束时间
      "durationMs": 2150,                         -- 分支耗时(毫秒)
      "errorMessage": null                        -- 分支错误信息
    }
  ',
  `payload` JSON DEFAULT NULL COMMENT '
    业务上下文参数,JSON格式。存储业务层面的事务关键信息,便于:
    1. 判断业务影响范围(如涉及的订单金额、用户ID等)
    2. 手动恢复时的业务状态校验
    示例:{"orderId": "ORD123456", "userId": "U789", "amount": 9999, "skuId": "SKU001"}
  ',
  
  -- ==================== 审计字段 ====================
  `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '记录创建时间',
  `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '记录更新时间',
  
  -- ==================== 主键与索引 ====================
  PRIMARY KEY (`xid`),
  INDEX `idx_business_id` (`business_id`) COMMENT '业务检索:通过订单号等业务标识快速查找相关事务',
  INDEX `idx_status` (`status`) COMMENT '状态检索:查询所有FAILED事务进行手动恢复',
  INDEX `idx_start_time` (`start_time`) COMMENT '时间范围检索:按事务开始时间过滤',
  INDEX `idx_type_status` (`transaction_type`, `status`) COMMENT '方案+状态组合检索:如查询所有TCC的悬挂事务',
  INDEX `idx_end_time_status` (`end_time`, `status`) COMMENT '清理任务:查找30天前的已完成事务'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 
COMMENT='分布式事务统一日志表 - 全局事务生命周期记录';

DDL 设计要点解读

  1. xid 为主键:全局事务 ID 天然具备唯一性,适合作为主键。对于非 Seata 方案(如纯 Saga 或纯消息),需要自定义 ID 生成器确保全局唯一。

  2. branches 使用 JSON 而非行记录 :一个全局事务可能有 1-N 个分支,行记录方式需要额外的 transaction_branch 表,查询时需要 JOIN,增加了复杂度。JSON 字段自包含,一次查询即可获取完整信息。MySQL 5.7+ 的 JSON 类型支持虚拟列索引:

    sql 复制代码
    ALTER TABLE transaction_log 
    ADD COLUMN branch_count INT GENERATED ALWAYS AS (JSON_LENGTH(branches)) VIRTUAL;
    CREATE INDEX idx_branch_count ON transaction_log(branch_count);
  3. payload 字段的业务价值:手动恢复时,运维人员需要知道"这个事务涉及哪些业务对象"。例如处理一个 TCC 悬挂事务,需要知道是哪个用户的资金操作、金额是多少,才能判断是否可以安全回滚。

  4. retry_counterror_message 分离retry_count 用于告警阈值判断(如重试 >5 次触发 P2),error_message 用于故障分析(存储完整堆栈)。

4.2 写入时机与逻辑

flowchart LR TM["TM: @GlobalTransactional AOP"] -->|"1.调用 TC begin() 获取xid"| TC["TC: Seata Server"] TM -->|"2.同步 INSERT
status='BEGIN'"| DB[("transaction_log 表")] TM -->|"3.执行业务逻辑
调用各 RM"| RM1["RM: 订单服务"] TM -->|"3.执行业务逻辑
调用各 RM"| RM2["RM: 库存服务"] RM1 -->|"4.Phase1完成
异步 UPDATE branches JSON"| DB RM2 -->|"4.Phase1完成
异步 UPDATE branches JSON"| DB TC -->|"5.全局决议 commit/rollback
UPDATE status, end_time, error_message"| DB

详细写入逻辑

步骤1:TM 写入初始记录

java 复制代码
// 在 @GlobalTransactional 切面的 around 通知中
@Around("@annotation(GlobalTransactional)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // 1. 调用 begin 获取 xid
    String xid = transactionManager.begin(timeout);
    
    // 2. 同步写入 transaction_log
    transactionLogMapper.insert(TransactionLog.builder()
        .xid(xid)
        .businessId(extractBusinessId(pjp))  // 从方法参数中提取
        .transactionType(detectType())        // 根据配置或注解判断
        .status("BEGIN")
        .startTime(LocalDateTime.now())
        .timeout(timeout)
        .payload(buildPayload(pjp))          // 序列化业务参数
        .build());
    
    // 3. 执行业务逻辑
    try {
        Object result = pjp.proceed();
        // TC 会在全局提交完成后更新 status 和 end_time
        return result;
    } catch (Exception e) {
        // 异常由 TC 全局回滚后更新 status
        throw e;
    }
}

步骤2:TC 更新最终状态

Seata TC 在全局事务完成时更新 transaction_log。可以通过两种方式实现:

方式一:TC 回调(推荐)。TC 完成全局决议后,通过事务钩子回调 TM:

java 复制代码
// Seata 1.6.x 支持 TransactionHook
GlobalTransaction globalTx = GlobalTransactionContext.getCurrentOrCreate();
globalTx.addHook(new TransactionHook() {
    @Override
    public void afterCompletion() {
        transactionLogMapper.updateStatus(
            xid, 
            globalTx.getStatus().name(),  // COMMITTED / ROLLBACKED
            LocalDateTime.now(),
            globalTx.getError()
        );
    }
});

方式二:RM 异步上报。各 RM 在 Phase 2 完成后更新 branches JSON:

java 复制代码
// 在 DataSourceProxy.commit() 或 TCC Confirm 方法中
TransactionLog log = transactionLogMapper.selectByXid(xid);
JSONArray branches = log.getBranches();
JSONObject branch = new JSONObject();
branch.put("branchId", branchId);
branch.put("status", "CONFIRMED");
branch.put("endTime", LocalDateTime.now().toString());
branches.add(branch);
transactionLogMapper.updateBranches(xid, branches.toJSONString());

4.3 与辅助表的关联查询 SQL

以下 SQL 是 L3 单事务追踪面板和手动恢复平台的核心查询:

查询1:获取全局事务完整信息(含分支详情)

sql 复制代码
SELECT 
  tl.xid,
  tl.business_id,
  tl.transaction_type,
  tl.status,
  tl.start_time,
  tl.end_time,
  TIMESTAMPDIFF(MILLISECOND, tl.start_time, tl.end_time) AS duration_ms,
  tl.retry_count,
  tl.error_message,
  JSON_PRETTY(tl.branches) AS branches_json,
  JSON_PRETTY(tl.payload) AS payload_json
FROM transaction_log tl
WHERE tl.xid = '192.168.1.100:8091:20250310001';

查询2:关联 AT 的 undo_log(查看回滚镜像数据)

sql 复制代码
SELECT 
  ul.branch_id,
  ul.xid,
  ul.log_status,
  JSON_PRETTY(ul.rollback_info) AS rollback_info,
  ul.log_created,
  ul.log_modified
FROM undo_log ul
WHERE ul.xid = '192.168.1.100:8091:20250310001'
ORDER BY ul.log_created;

查询3:关联 TCC 的防悬挂日志

sql 复制代码
SELECT 
  tfl.xid,
  tfl.branch_id,
  tfl.business_action,  -- TRY / CONFIRM / CANCEL
  tfl.status,
  tfl.request_json,
  tfl.response_json,
  tfl.create_time
FROM tcc_fence_log tfl
WHERE tfl.xid = '192.168.1.100:8091:20250310001'
ORDER BY tfl.create_time;

查询4:关联消息 outbox 表

sql 复制代码
SELECT 
  o.id,
  o.aggregate_id,
  o.aggregate_type,
  o.message_type,
  o.status,
  o.retry_count,
  o.error_message,
  JSON_PRETTY(o.message_body) AS message_body,
  o.created_at
FROM outbox o
WHERE o.aggregate_id = (
  SELECT business_id FROM transaction_log WHERE xid = '192.168.1.100:8091:20250310001'
)
ORDER BY o.created_at;

查询5:关联 Saga 事务日志

sql 复制代码
SELECT 
  stl.saga_id,
  stl.step_name,
  stl.step_type,  -- FORWARD / COMPENSATION
  stl.status,
  stl.start_time,
  stl.end_time,
  stl.error_message
FROM saga_transaction_log stl
WHERE stl.xid = '192.168.1.100:8091:20250310001'
ORDER BY stl.step_order;

查询6:查询所有需要人工介入的失败事务(手动恢复平台列表页)

sql 复制代码
SELECT 
  xid, business_id, transaction_type, status, 
  start_time, end_time, error_message
FROM transaction_log
WHERE status IN ('FAILED', 'TIMEOUT')
  AND start_time BETWEEN '2025-03-01' AND '2025-03-10'
ORDER BY start_time DESC
LIMIT 50 OFFSET 0;

4.4 清理策略

java 复制代码
@Component
public class TransactionLogCleaner {
    
    @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
    public void cleanCompletedTransactions() {
        LocalDateTime thirtyDaysAgo = LocalDateTime.now().minusDays(30);
        
        // 分批删除,避免长事务锁表
        int batchSize = 1000;
        int deleted;
        do {
            deleted = jdbcTemplate.update(
                "DELETE FROM transaction_log " +
                "WHERE status IN ('COMMITTED', 'ROLLBACKED') " +
                "  AND end_time < ? " +
                "LIMIT ?",
                thirtyDaysAgo, batchSize
            );
            // 每批间隔 100ms,减轻数据库压力
            if (deleted > 0) {
                Thread.sleep(100);
            }
        } while (deleted >= batchSize);
        
        log.info("清理完成:删除 {} 天前的已完成事务记录", 30);
    }
}

清理策略设计要点

  • 仅清理 COMMITTEDROLLBACKED 状态,保留 FAILEDTIMEOUT 用于长期复盘;
  • 分批删除避免大事务锁表;
  • 失败记录可以设定更长的保留期(如 90 天),或定期导出到离线存储(如 HDFS 或 S3)。

4.5 transaction_log 表与各辅助表的关联 ER 图

erDiagram TRANSACTION_LOG { varchar xid PK "全局事务ID" varchar business_id "业务标识" enum transaction_type "XA/AT/TCC/SAGA/MQ" enum status "BEGIN/COMMITTING/COMMITTED/ROLLBACKING/ROLLBACKED/FAILED/TIMEOUT" datetime start_time "事务开始时间" datetime end_time "事务结束时间" int timeout "超时时间(ms)" int retry_count "重试次数" text error_message "错误信息" json branches "分支事务详情数组" json payload "业务上下文参数" } UNDO_LOG { bigint id PK varchar branch_id FK "分支事务ID" varchar xid FK "全局事务ID" longblob rollback_info "回滚镜像数据" int log_status "日志状态" datetime log_created "创建时间" } OUTBOX { bigint id PK varchar aggregate_id "聚合根ID(对应business_id)" varchar aggregate_type "聚合类型" varchar message_type "消息类型" enum status "PENDING/SENDING/SENT/DEAD" int retry_count "重试次数" json message_body "消息体" datetime created_at "创建时间" } TCC_FENCE_LOG { bigint id PK varchar xid FK "全局事务ID" varchar branch_id "分支事务ID" varchar business_action "TRY/CONFIRM/CANCEL" enum status "TRYING/CONFIRMED/CANCELLED/SUSPENDED" json request_json "请求参数" datetime create_time "创建时间" } SAGA_TRANSACTION_LOG { bigint id PK varchar saga_id "Saga实例ID" varchar xid FK "全局事务ID" varchar step_name "步骤名称" enum step_type "FORWARD/COMPENSATION" enum status "RUNNING/SUCCESS/FAILED/COMPENSATION_FAILED" datetime start_time "开始时间" datetime end_time "结束时间" text error_message "错误信息" } RECOVERY_AUDIT_LOG { bigint id PK varchar operator "操作人" enum operation_type "操作类型" varchar target_xid FK "目标事务ID" varchar reason "操作原因" enum result "SUCCESS/FAILED" text detail "操作详情" datetime created_at "创建时间" } TRANSACTION_LOG ||--o{ UNDO_LOG : "关联查询: xid" TRANSACTION_LOG ||--o{ OUTBOX : "关联查询: business_id = aggregate_id" TRANSACTION_LOG ||--o{ TCC_FENCE_LOG : "关联查询: xid" TRANSACTION_LOG ||--o{ SAGA_TRANSACTION_LOG : "关联查询: xid" TRANSACTION_LOG ||--o{ RECOVERY_AUDIT_LOG : "操作审计: target_xid"

图 4-1 transaction_log 表与各辅助表的关联 ER 图

  • 整体说明 :该 ER 图展示了 transaction_log 作为中心枢纽与四大辅助表(undo_logoutboxtcc_fence_logsaga_transaction_log)以及 recovery_audit_log 审计表的关联关系。箭头方向表示"可追溯"的查询路径。
  • 逐表说明
    • transaction_log :核心日志表,记录全局事务从生到死的完整生命周期。branches JSON 字段自包含分支详情,减少跨表 JOIN。
    • undo_log :Seata AT 模式的行级回滚日志,记录数据修改前镜像。通过 xid 关联到全局事务。
    • outbox :本地消息表 / 事务发件箱,记录待发送消息。通过 business_id(或 aggregate_id)与全局事务间接关联------因为一条业务操作可能产生多条消息。
    • tcc_fence_log :TCC 防悬挂表,记录 Try/Confirm/Cancel 各阶段的执行状态。通过 xid 直接关联。
    • saga_transaction_log :Saga 状态机执行日志,记录每个步骤的正向操作和补偿操作。通过 xidsaga_id 关联。
    • recovery_audit_log :手动恢复操作的审计日志,通过 target_xid 与全局事务关联,实现"谁在何时对此事务做了何种操作"的完整追溯。
  • 关联说明
    • transaction_log ↔ undo_log :AT 模式手动恢复时,需查看 undo_log 确认回滚镜像是否正确;
    • transaction_log ↔ outbox :消息模式手动恢复时,需查看 outbox 确认消息内容和重试次数;
    • transaction_log ↔ tcc_fence_log:TCC 模式手动恢复时,需查看防悬挂表判断悬挂状态;
    • transaction_log ↔ saga_transaction_log:Saga 模式手动恢复时,需查看状态机执行历史定位补偿失败点;
    • transaction_log ↔ recovery_audit_log:记录所有针对该事务的手动操作,保障合规审计。
  • 关键结论 :这张 ER 图体现了"一个全局事务、多个辅助表"的数据组织方式。在手动恢复平台中,一次 xid 查询可以拉出该事务在所有方案层的完整证据链 ------从全局时间线(transaction_log.branches)到 AT 的回滚镜像(undo_log.rollback_info)、TCC 的防悬挂记录(tcc_fence_log)、消息的发件箱状态(outbox)、Saga 的补偿历史(saga_transaction_log)。这种设计将故障定位时间从"逐个表翻找"压缩到"一次查询全景呈现"。

5. 手动恢复平台功能设计:查询、详情、操作、审计

当告警响起,运维人员需要的不是"SQL 客户端",而是一个安全、受控、可追溯的 Admin 后台。本节设计手动恢复平台的完整功能架构。

5.1 平台架构总览

flowchart TB subgraph 前端层 FE["Admin 后台
Thymeleaf / Vue
角色:DBA / SRE / 开发者"] end subgraph API层 API1["事务查询 API
GET /admin/transactions"] API2["事务详情 API
GET /admin/transactions/{xid}"] API3["手动操作 API
POST /admin/recovery/*"] end subgraph 服务层 SVC["RecoveryService
• 事务检索
• 状态校验
• 操作执行
• 审计记录"] end subgraph 数据层 DB1[("transaction_log")] DB2[("undo_log")] DB3[("tcc_fence_log")] DB4[("outbox")] DB5[("saga_transaction_log")] DB6[("recovery_audit_log")] end subgraph 外部依赖 EXT1["Seata TC
commit() / rollback()"] EXT2["RocketMQ
重投 / 跳过死信"] EXT3["Atomikos
强制提交 / 回滚"] end FE --> API1 & API2 & API3 API1 & API2 & API3 --> SVC SVC --> DB1 & DB2 & DB3 & DB4 & DB5 & DB6 SVC --> EXT1 & EXT2 & EXT3

分层详细说明

  • 用户交互层:采用前后端分离架构,前端可使用 Vue + Element UI 构建。页面包括:事务列表页(支持多条件组合检索、分页、状态颜色标识)、事务详情页(时间线组件、辅助表 JSON 渲染、错误堆栈折叠)、手动操作对话框(二次确认、原因填写)、审计日志页。所有危险按钮(强制回滚、释放锁)默认置灰,需勾选"我已了解风险"后才可点击。
  • API 网关层 :负责统一的认证(验证 JWT Token)、鉴权(基于 RBAC,DBA 可执行全部操作,SRE 可重试和跳过,开发者只读)、限流(同一 IP 每分钟最多 10 次写操作)和请求日志(记录所有 API 调用的 operatorURI参数响应状态码,作为审计的辅助数据)。
  • 核心服务层 :拆分为三个独立模块。事务查询服务 构建动态 SQL,从 transaction_log 表中检索事务列表,对 error_message 进行截断处理以节省带宽。事务详情服务 是平台的核心------它根据 xidtransaction_type 决策需要查询哪些辅助表,构建完整时间线,并将 branches JSON 与辅助表记录交叉验证。手动操作服务是所有变更操作的唯一入口,执行固定的操作流程:前置校验 → 风险评估 → 二次确认 → 执行 → 审计写入。
  • 数据访问层 :封装对所有数据库和外部系统的调用。对 transaction_log 的更新使用乐观锁(version 字段)防止并发覆盖;对辅助表的查询使用只读事务或从库读取,避免影响业务主库;对 Seata TC 的调用通过其 RPC 接口(DefaultCore.commit(xid)),对 RocketMQ 的调用通过 DefaultMQAdminExt 查看死信队列或重置消费位点。
  • 外部依赖层 :手动恢复操作的"执行器"。注意 Seata TCAtomikos 的接口通常不直接暴露 HTTP,需要自定义一个薄薄的代理层(如一个 Spring Boot 管理端点)将 HTTP 请求转换为 TC 的 RPC 调用。对于 RocketMQ,平台直接连接 NameServer 获取 Admin 视图。业务服务(如订单查询接口)用于在处理悬挂事务时获取业务对象的真实状态,避免误操作。

安全设计要点

  1. 所有写操作强制记录审计日志 ,采用 @Audited 自定义注解 + AOP 实现,杜绝遗漏。
  2. 操作前二次确认 :后端返回需要确认的信息(如"将回滚事务 XXX,涉及订单 ORD123456,金额 999 元"),前端展示后要求用户再次提交 confirmed=true 参数。
  3. 操作隔离:手动恢复平台部署在独立的网络区域,仅允许运维 VPN 访问,与业务流量完全隔离。
  4. 回滚操作:高风险操作(强制回滚、释放全局锁)要求操作者具有 DBA 角色,并填写不少于 10 个字符的操作原因。

5.2 事务查询功能

接口设计

java 复制代码
@RestController
@RequestMapping("/admin")
public class RecoveryController {
    
    @Autowired
    private RecoveryService recoveryService;
    
    @Autowired
    private TransactionLogMapper transactionLogMapper;
    
    /**
     * 多条件分页查询事务列表
     */
    @GetMapping("/transactions")
    @PreAuthorize("hasRole('OPS') or hasRole('DBA')") // 权限控制
    public PageResponse<TransactionListVO> queryTransactions(
            @RequestParam(required = false) String xid,
            @RequestParam(required = false) String businessId,
            @RequestParam(required = false) String transactionType,
            @RequestParam(required = false) String status,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime,
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "20") int size) {
        
        // 构建动态查询条件
        QueryWrapper<TransactionLog> wrapper = new QueryWrapper<>();
        wrapper.eq(StringUtils.hasText(xid), "xid", xid)
               .eq(StringUtils.hasText(businessId), "business_id", businessId)
               .eq(StringUtils.hasText(transactionType), "transaction_type", transactionType)
               .eq(StringUtils.hasText(status), "status", status)
               .ge(startTime != null, "start_time", startTime)
               .le(endTime != null, "start_time", endTime)
               .orderByDesc("start_time");
        
        Page<TransactionLog> pageResult = transactionLogMapper.selectPage(
            new Page<>(page, size), wrapper);
        
        // 转换为列表 VO(脱敏处理,不返回完整 JSON)
        return PageResponse.of(pageResult.convert(this::toListVO));
    }
    
    private TransactionListVO toListVO(TransactionLog log) {
        return TransactionListVO.builder()
            .xid(log.getXid())
            .businessId(log.getBusinessId())
            .transactionType(log.getTransactionType())
            .status(log.getStatus())
            .startTime(log.getStartTime())
            .endTime(log.getEndTime())
            .durationMs(log.getEndTime() != null ? 
                Duration.between(log.getStartTime(), log.getEndTime()).toMillis() : null)
            .errorSummary(log.getErrorMessage() != null ? 
                log.getErrorMessage().substring(0, Math.min(200, log.getErrorMessage().length())) : null)
            .build();
    }
}

前端页面设计要点

  • 顶部筛选区:xid 输入框、businessId 输入框、transactionType 下拉(XA/AT/TCC/Saga/消息)、status 下拉(FAILED/TIMEOUT/COMMITTED/ROLLBACKED)、时间范围选择器
  • 列表展示:表格列包括 xid(可点击跳转详情)、businessId类型状态(颜色标识:绿色 COMMITTED、橙色 ROLLBACKED、红色 FAILED)、开始时间耗时错误摘要
  • 列表提供"快速操作"按钮:对于 FAILED 事务,提供"一键回滚"入口

5.3 事务详情展示

接口设计

java 复制代码
@GetMapping("/transactions/{xid}")
@PreAuthorize("hasRole('OPS') or hasRole('DBA')")
public TransactionDetailVO getTransactionDetail(@PathVariable String xid) {
    // 1. 查询全局事务基本信息
    TransactionLog tl = transactionLogMapper.selectById(xid);
    if (tl == null) {
        throw new NotFoundException("事务不存在: " + xid);
    }
    
    // 2. 构建全局时间线
    List<TimelineEvent> timeline = buildTimeline(tl);
    
    // 3. 查询关联辅助表数据
    Map<String, Object> auxiliaryData = queryAuxiliaryData(tl);
    
    return TransactionDetailVO.builder()
        .xid(tl.getXid())
        .businessId(tl.getBusinessId())
        .transactionType(tl.getTransactionType())
        .status(tl.getStatus())
        .startTime(tl.getStartTime())
        .endTime(tl.getEndTime())
        .durationMs(Duration.between(tl.getStartTime(), 
            tl.getEndTime() != null ? tl.getEndTime() : LocalDateTime.now()).toMillis())
        .timeout(tl.getTimeout())
        .retryCount(tl.getRetryCount())
        .errorMessage(tl.getErrorMessage())
        .payload(tl.getPayload())
        .timeline(timeline)
        .auxiliaryData(auxiliaryData)
        .build();
}

/**
 * 构建全局事务时间线
 */
private List<TimelineEvent> buildTimeline(TransactionLog tl) {
    List<TimelineEvent> events = new ArrayList<>();
    
    // 添加 BEGIN 事件
    events.add(new TimelineEvent("BEGIN", tl.getStartTime(), null, null));
    
    // 解析 branches JSON,添加各分支事件
    JSONArray branches = tl.getBranches();
    if (branches != null) {
        for (int i = 0; i < branches.size(); i++) {
            JSONObject branch = branches.getJSONObject(i);
            events.add(new TimelineEvent(
                "Branch-" + branch.getString("serviceName") + "-" + branch.getString("status"),
                LocalDateTime.parse(branch.getString("startTime")),
                branch.containsKey("endTime") ? 
                    LocalDateTime.parse(branch.getString("endTime")) : null,
                branch.optString("errorMessage")
            ));
        }
    }
    
    // 添加最终状态事件
    events.add(new TimelineEvent(tl.getStatus(), tl.getEndTime(), null, tl.getErrorMessage()));
    
    // 按时间排序
    events.sort(Comparator.comparing(TimelineEvent::getTime, 
        Comparator.nullsLast(Comparator.naturalOrder())));
    return events;
}

/**
 * 查询辅助表数据
 */
private Map<String, Object> queryAuxiliaryData(TransactionLog tl) {
    Map<String, Object> data = new HashMap<>();
    
    switch (tl.getTransactionType()) {
        case "AT":
            // 查询 undo_log
            data.put("undoLogs", jdbcTemplate.queryForList(
                "SELECT branch_id, log_status, rollback_info, log_created FROM undo_log WHERE xid = ?",
                tl.getXid()));
            // 查询全局锁
            data.put("globalLocks", jdbcTemplate.queryForList(
                "SELECT row_key, xid, table_name, pk FROM lock_table WHERE xid = ?",
                tl.getXid()));
            break;
        case "TCC":
            data.put("fenceLogs", jdbcTemplate.queryForList(
                "SELECT branch_id, business_action, status, create_time FROM tcc_fence_log WHERE xid = ?",
                tl.getXid()));
            break;
        case "SAGA":
            data.put("sagaLogs", jdbcTemplate.queryForList(
                "SELECT step_name, step_type, status, error_message FROM saga_transaction_log WHERE xid = ?",
                tl.getXid()));
            break;
        case "MQ":
            data.put("outboxMessages", jdbcTemplate.queryForList(
                "SELECT id, message_type, status, retry_count, message_body FROM outbox WHERE aggregate_id = ?",
                tl.getBusinessId()));
            break;
    }
    
    return data;
}

5.4 手动操作功能

操作1:重试失败的 Confirm / Cancel(TCC)

java 复制代码
/**
 * 重试 TCC 的 Confirm 或 Cancel 操作
 * 适用场景:TCC 事务因网络抖动导致 Confirm/Cancel 暂时失败,但幂等逻辑保证重试安全
 */
@PostMapping("/recovery/retry/{xid}")
@PreAuthorize("hasRole('DBA')") // 高危操作,仅 DBA 可执行
@Audited(operation = "RETRY_CONFIRM") // 自定义注解触发审计
public ApiResponse<String> retryConfirmOrCancel(
        @PathVariable String xid,
        @RequestParam String reason) {
    
    // 1. 查询事务当前状态
    TransactionLog tl = transactionLogMapper.selectById(xid);
    if (tl == null) {
        return ApiResponse.error("事务不存在");
    }
    
    // 2. 前置校验:必须是 TCC 且状态为 CONFIRM_FAILED 或 CANCEL_FAILED
    if (!"TCC".equals(tl.getTransactionType())) {
        return ApiResponse.error("仅支持 TCC 事务的重试操作");
    }
    if (!Arrays.asList("CONFIRM_FAILED", "CANCEL_FAILED").contains(tl.getBranchStatus())) {
        return ApiResponse.error("当前状态不支持重试,status=" + tl.getStatus());
    }
    
    // 3. 调用 TC 重试
    try {
        if ("CONFIRM_FAILED".equals(tl.getBranchStatus())) {
            seataTransactionManager.commit(xid); // 幂等重试 Commit
        } else {
            seataTransactionManager.rollback(xid); // 幂等重试 Rollback
        }
        
        // 4. 记录审计日志
        recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
            .operator(getCurrentUser())
            .operationType("RETRY_CONFIRM")
            .targetXid(xid)
            .reason(reason)
            .result("SUCCESS")
            .detail("重试 TCC " + tl.getBranchStatus() + " 操作成功")
            .build());
        
        return ApiResponse.success("重试操作已提交,请刷新查看最新状态");
        
    } catch (Exception e) {
        recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
            .operator(getCurrentUser())
            .operationType("RETRY_CONFIRM")
            .targetXid(xid)
            .reason(reason)
            .result("FAILED")
            .detail("重试失败: " + e.getMessage())
            .build());
        
        return ApiResponse.error("重试失败: " + e.getMessage());
    }
}

操作2:回滚未完成的全局事务

java 复制代码
/**
 * 强制回滚全局事务
 * 适用场景:XA 的 In-Doubt 事务、AT 事务长时间处于 COMMITTING 超时
 * 风险提示:必须确认该事务未产生实际业务副作用,否则回滚后数据不一致
 */
@PostMapping("/recovery/rollback/{xid}")
@PreAuthorize("hasRole('DBA')")
@Audited(operation = "ROLLBACK")
public ApiResponse<String> forceRollback(
        @PathVariable String xid,
        @RequestParam String reason,
        @RequestParam(defaultValue = "false") boolean confirmed) {
    
    // 0. 强制要求二次确认
    if (!confirmed) {
        TransactionLog tl = transactionLogMapper.selectById(xid);
        return ApiResponse.confirm(String.format(
            "确认回滚事务 %s?业务标识:%s,当前状态:%s,请评估业务影响后再次确认",
            xid, tl.getBusinessId(), tl.getStatus()));
    }
    
    // 1. 查询事务状态
    TransactionLog tl = transactionLogMapper.selectById(xid);
    if (tl == null) {
        return ApiResponse.error("事务不存在");
    }
    
    // 2. 执行回滚(根据事务类型选择不同策略)
    try {
        switch (tl.getTransactionType()) {
            case "XA":
                // XA: 调用 Atomikos 强制回滚 In-Doubt 事务
                atomikosService.forceRollback(xid);
                break;
            case "AT":
                // AT: 调用 Seata TC 全局回滚
                seataTransactionManager.rollback(xid);
                // 如果 TC 回滚失败(如 TC 与该事务失联),直接清理残留全局锁
                jdbcTemplate.update("DELETE FROM lock_table WHERE xid = ?", xid);
                break;
            default:
                return ApiResponse.error("不支持该类型事务的强制回滚: " + tl.getTransactionType());
        }
        
        // 3. 更新 transaction_log 状态
        transactionLogMapper.updateStatus(xid, "ROLLBACKED", LocalDateTime.now(), "手动强制回滚");
        
        // 4. 审计
        recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
            .operator(getCurrentUser())
            .operationType("ROLLBACK")
            .targetXid(xid)
            .reason(reason)
            .result("SUCCESS")
            .build());
        
        return ApiResponse.success("回滚成功");
        
    } catch (Exception e) {
        recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
            .operator(getCurrentUser())
            .operationType("ROLLBACK")
            .targetXid(xid)
            .reason(reason)
            .result("FAILED")
            .detail(e.getMessage())
            .build());
        return ApiResponse.error("回滚失败: " + e.getMessage());
    }
}

操作3:跳过 / 重投死信消息

java 复制代码
/**
 * 处理死信消息:重新投递或跳过
 */
@PostMapping("/recovery/dead-message/{messageId}")
@PreAuthorize("hasRole('OPS') or hasRole('DBA')")
@Audited(operation = "SKIP_DEAD")
public ApiResponse<String> handleDeadMessage(
        @PathVariable Long messageId,
        @RequestParam String action,  // RETRY 或 SKIP
        @RequestParam String reason) {
    
    OutboxMessage msg = outboxMapper.selectById(messageId);
    if (msg == null || !"DEAD".equals(msg.getStatus())) {
        return ApiResponse.error("消息不存在或非死信状态");
    }
    
    if ("RETRY".equals(action)) {
        // 重新投递:将状态重置为 PENDING,retry_count 清零
        outboxMapper.updateStatus(messageId, "PENDING", 0);
        
        recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
            .operator(getCurrentUser())
            .operationType("SKIP_DEAD") // 实际是重投,也记录在这里
            .targetXid(msg.getAggregateId()) // 使用 aggregate_id 关联
            .reason(reason)
            .result("SUCCESS")
            .detail("消息重投: messageId=" + messageId + ", 原aggregate_id=" + msg.getAggregateId())
            .build());
        
        return ApiResponse.success("消息已重新投递");
        
    } else if ("SKIP".equals(action)) {
        // 跳过:标记为 SKIPPED
        outboxMapper.updateStatus(messageId, "SKIPPED", msg.getRetryCount());
        
        recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
            .operator(getCurrentUser())
            .operationType("SKIP_DEAD")
            .targetXid(msg.getAggregateId())
            .reason(reason)
            .result("SUCCESS")
            .detail("消息跳过: messageId=" + messageId + ", 内容=" + msg.getMessageBody())
            .build());
        
        return ApiResponse.success("消息已标记为跳过");
    }
    
    return ApiResponse.error("无效操作: " + action);
}

操作4:释放全局锁

java 复制代码
/**
 * 释放 Seata AT 的残留全局锁
 * 适用场景:Phase 2 超时但未清理锁,或 TC 重启后锁残留
 * 风险:仅当确认对应全局事务已彻底失效时才可释放
 */
@PostMapping("/recovery/release-lock")
@PreAuthorize("hasRole('DBA')")
@Audited(operation = "RELEASE_LOCK")
public ApiResponse<String> releaseGlobalLock(
        @RequestParam String xid,
        @RequestParam(required = false) String rowKey,
        @RequestParam String reason,
        @RequestParam(defaultValue = "false") boolean confirmed) {
    
    if (!confirmed) {
        List<Map<String, Object>> locks = jdbcTemplate.queryForList(
            "SELECT row_key, table_name, pk FROM lock_table WHERE xid = ?", xid);
        return ApiResponse.confirm(String.format(
            "确认释放事务 %s 的 %d 个全局锁?锁信息:%s",
            xid, locks.size(), JSON.toJSONString(locks)));
    }
    
    // 1. 校验事务状态(已结束的事务才能释放锁)
    TransactionLog tl = transactionLogMapper.selectById(xid);
    if (tl != null && !Arrays.asList("ROLLBACKED", "FAILED", "TIMEOUT").contains(tl.getStatus())) {
        return ApiResponse.error("事务仍处于活跃状态,不能释放锁。当前状态: " + tl.getStatus());
    }
    
    // 2. 删除锁记录
    int deleted;
    if (StringUtils.hasText(rowKey)) {
        deleted = jdbcTemplate.update("DELETE FROM lock_table WHERE xid = ? AND row_key = ?", xid, rowKey);
    } else {
        deleted = jdbcTemplate.update("DELETE FROM lock_table WHERE xid = ?", xid);
    }
    
    // 3. 审计
    recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
        .operator(getCurrentUser())
        .operationType("RELEASE_LOCK")
        .targetXid(xid)
        .reason(reason)
        .result("SUCCESS")
        .detail("释放了 " + deleted + " 个全局锁")
        .build());
    
    return ApiResponse.success("成功释放 " + deleted + " 个全局锁");
}

操作5:批量处理悬挂事务

java 复制代码
/**
 * 批量处理 TCC 悬挂事务
 * 适用场景:防悬挂表积压大量无对应 Try 的 Cancel 记录
 */
@PostMapping("/recovery/batch-resolve-hanging")
@PreAuthorize("hasRole('DBA')")
@Audited(operation = "BATCH_RESOLVE_HANGING")
public ApiResponse<BatchResult> batchResolveHanging(
        @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime before,
        @RequestParam String reason,
        @RequestParam(defaultValue = "false") boolean dryRun) {
    
    // 1. 查询悬挂事务列表
    List<String> hangingXids = jdbcTemplate.queryForList(
        "SELECT DISTINCT xid FROM tcc_fence_log " +
        "WHERE status = 'CANCELLED' " +
        "  AND xid NOT IN (SELECT xid FROM tcc_fence_log WHERE status = 'TRYING') " +
        "  AND create_time < ? " +
        "ORDER BY create_time",
        String.class, before);
    
    if (dryRun) {
        return ApiResponse.success(BatchResult.builder()
            .totalCount(hangingXids.size())
            .detail("试运行模式,未实际处理。发现 " + hangingXids.size() + " 条悬挂事务")
            .build());
    }
    
    // 2. 逐条处理
    int successCount = 0;
    int failedCount = 0;
    List<String> errors = new ArrayList<>();
    
    for (String xid : hangingXids) {
        try {
            // 调用业务方接口查询业务状态,确认是否可以安全处理
            boolean canResolve = businessService.checkHangingStatus(xid);
            
            if (canResolve) {
                jdbcTemplate.update(
                    "UPDATE tcc_fence_log SET status = 'RESOLVED' WHERE xid = ? AND status = 'CANCELLED'",
                    xid);
                successCount++;
            } else {
                failedCount++;
                errors.add(xid + ": 业务状态校验未通过,需人工判断");
            }
        } catch (Exception e) {
            failedCount++;
            errors.add(xid + ": " + e.getMessage());
        }
    }
    
    // 3. 审计
    recoveryAuditLogMapper.insert(RecoveryAuditLog.builder()
        .operator(getCurrentUser())
        .operationType("BATCH_RESOLVE_HANGING")
        .targetXid("BATCH:" + before.toString()) // 批量操作的 target_xid 使用批次标识
        .reason(reason)
        .result(failedCount == 0 ? "SUCCESS" : "PARTIAL_SUCCESS")
        .detail(String.format("成功处理 %d 条,失败 %d 条。失败详情:%s", 
            successCount, failedCount, String.join("; ", errors)))
        .build());
    
    return ApiResponse.success(BatchResult.builder()
        .successCount(successCount)
        .failedCount(failedCount)
        .errors(errors)
        .build());
}

5.5 操作审计日志设计

sql 复制代码
CREATE TABLE `recovery_audit_log` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '自增主键',
  `operator` VARCHAR(64) NOT NULL COMMENT '操作人(从认证信息获取)',
  `operator_ip` VARCHAR(45) DEFAULT NULL COMMENT '操作人IP地址',
  `operation_type` ENUM(
    'RETRY_CONFIRM',        -- 重试 TCC Confirm/Cancel
    'ROLLBACK',             -- 强制回滚全局事务
    'SKIP_DEAD',            -- 跳过/重投死信消息
    'RELEASE_LOCK',         -- 释放全局锁
    'BATCH_RESOLVE_HANGING' -- 批量处理悬挂事务
  ) NOT NULL COMMENT '操作类型',
  `target_xid` VARCHAR(128) DEFAULT NULL COMMENT '目标全局事务ID(批量操作使用批次标识)',
  `target_business_id` VARCHAR(128) DEFAULT NULL COMMENT '关联业务标识',
  `reason` VARCHAR(500) NOT NULL COMMENT '操作原因(操作人填写)',
  `result` ENUM('SUCCESS', 'FAILED', 'PARTIAL_SUCCESS') NOT NULL COMMENT '操作结果',
  `detail` TEXT COMMENT '操作详情(成功时的摘要或失败时的错误信息)',
  `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间',
  
  INDEX `idx_operator` (`operator`),
  INDEX `idx_target_xid` (`target_xid`),
  INDEX `idx_operation_type` (`operation_type`),
  INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='手动恢复操作审计日志表 - 合规追溯基础';

审计日志的设计价值

  1. 合规性:金融、支付等行业要求所有数据变更操作可追溯;
  2. 复盘依据:故障复盘时可以还原运维人员的操作序列,评估操作是否合理;
  3. 权限审计:可以定期审查谁在何时执行了多少手动操作,是否存在权限滥用;
  4. 操作指引:同一类故障的手动恢复操作可以被沉淀为 SOP(标准操作流程)。

5.6 手动恢复平台的操作流程图

flowchart TD Start["告警触发或人工发现异常"] --> Query["1. 事务查询
─────────────────
• 按 xid/businessId/status 检索
• 按时间范围过滤
• 分页浏览异常事务列表"] Query --> Select["2. 选择目标事务
─────────────────
点击 xid 进入详情页"] Select --> Detail["3. 事务详情展示
─────────────────
• 全局事务基本信息
• 全局时间线(甘特图)
• 各分支状态与耗时
• 辅助表数据联动
(undo_log/outbox/tcc_fence_log/saga_log)
• 错误堆栈信息"] Detail --> Decision{"4. 判断恢复策略
─────────────────
根据事务类型、状态、
辅助表数据分析根因
选择合适的手动操作"} Decision -->|"TCC Confirm/Cancel 失败
+ 网络抖动/幂等安全"| Op1["5a. 重试 Confirm/Cancel
─────────────────
• 调用 TC commit()/rollback()
• 幂等重试
• 更新 transaction_log"] Decision -->|"XA In-Doubt
或 AT COMMITTING 超时
+ 确认无业务副作用"| Op2["5b. 强制回滚全局事务
─────────────────
• XA: Atomikos 强制回滚
• AT: Seata TC 回滚 + 清理全局锁
• 二次确认 + 填写原因"] Decision -->|"outbox DEAD 消息
+ 业务可接受跳过"| Op3["5c. 跳过/重投死信
─────────────────
• 选择 RETRY 或 SKIP
• 更新 outbox 状态
• 若重投: 重置 retry_count"] Decision -->|"AT 全局锁残留
+ 事务已确认结束"| Op4["5d. 释放全局锁
─────────────────
• 校验事务已 FAILED/TIMEOUT
• 从 lock_table 删除
• 显示锁详情供确认"] Decision -->|"TCC 大量悬挂
+ 需要批量清理"| Op5["5e. 批量处理悬挂
─────────────────
• 查询悬挂事务列表
• 逐条业务状态校验
• 标记 RESOLVED
• 支持 dryRun 试运行"] Op1 & Op2 & Op3 & Op4 & Op5 --> Audit["6. 记录审计日志
─────────────────
recovery_audit_log 表
• operator / operationType
• targetXid / reason / result
• detail(操作详情)"] Audit --> Verify["7. 验证恢复结果
─────────────────
• 刷新事务详情页
• 确认状态已更新
• 检查辅助表数据一致性
• 观察监控指标恢复"] Verify --> End["恢复完成
故障复盘 + SOP 沉淀"] style Op1 fill:#e1f5fe style Op2 fill:#ffebee style Op3 fill:#fff3e0 style Op4 fill:#f3e5f5 style Op5 fill:#e8f5e9 style Audit fill:#fafafa,stroke:#333,stroke-width:2px

图 5-1 手动恢复平台操作流程图

  • 整体说明:该流程图覆盖从故障发现到恢复验证的完整闭环,7 个步骤层层递进,5 种操作各有明确的适用场景和安全校验。
  • 逐步骤说明
    • 步骤1-3:信息收集阶段。通过多条件查询定位异常事务,通过详情页获取完整的故障信息(时间线 + 辅助表 + 错误堆栈)。
    • 步骤4:决策阶段。这是整个流程中最关键的节点------运维人员需要综合事务类型、状态、辅助表数据来判断根因并选择恢复策略。
    • 步骤5a-5e:执行阶段。五种操作各自封装了对 TC、MQ、数据库的直接交互,并有前置校验和二次确认。
    • 步骤6 :审计阶段。所有操作无例外地写入 recovery_audit_log,确保可追溯。
    • 步骤7:验证阶段。操作完成后验证结果,确认系统恢复,沉淀故障处理 SOP。
  • 关联说明
    • 步骤5a(重试 Confirm/Cancel)依赖第 3 篇的 TCC 幂等设计;
    • 步骤5b(强制回滚)依赖第 1 篇的 XA In-Doubt 处理和第 2 篇的 AT 全局锁机制;
    • 步骤5c(处理死信)依赖第 5 篇的 outbox 表状态流转和第 6 篇的 CDC 投递链路;
    • 步骤5d(释放锁)直接操作第 2 篇的 lock_table
    • 步骤5e(批量处理悬挂)操作第 3 篇的 tcc_fence_log
  • 关键结论 :手动恢复平台的价值在于将"高风险直接改库操作"转变为"受控、可审计的 UI 操作"。每个操作都有前置校验(事务状态确认、业务影响评估)、二次确认(confirmed 参数)和事后审计,最大程度降低人为失误的风险。

6. Micrometer 自定义指标埋点方案:TM/RM/TC 代码示例

Micrometer 作为 Spring Boot 生态的标准指标库,提供了 CounterTimerGaugeDistributionSummary 等丰富的指标类型。本节给出 TM、RM、TC 三个关键节点的完整埋点实现。

6.1 TM 侧:全局事务拦截器埋点

TM(Transaction Manager)侧负责启动全局事务、协调各 RM 完成业务逻辑。在 @GlobalTransactional AOP 中,需要记录事务耗时、结果、以及异常分类。

java 复制代码
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 1) // 确保在 Seata 切面外层
@Slf4j
public class GlobalTransactionMetricsAspect {

    private final MeterRegistry meterRegistry;
    private final TransactionLogMapper transactionLogMapper;
    
    // 缓存各方案的基线 P99(毫秒),用于 P2 告警判断
    private static final Map<String, Integer> BASELINE_P99 = Map.of(
        "XA", 500,
        "AT", 50,
        "TCC", 20,
        "SAGA", 200,
        "MQ", 5000
    );

    public GlobalTransactionMetricsAspect(MeterRegistry meterRegistry, 
                                           TransactionLogMapper transactionLogMapper) {
        this.meterRegistry = meterRegistry;
        this.transactionLogMapper = transactionLogMapper;
    }

    @Around("@annotation(io.seata.spring.annotation.GlobalTransactional)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // ========== 1. 提取方法信息 ==========
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String serviceName = getServiceName();           // 从应用配置获取
        String methodName = signature.getMethod().getName();
        
        // ========== 2. 获取或等待 Seata 的 xid ==========
        String xid = null;
        String transactionType = detectTransactionType(joinPoint); // XA/AT/TCC/Saga
        
        // ========== 3. 开始计时 ==========
        Timer.Sample sample = Timer.start(meterRegistry);
        long startTime = System.currentTimeMillis();
        
        String finalStatus = "UNKNOWN";
        String failureCategory = "NONE"; // BUSINESS / SYSTEM / TIMEOUT
        
        try {
            // ========== 4. 执行业务逻辑 ==========
            Object result = joinPoint.proceed();
            
            // 正常完成,TC 会自动提交
            finalStatus = "COMMITTED";
            return result;
            
        } catch (Exception e) {
            // ========== 5. 异常分类 ==========
            // 需要根据实际异常类型细分
            if (e instanceof LockConflictException) {
                finalStatus = "ROLLBACKED";
                failureCategory = "LOCK_CONFLICT";
            } else if (isTimeoutException(e)) {
                finalStatus = "TIMEOUT";
                failureCategory = "TIMEOUT";
            } else if (isRollbackFailure(e)) {
                finalStatus = "FAILED";
                failureCategory = "ROLLBACK_FAILURE";
            } else {
                finalStatus = "ROLLBACKED";
                failureCategory = "BUSINESS";
            }
            throw e; // 让上层感知异常
            
        } finally {
            // ========== 6. 记录指标(无论成功失败都执行) ==========
            long duration = System.currentTimeMillis() - startTime;
            
            // 6.1 记录事务耗时 Timer(自动生成 _count, _sum, _bucket, _max)
            sample.stop(Timer.builder("transaction.duration")
                .description("Global transaction end-to-end duration")
                .tags("type", transactionType,
                      "status", finalStatus,
                      "service", serviceName,
                      "method", methodName)
                .publishPercentiles(0.5, 0.9, 0.99) // 预计算分位数
                .publishPercentileHistogram(true)    // 启用 Histogram
                .minimumExpectedValue(Duration.ofMillis(1))
                .maximumExpectedValue(Duration.ofSeconds(30))
                .register(meterRegistry));
            
            // 6.2 记录事务计数 Counter
            Counter.builder("transaction.total")
                .description("Total global transactions")
                .tags("type", transactionType,
                      "status", finalStatus,
                      "failure_category", failureCategory,
                      "service", serviceName)
                .register(meterRegistry)
                .increment();
            
            // 6.3 成功提交的单独计数(方便计算成功率)
            if ("COMMITTED".equals(finalStatus)) {
                Counter.builder("transaction.committed")
                    .tags("type", transactionType, "service", serviceName)
                    .register(meterRegistry)
                    .increment();
            }
            
            // 6.4 回滚失败的单独计数(需要人工介入)
            if ("ROLLBACK_FAILURE".equals(failureCategory)) {
                Counter.builder("transaction.rollback.failed")
                    .tags("type", transactionType, "service", serviceName)
                    .register(meterRegistry)
                    .increment();
            }
            
            // 6.5 记录超时事务
            if ("TIMEOUT".equals(finalStatus)) {
                Counter.builder("transaction.timeout")
                    .tags("type", transactionType, "service", serviceName)
                    .register(meterRegistry)
                    .increment();
            }
        }
    }
    
    /**
     * 判断事务类型
     */
    private String detectTransactionType(ProceedingJoinPoint joinPoint) {
        GlobalTransactional annotation = 
            ((MethodSignature) joinPoint.getSignature()).getMethod()
                .getAnnotation(GlobalTransactional.class);
        // 可根据注解属性或上下文判断
        // 这里简化:从 Spring 环境变量获取默认类型
        return environment.getProperty("seata.tx-type", "AT").toUpperCase();
    }
    
    private boolean isTimeoutException(Exception e) {
        return e.getMessage() != null && 
            (e.getMessage().contains("timeout") || 
             e.getMessage().contains("Timeout"));
    }
    
    private boolean isRollbackFailure(Exception e) {
        return e.getMessage() != null && 
            e.getMessage().contains("rollback failed");
    }
}

设计意图解读

  1. Timer 而非 DistributionSummaryTimer 专为记录耗时设计,自动生成 _count_sum_bucket 指标,这些是 histogram_quantile() 函数计算 P99 的基础数据。

  2. 多维度 Tagstype(方案类型)、status(最终状态)、service(服务名)提供了丰富的聚合维度。但需注意 Tags 基数不能过高 ------method 标签如果方法名太多会导致指标爆炸。生产环境可考虑仅保留 type + status + service

  3. publishPercentileHistogram(true) :启用客户端预计算 Histogram,增加存储开销但避免 Prometheus 查询时对 _bucket 的估算误差。

  4. 异常分类 :区分业务异常(BUSINESS)、锁冲突(LOCK_CONFLICT)、超时(TIMEOUT)和回滚失败(ROLLBACK_FAILURE),后两者需要不同的告警策略。

6.2 RM 侧:Seata AT Phase 1 耗时埋点

RM(Resource Manager)负责执行分支事务。在 AT 模式下,Phase 1 包括执行本地业务 SQL + 保存 undo_log + 向 TC 注册分支,这是事务耗时的主要来源。

java 复制代码
@Component
@Slf4j
public class SeataDataSourceMetricsWrapper {

    private final MeterRegistry meterRegistry;

    public SeataDataSourceMetricsWrapper(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    /**
     * 包装 DataSourceProxy,在 Connection.commit() 时记录 Phase 1 耗时
     * 
     * 方案:使用 BeanPostProcessor 对 Seata 的 DataSourceProxy 进行代理增强
     */
    @Bean
    public BeanPostProcessor seataDataSourceMetricsPostProcessor() {
        return new BeanPostProcessor() {
            @Override
            public Object postProcessAfterInitialization(Object bean, String beanName) 
                    throws BeansException {
                if (bean instanceof DataSourceProxy) {
                    return createMetricsProxy((DataSourceProxy) bean, beanName);
                }
                return bean;
            }
        };
    }

    private DataSourceProxy createMetricsProxy(DataSourceProxy target, String beanName) {
        // 使用 CGLIB 或 JDK Proxy 增强 getConnection()
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(DataSourceProxy.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
            Object result = proxy.invokeSuper(obj, args);
            
            // 如果是 getConnection(),返回包装后的 Connection
            if ("getConnection".equals(method.getName()) && result instanceof ConnectionProxy) {
                return wrapConnection((ConnectionProxy) result, beanName);
            }
            return result;
        });
        return (DataSourceProxy) enhancer.create();
    }

    private Connection wrapConnection(ConnectionProxy connection, String dataSourceName) {
        return (Connection) Proxy.newProxyInstance(
            Connection.class.getClassLoader(),
            new Class[]{Connection.class},
            (proxy, method, args) -> {
                if ("commit".equals(method.getName())) {
                    // ========== Phase 1 耗时埋点 ==========
                    Timer.Sample phase1Sample = Timer.start(meterRegistry);
                    try {
                        Object result = method.invoke(connection, args);
                        // 记录 Phase 1 耗时(包含 undo_log 写入和 TC 注册)
                        phase1Sample.stop(Timer.builder("seata.at.phase1.duration")
                            .description("Seata AT Phase 1 duration (local commit + undo_log + TC register)")
                            .tag("datasource", dataSourceName)
                            .publishPercentiles(0.5, 0.95, 0.99)
                            .register(meterRegistry));
                        
                        // 记录 Phase 1 计数
                        Counter.builder("seata.at.phase1.total")
                            .tag("datasource", dataSourceName)
                            .tag("status", "success")
                            .register(meterRegistry)
                            .increment();
                        
                        return result;
                    } catch (Exception e) {
                        // Phase 1 失败计数
                        Counter.builder("seata.at.phase1.total")
                            .tag("datasource", dataSourceName)
                            .tag("status", "failed")
                            .register(meterRegistry)
                            .increment();
                        throw e;
                    }
                }
                return method.invoke(connection, args);
            });
    }
}

设计要点

  • Phase 1 是 AT 事务耗时的核心部分,其耗时 = 业务 SQL 执行时间 + undo_log 写入时间 + TC 注册分支时间;
  • datasource 标签分组可以区分不同数据库的性能差异;
  • 此埋点对业务代码完全无侵入。

6.3 TC 侧:Seata Server 全局状态暴露

TC(Transaction Coordinator)作为全局事务的协调者,需要通过 Gauge 暴露内部状态。

java 复制代码
@Component
public class SeataTcMetricsExporter {

    private final MeterRegistry meterRegistry;
    private final JdbcTemplate jdbcTemplate;  // 直连 Seata Server 的数据库

    public SeataTcMetricsExporter(MeterRegistry meterRegistry, 
                                   @Qualifier("seataJdbcTemplate") JdbcTemplate jdbcTemplate) {
        this.meterRegistry = meterRegistry;
        this.jdbcTemplate = jdbcTemplate;
        
        // 注册所有 Gauge
        registerGauges();
    }

    private void registerGauges() {
        // ========== 1. 活跃全局事务数 ==========
        Gauge.builder("seata.tc.active.transactions", () -> {
            // 查询 global_table 中状态为 Begin 的事务数
            return jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM global_table WHERE status = 1", // 1 = Begin
                Integer.class);
        }).description("Seata TC 当前活跃的全局事务数")
          .register(meterRegistry);

        // ========== 2. 全局锁数量 ==========
        Gauge.builder("seata.tc.locks.total", () -> {
            return jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM lock_table", 
                Integer.class);
        }).description("Seata TC 当前持有的全局锁数量")
          .register(meterRegistry);

        // ========== 3. 分支事务数 ==========
        Gauge.builder("seata.tc.branches.total", () -> {
            return jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM branch_table", 
                Integer.class);
        }).description("Seata TC 记录的分支事务总数")
          .register(meterRegistry);

        // ========== 4. In-Doubt 事务数(XA 模式) ==========
        Gauge.builder("seata.tc.indoubt.transactions", () -> {
            // XA 模式下,branch_table 中状态为 PhaseOne_Done 但无后续状态的分支
            return jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM branch_table WHERE status = 1", // 1 = PhaseOne_Done
                Integer.class);
        }).description("Seata TC XA 模式 In-Doubt 事务数")
          .register(meterRegistry);

        // ========== 5. undo_log 行数汇总 ==========
        Gauge.builder("seata.tc.undo_log.rows", () -> {
            return jdbcTemplate.queryForObject(
                "SELECT COUNT(*) FROM undo_log", 
                Long.class);
        }).description("Seata undo_log 表总行数")
          .register(meterRegistry);
    }
}

设计要点

  • Gauge 的值在 Prometheus 每次拉取时实时计算,适合监控变化量(如表行数);
  • 直接查询 Seata Server 的数据库表,不需要 Seata Server 本身集成 Micrometer;
  • 查询频率不宜过高(Prometheus 默认 15-30s 拉取一次),对数据库压力可控。

6.4 RocketMQ 消费者侧埋点

java 复制代码
@Component
@Slf4j
public class MonitoredMessageListener implements RocketMQListener<MessageExt> {

    private final MeterRegistry meterRegistry;

    public MonitoredMessageListener(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Override
    public void onMessage(MessageExt message) {
        String topic = message.getTopic();
        String msgId = message.getMsgId();
        
        // ========== 开始计时 ==========
        Timer.Sample sample = Timer.start(meterRegistry);
        boolean success = false;
        
        try {
            // ========== 业务处理 ==========
            processMessage(message);
            success = true;
            
        } catch (Exception e) {
            // ========== 失败计数 ==========
            Counter.builder("rocketmq.consume.failed")
                .tag("topic", topic)
                .tag("exception", e.getClass().getSimpleName())
                .register(meterRegistry)
                .increment();
            
            log.error("消息消费失败: topic={}, msgId={}", topic, msgId, e);
            throw e; // 抛出异常触发 RocketMQ 重试
            
        } finally {
            // ========== 记录耗时 ==========
            sample.stop(Timer.builder("rocketmq.consume.duration")
                .description("RocketMQ message consume duration")
                .tag("topic", topic)
                .tag("status", success ? "success" : "failed")
                .publishPercentiles(0.5, 0.95, 0.99)
                .register(meterRegistry));
            
            // ========== 记录计数 ==========
            Counter.builder("rocketmq.consume.total")
                .tag("topic", topic)
                .tag("status", success ? "success" : "failed")
                .register(meterRegistry)
                .increment();
        }
    }
    
    private void processMessage(MessageExt message) {
        // 实际业务逻辑
    }
}

6.5 Prometheus 配置(prometheus.yml)

yaml 复制代码
# prometheus.yml
global:
  scrape_interval: 15s      # 全局拉取间隔
  evaluation_interval: 15s  # 告警规则评估间隔

scrape_configs:
  # ========== Spring Boot 应用(TM/RM) ==========
  - job_name: 'spring-boot-apps'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets:
          - 'order-service:8081'
          - 'inventory-service:8082'
          - 'payment-service:8083'
    relabel_configs:
      - source_labels: [__address__]
        target_label: instance
        regex: '([^:]+):\d+'
        replacement: '${1}'

  # ========== Seata TC Server ==========
  - job_name: 'seata-server'
    metrics_path: '/actuator/prometheus'
    static_configs:
      - targets: ['seata-server:7091']

  # ========== Debezium (通过 JMX Exporter) ==========
  - job_name: 'debezium'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['debezium-connect:8083']

  # ========== RocketMQ Exporter ==========
  - job_name: 'rocketmq'
    metrics_path: '/metrics'
    static_configs:
      - targets: ['rocketmq-exporter:5557']

# ========== 告警规则文件 ==========
rule_files:
  - '/etc/prometheus/rules/distributed-transaction.yml'

相关推荐
逆境不可逃2 小时前
Hello-Agents 第二部分-第四章总结:智能体经典范式构建-包含习题解析和Java版
java·开发语言·javascript·人工智能·分布式·agent
heimeiyingwang2 小时前
【架构实战】RocketMQ实战:分布式消息中间件
分布式·架构·rocketmq
报错小能手2 小时前
分布式讲解—分布式事务解决方案 刚性(2PC、3PC、XA协议)
分布式
Evand J1 天前
【MATLAB例程】5个UAV 分布式围捕编队运动仿真 —— 基于PID控制
开发语言·分布式·matlab
蓝眸少年CY1 天前
Spark - Code 核心教程
大数据·分布式·spark
敖正炀1 天前
CAP 定理、BASE 理论与一致性模型深度
分布式
勤自省1 天前
ROS2分布式通信与Launch文件实战:从踩坑到打通(第12-20讲总结)
分布式·ubuntu·ros2·gazebo·launch·rqt·rviz2
qq_452396232 天前
第十三篇:《分布式压测:JMeter Master-Slave集群》
分布式·jmeter
小英雄大肚腩丶2 天前
RabbitMQ消息队列
java·数据结构·spring boot·分布式·rabbitmq·java-rabbitmq