SEATA:Server 到 Golang Client 全链路走读

SEATA 在解决什么问题?

微服务拆分后,一个业务操作往往需要跨多个服务、多个数据库。拿下单操作举个例子:扣减库存、扣减余额、创建订单几个动作分布在不同的服务下,任何一步失败,前面已经执行完成的操作都需要撤回。这就是分布式事务需要解决的核心问题。

SEATA 的解决方式如下:

  • TC(Transaction Coordinator) :Seata Server,Java实现(Seata Go 只是 Seata 的 Golang Client)。负责维护全局事务和分支事务的状态,做最终的提交和回滚决策。
  • TM(Transaction Manager) :事务发起方。在 Go 生态下就是调用 tm.WithGlobalTx() 的服务。
  • RM(Resource Manager) :资源管理器,每个参与分布式事务的服务下都有这个角色。负责管理本地事务,向 TC 注册事务分支,执行 TC 下发的 Commit/Rollback 指令。

Golang Client 启动

如果阅读过 incubator-seata-go-samples 仓库,应该可以看到每个 sample 的第一行几乎都是 client.InitPath("../../conf/seatago.yml") 。我们先看看这一行代码背后都发生了什么事情。

详细介绍下几个关键节点:

  1. 服务发现:Go Client 需要知道 TC Server 在哪里,服务发现需要先行。
  2. RM 初始化 :这是其中最重的部分,需要建立到 TC 的 TCP 长连接(基于 dubbo-getty),注册消息处理器来接收 TC 的回调(Commit/Rollback 指令),还需要初始化四种事务模式各自的资源管理器。
  3. 数据源代理 :这是 AT/XA 模式的基础,通过 Golang 的 database/sql/driver 接口注册自定义 Driver,所有 SQL 语句都会经过 Seata 的代理层。

WithGlobalTx 全流程

这应该算是 Seata Go 最重要的 API,贯穿一个全局事务的生命周期。

事务传播机制

WithGlobalTx 支持类似 Spring 的事务传播语义。当一个全局事务内部又调用了 WithGlobalTx 时,其传播级别决定了行为:

传播级别 已有事务 无事务
Required(Default) 加入当前事务 新建事务
RequiresNew 挂起当前,新建事务 新建事务
NotSupported 挂起当前,无事务执行 无事务执行
Supports 加入当前事务 无事务执行
Never 抛异常 无事务执行
Mandatory 加入当前事务 抛异常

角色区分:Launcher & Participant

发起全局事务的服务是 Launcher ,被调用的下游服务是 Participant。只有 Launcher 才有权提交/回滚全局事务,Participant 只负责执行本地分支。这个区分在 commitOrRollback 中语义非常清晰,即 Participant 直接跳过二阶段操作。

XID 传播

XID 作为全局事务的唯一标识,格式类似 192.168.1.1:8091:123456789 。它必须在整个调用链中传递,否则下游事务将无法明确自己属于哪个全局事务。

Seata Go 通过中间件/拦截器的方式实现 XID 的透明传播。

业务代码完全不需要关注 XID 的传递。只需要引入对应框架的中间件,XID 会自动跟着请求走。Gin 走 HTTP Header,gRPC 用 Metadata,Dubbo 用 Attachment,但最终都汇入 Seata Context。

AT 模式

AT(Automatic Transaction)是 Seata 最常用的模式,也是业务侵入最小的模式。它的核心思想是在执行 SQL 前后自动记录数据快照(UndoLog),回滚时用快照恢复数据。

SQL 代理层:通过实现 Go 标准库的 database/sql/driver 接口,注册自定义的 Driver。应用使用 sql.Open("seata-at", dsn) 时所有 SQL 都经过 ATConn 代理。代理层根据 SQL 类型(INSERT/UPDATE/DELETE/SELECT FOR UPDATE)选择不同的 Executor。

UndoLog 结构:每条 undo log 都包含 beforeImage 和 afterImage。beforeImage 是执行 SQL 前的数据快照,afterImage 是执行后的。回滚的时候用 beforeImage 生成反向 SQL(UPDATE 变回原值,INSERT 变 DELETE,DELETE 变 INSERT)。

全局锁:AT 模式在注册分支时会向 TC 申请行锁(lock keys),防止多个全局事务同时修改同一行数据,是 AT 模式保证隔离性的关键机制。

异步提交:二阶段提交时,AT 模式只需要删除 undo log,不需要做任何业务操作,所以 TC 可以异步批量处理。

TCC 模式

TCC(Try-Confirm-Cancel)把事务的控制权完全交还给业务。开发者为每个参与方实现三个方法:Try(预留资源)、Confirm(确认提交)、Cancel(取消回滚)。

使用 TCC 时,需要实现下面接口方法:

方法 作用 调用时机
Prepare(ctx, params) 预留资源(如冻结库存) 一阶段,业务主动调用
Commit(ctx, bac) 确认提交(如扣减冻结库存) 二阶段,TC 驱动
Rollback(ctx, bac) 取消回滚(如释放冻结库存) 二阶段,TC 驱动
GetActionName() 返回资源标识 注册时使用

然后通过 tcc.NewTCCServiceProxy(service) 创建代理,代理会自动向 TC 注册资源。业务代码只需要在全局事务内调用 proxy.Prepare(),二阶段的 Confirm/Cancel 由 TC 回调触发。

