概述
系列定位说明
本文是"分布式事务工程实践"系列的第七篇。前六篇我们沿着分布式事务的技术演进路径,从 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为主键,记录全局事务的完整生命周期(状态变迁、时间戳、错误信息),并通过branchesJSON 字段和payloadJSON 字段存储分支事务详情与业务上下文。与各方案辅助表形成星型关联。- Micrometer 埋点方案 :在 TM 侧通过
@GlobalTransactionalAOP 拦截器记录事务耗时与结果;在 RM 侧记录 Phase 1/Phase 2 耗时;在 TC 侧暴露活跃事务数、In-Doubt 数等 Gauge 指标;在消息消费者侧记录消费耗时与失败计数。
文章组织架构图
全局指标 + 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 到最终状态(COMMITTED 或 ROLLBACKED)的端到端耗时分布。
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 活跃事务数
定义 :当前时刻处于中间状态(BEGIN、COMMITTING、ROLLBACKING)尚未完结的全局事务数量。
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_commit 或 xa_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 级告警。原因:
- In-Doubt 事务持有数据库锁,会导致所有涉及相同数据行的写操作被阻塞;
- 长时间不处理可能导致锁等待超时,引发连锁故障;
- 恢复时需要人工判断是提交还是回滚,决策错误可能造成数据不一致。
关联前文(第 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_rows、seata_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_seconds、seata_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_log 中 status = '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_seconds、tcc_confirm_duration_seconds、tcc_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_total:saga_transaction_log中status = 'COMPENSATION_FAILED'的记录数;saga_retry_exhausted_total:补偿重试次数达到上限(retry_count >= maxRetry)且最终状态为FAILED的事务数。
Prometheus 数据类型 :Counter(saga_compensation_failed_total、saga_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 分布式事务监控指标体系全景图
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_total、dead_message_total、tcc_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 节)和辅助表数据。
面板设计:
顶部:事务检索区
- 输入框:
xid或businessId - 可选过滤器:
transactionType、status、时间范围 - Grafana 通过 Variable 实现动态查询:
SELECT xid FROM transaction_log WHERE $__timeFilter(start_time) ORDER BY start_time DESC LIMIT 100
中部:全局事务时间线(甘特图或瀑布图)
使用 Grafana 的 State Timeline 或 Bar 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 节),展示:
- AT :
undo_log的rollback_info(回滚镜像数据)、lock_table的锁持有情况 - TCC :
tcc_fence_log的完整记录(Try / Confirm / Cancel 的状态和时间) - Saga :
saga_transaction_log的每一步状态机执行记录 - 消息 :
outbox的消息内容、重试次数、当前状态 - 错误堆栈 :
transaction_log.error_message字段内容
Data Link 跳转 :配置 Grafana 的 Data Link,点击 xid 可以跳转到 ELK 或 Loki 中搜索该 xid 的所有日志。
2.4 Grafana 三层监控面板架构图
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 告警分级规则决策树图
或 成功率 < 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 设计要点解读:
-
xid为主键:全局事务 ID 天然具备唯一性,适合作为主键。对于非 Seata 方案(如纯 Saga 或纯消息),需要自定义 ID 生成器确保全局唯一。 -
branches使用 JSON 而非行记录 :一个全局事务可能有 1-N 个分支,行记录方式需要额外的transaction_branch表,查询时需要 JOIN,增加了复杂度。JSON 字段自包含,一次查询即可获取完整信息。MySQL 5.7+ 的 JSON 类型支持虚拟列索引:sqlALTER 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); -
payload字段的业务价值:手动恢复时,运维人员需要知道"这个事务涉及哪些业务对象"。例如处理一个 TCC 悬挂事务,需要知道是哪个用户的资金操作、金额是多少,才能判断是否可以安全回滚。 -
retry_count与error_message分离 :retry_count用于告警阈值判断(如重试 >5 次触发 P2),error_message用于故障分析(存储完整堆栈)。
4.2 写入时机与逻辑
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);
}
}
清理策略设计要点:
- 仅清理
COMMITTED和ROLLBACKED状态,保留FAILED和TIMEOUT用于长期复盘; - 分批删除避免大事务锁表;
- 失败记录可以设定更长的保留期(如 90 天),或定期导出到离线存储(如 HDFS 或 S3)。
4.5 transaction_log 表与各辅助表的关联 ER 图
图 4-1 transaction_log 表与各辅助表的关联 ER 图
- 整体说明 :该 ER 图展示了
transaction_log作为中心枢纽与四大辅助表(undo_log、outbox、tcc_fence_log、saga_transaction_log)以及recovery_audit_log审计表的关联关系。箭头方向表示"可追溯"的查询路径。 - 逐表说明 :
- transaction_log :核心日志表,记录全局事务从生到死的完整生命周期。
branchesJSON 字段自包含分支详情,减少跨表 JOIN。 - undo_log :Seata AT 模式的行级回滚日志,记录数据修改前镜像。通过
xid关联到全局事务。 - outbox :本地消息表 / 事务发件箱,记录待发送消息。通过
business_id(或aggregate_id)与全局事务间接关联------因为一条业务操作可能产生多条消息。 - tcc_fence_log :TCC 防悬挂表,记录 Try/Confirm/Cancel 各阶段的执行状态。通过
xid直接关联。 - saga_transaction_log :Saga 状态机执行日志,记录每个步骤的正向操作和补偿操作。通过
xid或saga_id关联。 - recovery_audit_log :手动恢复操作的审计日志,通过
target_xid与全局事务关联,实现"谁在何时对此事务做了何种操作"的完整追溯。
- transaction_log :核心日志表,记录全局事务从生到死的完整生命周期。
- 关联说明 :
- 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:记录所有针对该事务的手动操作,保障合规审计。
- transaction_log ↔ undo_log :AT 模式手动恢复时,需查看
- 关键结论 :这张 ER 图体现了"一个全局事务、多个辅助表"的数据组织方式。在手动恢复平台中,一次 xid 查询可以拉出该事务在所有方案层的完整证据链 ------从全局时间线(
transaction_log.branches)到 AT 的回滚镜像(undo_log.rollback_info)、TCC 的防悬挂记录(tcc_fence_log)、消息的发件箱状态(outbox)、Saga 的补偿历史(saga_transaction_log)。这种设计将故障定位时间从"逐个表翻找"压缩到"一次查询全景呈现"。
5. 手动恢复平台功能设计:查询、详情、操作、审计
当告警响起,运维人员需要的不是"SQL 客户端",而是一个安全、受控、可追溯的 Admin 后台。本节设计手动恢复平台的完整功能架构。
5.1 平台架构总览
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 调用的
operator、URI、参数、响应状态码,作为审计的辅助数据)。 - 核心服务层 :拆分为三个独立模块。事务查询服务 构建动态 SQL,从
transaction_log表中检索事务列表,对error_message进行截断处理以节省带宽。事务详情服务 是平台的核心------它根据xid和transaction_type决策需要查询哪些辅助表,构建完整时间线,并将branchesJSON 与辅助表记录交叉验证。手动操作服务是所有变更操作的唯一入口,执行固定的操作流程:前置校验 → 风险评估 → 二次确认 → 执行 → 审计写入。 - 数据访问层 :封装对所有数据库和外部系统的调用。对
transaction_log的更新使用乐观锁(version字段)防止并发覆盖;对辅助表的查询使用只读事务或从库读取,避免影响业务主库;对 Seata TC 的调用通过其 RPC 接口(DefaultCore.commit(xid)),对 RocketMQ 的调用通过DefaultMQAdminExt查看死信队列或重置消费位点。 - 外部依赖层 :手动恢复操作的"执行器"。注意 Seata TC 和 Atomikos 的接口通常不直接暴露 HTTP,需要自定义一个薄薄的代理层(如一个 Spring Boot 管理端点)将 HTTP 请求转换为 TC 的 RPC 调用。对于 RocketMQ,平台直接连接 NameServer 获取 Admin 视图。业务服务(如订单查询接口)用于在处理悬挂事务时获取业务对象的真实状态,避免误操作。
安全设计要点:
- 所有写操作强制记录审计日志 ,采用
@Audited自定义注解 + AOP 实现,杜绝遗漏。 - 操作前二次确认 :后端返回需要确认的信息(如"将回滚事务 XXX,涉及订单 ORD123456,金额 999 元"),前端展示后要求用户再次提交
confirmed=true参数。 - 操作隔离:手动恢复平台部署在独立的网络区域,仅允许运维 VPN 访问,与业务流量完全隔离。
- 回滚操作:高风险操作(强制回滚、释放全局锁)要求操作者具有 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='手动恢复操作审计日志表 - 合规追溯基础';
审计日志的设计价值:
- 合规性:金融、支付等行业要求所有数据变更操作可追溯;
- 复盘依据:故障复盘时可以还原运维人员的操作序列,评估操作是否合理;
- 权限审计:可以定期审查谁在何时执行了多少手动操作,是否存在权限滥用;
- 操作指引:同一类故障的手动恢复操作可以被沉淀为 SOP(标准操作流程)。
5.6 手动恢复平台的操作流程图
─────────────────
• 按 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 生态的标准指标库,提供了 Counter、Timer、Gauge、DistributionSummary 等丰富的指标类型。本节给出 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");
}
}
设计意图解读:
-
Timer而非DistributionSummary:Timer专为记录耗时设计,自动生成_count、_sum、_bucket指标,这些是histogram_quantile()函数计算 P99 的基础数据。 -
多维度 Tags :
type(方案类型)、status(最终状态)、service(服务名)提供了丰富的聚合维度。但需注意 Tags 基数不能过高 ------method标签如果方法名太多会导致指标爆炸。生产环境可考虑仅保留type+status+service。 -
publishPercentileHistogram(true):启用客户端预计算 Histogram,增加存储开销但避免 Prometheus 查询时对_bucket的估算误差。 -
异常分类 :区分业务异常(
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'