经典问题:如果 Try 请求因为网络延迟还没到达,TC 已经超时触发了 Cancel,之后 Try 请求才到达然后执行,也就是悬挂问题。Seata Go 提供了 Fence 机制来解决,在数据库中维护一张 tcc_fence_log 表,记录每个分支事务的阶段状态,通过本地事务保证幂等性和防止悬挂。

XA 模式

XA 是 DB 层面的分布式事务标准协议,与 AT 模式不同,XA 模式不需要 undo log,依赖 DB 自身的 XA 事务支持。

简单对比 AT 和 XA 模式:

维度 AT 模式 XA 模式
锁粒度 全局锁(Seata 管理) 数据库锁(DB 管理)
一阶段锁释放 本地事务提交后释放 DB 锁 Prepare 后仍持有 DB 锁
一致性 最终一致 强一致
回滚方式 undo log 反向补偿 数据库原生 XA ROLLBACK

简单来讲,AT 牺牲了一点一致性换来换取高性能,它在一阶段时就释放 DB 锁。相反 XA 牺牲性能换来强一致性,锁会一直持有到二阶段结束。

Saga 模式

Saga 适用于长事务场景,通过状态机来编排一系列正向操作和补偿操作。

Seata Go 实现了完整的状态机引擎 ProcessCtrlStateMachineEngine 。

流程定义用 JSON 格式的 statelang,类似 AWS Step Functions。

状态类型 作用
ServiceTask 调用一个服务方法,可配置补偿方法
Choice 条件分支,根据表达式选择下一步
CompensationTrigger 触发已执行状态的补偿
Succeed / Fail 终态
SubStateMachine 嵌套调用另一个状态机

TC Server

前面我们讲完了 Go Client,下面看看 Seata Server 是怎么处理这些请求的。

DefaultCoordinator 是 TC Server 入口,处理所有来自 TM/RM 的 RPC 请求。同时管理一组定时任务:

定时任务 职责
retryRollbacking 重试回滚中的事务
retryCommitting 重试提交中的事务
asyncCommitting 处理异步提交(AT 模式)
timeoutCheck 检测超时事务并触发回滚
undoLogDelete 清理过期的 undo log

DefaultCore 为事务处理核心,内部维护了一个 CORE_MAP,按 BranchType 映射到不同的 AbstractCore 实现(ATCore、TCCCore、XACore、SagaCore)。所有分支级别的操作(register、commit、rollback)委托给对应模式的 Core 进行处理。

全局事务状态流转

  1. 异步提交:如果所有分支都支持异步提交(AT 模式的二阶段只需删除 undo log),TC 会直接标记为 AsyncCommitting,由后台定时任务批量处理,提升吞吐量。
  2. 超时:TC 有专门的定时任务扫描超时事务。一旦发现超时,直接将状态改为 TimeoutRollbacking,触发回滚流程。
  3. Raft 集群:TC 支持 Raft 模式部署,通过 RaftCoordinator 实现 leader 选举和状态同步,保证 TC 自身的高可用。

RPC 通信

Go Client 和 TC Server 之间的通信基于 TCP 长连接,Go 端使用 dubbo-getty。

通信协议的消息类型覆盖了所有事务操作:

消息方向 消息类型 说明
Client → TC GlobalBeginRequest TM 开启全局事务
Client → TC GlobalCommitRequest TM 提交全局事务
Client → TC GlobalRollbackRequest TM 回滚全局事务
Client → TC BranchRegisterRequest RM 注册分支事务
Client → TC BranchReportRequest RM 报告分支状态
Client → TC GlobalLockQueryRequest RM 查询全局锁
Client → TC RegisterRMRequest RM 注册资源
TC → Client BranchCommitRequest TC 驱动分支提交
TC → Client BranchRollbackRequest TC 驱动分支回滚

Client 端的消息处理器在初始化时注册:rmBranchCommitProcessor 处理提交回调,rmBranchRollbackProcessor 处理回滚回调。收到 TC 的指令后,处理器会根据 BranchType 找到对应的 ResourceManager,执行实际的 Commit/Rollback,然后将结果通过 Response 返回给 TC。

完整请求链路

最后,通过一个请求的完整处理链路串联上述所有内容:

相关推荐
超人气王1 小时前
新手学前端JS浅拷贝和深拷贝:对象复制竟然是个“替身文学”?
javascript·面试
Aphasia3112 小时前
从内存模型看深浅拷贝
前端·javascript·面试
嵌入式ZYXC2 小时前
第1篇:《面试题:画一个STM32最小系统电路,每个元件的作用》
stm32·单片机·嵌入式硬件·面试·职场和发展
嵌入式ZYXC4 小时前
第2篇:《面试题:LDO和DC-DC的区别?分别用在什么场景?》
stm32·单片机·嵌入式硬件·面试·职场和发展
小江的记录本4 小时前
【Spring全家桶】Spring Cloud 2023.0.x:微服务核心理论、CAP/BASE定理(附《思维导图》+《面试高频考点清单》)
java·spring boot·后端·spring·spring cloud·微服务·面试
胡萝卜术6 小时前
从零搭建 NLP Demo:用 ES6 模块化 + DeepSeek API 构建你的第一个 AI 应用
javascript·面试
Raink老师6 小时前
【AI面试临阵磨枪-99】纯浏览器 Agent:记忆、工具、RAG、流式、安全如何实现?
人工智能·安全·面试
Oo_行者_oO7 小时前
Spring Authorization Server 下 Token 刷新流程自定义实现
后端·面试
lcj25117 小时前
【list】【手撕 STL】List 容器全解析!迭代器 / 增删改查 / 去重排序,面试必背的核心考点!
c++·面试·list