ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Seata分布式事务实战:TC、TM、RM角色与AT模式落地

Seata分布式事务实战:TC、TM、RM角色与AT模式落地 做分布式系统绕不开分布式事务而 Seata 几乎是我能给出的最直接答案。先说我自己的判断Seata 是一个开源的分布式事务解决方案核心由 TC、TM、RM 三个角色一起配合解决微服务架构下“多个库、多个服务之间数据状态不一致”的问题。它适合正在做 Spring Cloud、Dubbo 或纯 Spring Boot 微服务改造的团队也适合那些已经被跨库事务折磨过、准备引入成熟方案的人。这篇文章我会从问题本身讲起把 TC、TM、RM 的角色拆开揉碎再带着你走一遍落地配置和排障过程希望能给你一份可以直接抄作业的参考。1. 分布式事务到底是什么先用大白话讲清楚问题1.1 从一次跨服务下单说起想象一个最简单的下单场景用户下单后要扣库存、要扣余额、还要写订单。传统单应用时代这三个动作在同一个数据库里用Transactional包一层就行要么全成功要么全回滚不存在中间状态。但微服务化之后订单服务、库存服务、账户服务各自拥有独立的数据库。你在订单服务里开启事务只能保证订单库的一致库存扣减在库存服务里它自己也有独立事务。此时发生“订单创建成功库存扣减失败”的情况是任何一个本地事务都管不到的。这就是跨服务事务问题它本质上是把原本一个数据库里的 ACID拆到了多个数据库、多个服务之间。Seata 要解决的就是这件事。它不强求你把所有数据都搬到一个库里也不需要你为了事务去改掉微服务拆分而是通过一套协调机制把分散在多个服务里的“本地事务”串成一个“全局事务”。1.2 为什么传统的本地事务撑不住本地事务依赖数据库本身的能力核心是 ACID。但跨服务之后面临几个现实问题无法跨库持有锁。你在订单库开启事务库存库的事务并不受你控制。网络调用天然不可靠。服务 A 调用服务 B结果可能超时、失败或者结果返回“失败”但实际“成功”。数据源分散。每个服务的数据源不同Spring 的声明式事务只能管到当前线程里的同一个数据源。所以解决分布式事务的思路不是去“改进本地事务”而是引入一个独立的协调者把多个本地事务纳入同一个全局事务的决策范围。Seata 做的事情本质上就是“找一个全局的裁判记录每个人做了什么最后统一说全部提交或者全部回滚”。2. 认识 Seata 的三大核心角色TM、RM 与 TC2.1 TC事务协调器全局事务的“调度中心”TC 是 Transaction Coordinator事务协调器。它通常是独立部署的 Seata Server也是整个分布式事务的大脑。TC 负责全局事务和分支事务的状态记录谁开启了事务、这个全局事务下有哪些分支事务、每个分支事务当前处于什么状态全部由 TC 统一维护。TC 不直接参与业务数据操作它更像是全局事务的“状态机”。在实际部署中TC 需要注册到服务发现中心比如 Nacos、Eureka、Consul业务服务通过配置的事务分组来找到 TC 地址。TC 还负责在全局事务提交或者回滚时向所有相关的 RM 下发指令。2.2 TM事务管理器全局事务的“发起人”TM 是 Transaction Manager事务管理器。它的职责是定义全局事务的边界开始一个全局事务、提交或回滚整个全局事务。TM 通常嵌入在业务代码中通过GlobalTransactional注解就能触发。你可以把 TM 理解成“全局事务的发起人”。当某个业务方法被标注为全局事务时TM 会先向 TC 注册一个全局事务拿到全局事务 XID方法执行成功后再由 TM 通知 TC 提交如果方法抛异常TM 则通知 TC 回滚。它只负责“启动、提交、回滚”这三个动作不关心底层的分支事务是怎么做的。2.3 RM资源管理器分支事务的“执行者”RM 是 Resource Manager资源管理器。它管理的是参与者所连接的数据库资源负责执行分支事务的提交与回滚。RM 一般也嵌入在业务服务中当业务执行 SQL 时RM 会向 TC 注册分支事务、将本地事务执行结果上报、根据 TC 的指令执行业务数据的提交或回滚。RM 是真正干活的人。以 Seata 的 AT 模式为例RM 在执行业务 SQL 的前后会生成 undo log记录数据变更前后的镜像当全局事务回滚时RM 根据 undo log 把数据还原到修改之前。2.4 三个角色如何协作跑完一次分布式事务我把它们协作的过程整理成一个典型的全局事务时序TM 向 TC 发起beginTC 创建全局事务生成 XID。TM 把 XID 放入当前调用链上下文通过服务调用传递给下游。下游服务中的 RM 解析到 XID向 TC 注册分支事务并执行本地事务。RM 在本地事务执行完成后向 TC 上报分支事务状态。所有业务逻辑执行完毕后TM 向 TC 发起commit或rollback。TC 汇总所有分支事务的状态决定全局提交或回滚并向各个 RM 下发指令。RM 收到指令后执行分支提交或分支回滚并清理本地资源。这里面最关键的一步是 XID 的传递。如果 XID 没传成功下游的 RM 就不知道属于哪个全局事务事务自然也就没法统一协调。实际工作中遇到“某个分支事务没有纳入全局事务”十有八九是 XID 在远程调用过程中丢了。3. 事务模式的选型AT、TCC、Saga、XA 该怎么选3.1 AT 模式默认选择自动补偿的魅力Seata 最常用、也是我推荐团队优先尝试的模式是 AT 模式。AT 是 Automatic Transaction 的缩写利用了数据库本身的事务能力加上 Seata 的 undo log 机制实现自动化的两阶段提交。AT 模式一阶段做的事是解析业务 SQL生成“前镜像”和“后镜像”执行业务 SQL并在同一个本地事务里写入 undo log然后立即提交本地事务并释放本地锁。二阶段如果全局提交RM 只需要删除 undo log如果全局回滚RM 读取 undo log反向生成补偿 SQL把数据恢复到前镜像状态。这个模式最大的优点是对业务代码侵入性很小。你不需要自己写补偿逻辑只要把GlobalTransactional加到方法上再把数据源包装成 Seata 的代理数据源就能工作。代价是每个业务表需要建立 undo_log 表并且需要 TC 协调管理全局锁。3.2 TCC、Saga、XA 的适用场景与差别如果团队需要更精细地控制事务或者业务本身不适合自动补偿就需要考虑其他模式。TCCTry-Confirm-Cancel将业务拆成三个阶段Try 阶段预留资源Confirm 阶段确认执行Cancel 阶段回滚释放。它对业务接口有明确要求侵入性最强但控制力也最强适合资金类等对一致性要求极高的场景。Saga基于补偿事务把一个长事务拆成多个子事务每个子事务都有正向操作和反向补偿操作适合业务流程长、中间步骤多、不需要实时强一致的场景。XA依赖数据库原生对 XA 协议的支持属于标准的两阶段提交。它的一致性很强但锁定资源时间长并发性能差而且要求数据库本身支持 XA通常不是微服务场景的首选。我的建议很直接大部分业务场景优先考虑 AT因为改造量小、可维护性好如果 AT 因为锁竞争导致吞吐上不去再评估 TCC 或 Saga。别一上来就搞 TCC自己写三套接口的维护成本足够让你后悔一两个月。4. 从零落地一个 Seata 示例环境准备与配置4.1 部署 TCSeata ServerSeata Server 是 TC 的载体通常称为 Seata Server从官网下载压缩包即可。我习惯用 Docker 部署但为了讲清楚原理这里说下最直接的二进制部署方式。解压后有两个核心配置需要改registry.conf和config.conf。registry.conf决定 TC 如何注册到服务发现中心config.conf决定 TC 自身的持久化方式。我用 Nacos 作为注册中心和配置中心配置大致是server: port: 7091 registry: type: nacos nacos: serverAddr: 127.0.0.1:8848 group: SEATA_GROUP namespace: cluster: default启动方式很简单Windows 下双击bin\seata-server.batLinux 下执行bin\seata-server.sh。启动成功后可以在 Nacos 服务列表里看到 seata-server。有个细节容易踩坑Seata Server 有两个端口一个对外服务端口默认 8091一个管理端口7091。确认防火墙和客户端配置里用的端口是服务端口别把管理端口当成 RPC 端口。4.2 引入客户端依赖并配置 TM、RM以 Spring Boot 项目为例先在pom.xml引入 Seata 依赖dependency groupIdio.seata/groupId artifactIdseata-spring-boot-starter/artifactId version1.7.0/version /dependency然后在application.yml里做必要配置seata: enabled: true application-id: order-service tx-service-group: my_test_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: config: type: nacos nacos: server-addr: 127.0.0.1:8848 group: SEATA_GROUP namespace: 这里有一个核心概念叫事务分组tx-service-group。客户端使用事务分组去配置中心找对应的 TC 集群地址。也就是说服务端注册到 Nacos客户端通过“分组名”映射到具体 TC。每个服务的事务分组要保持一致才能让同一批服务属于同一个全局事务体系。同时AT 模式要求数据源由 Seata 代理包装Spring Boot 环境通常会自动完成 DataSourceProxy 的注入但如果你用了自定义数据源需要手动包装Bean public DataSource dataSource(DataSource dataSource) { return new DataSourceProxy(dataSource); }每个业务库都需要创建 undo_log 表CREATE TABLE IF NOT EXISTS undo_log ( id BIGINT NOT NULL AUTO_INCREMENT, branch_id BIGINT NOT NULL, xid VARCHAR(128) NOT NULL, context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL, log_status INT NOT NULL, log_created DATETIME NOT NULL, log_modified DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid, branch_id) ) ENGINE InnoDB AUTO_INCREMENT 1 DEFAULT CHARSET utf8mb4;4.3 用 GlobalTransactional 声明全局事务假设订单服务、库存服务、账户服务都在同一个全局事务里。在订单服务入口方法上加上注解Service public class OrderService { Resource private OrderDAO orderDAO; Resource private AccountService accountService; Resource private InventoryService inventoryService; GlobalTransactional(rollbackFor Exception.class) public void createOrder(OrderDTO dto) { orderDAO.insert(dto); accountService.deductAmount(dto.getAccountId(), dto.getAmount()); inventoryService.reduceStock(dto.getProductId(), dto.getCount()); } }注意rollbackFor Exception.class必须写。GlobalTransactional默认只对 RuntimeException 回滚如果你在业务里抛了一个自定义的 checked exception又不写 rollbackFor全局事务不会回滚数据就会不一致。这是我实际工作中踩过最多的坑。远程服务调用时Seata 通过上下文传递 XID。如果你用 OpenFeign需要确认 Seata 自带的SeataFeignContext是否生效。通常引入了 starter 后会创建配置类但如果你自定义了 Feign 拦截器要注意不要拦截掉对 XID 的处理。调用链中任何一环抛异常TM 会向 TC 发起回滚TC 通知所有 RM 执行反向补偿。你可以启动三个服务调用接口后分别看三个库的状态验证事务是否真的生效。5. 实操中的坑与排查技巧5.1 常见错误与排查思路我整理了在项目里见到频率最高的问题按“现象、原因、处理”列出现象可能原因处理方式启动报错no available service found客户端找 TC 失败检查 Nacos 上是否能看到 seata-server核对 tx-service-group 映射分支事务完全不生效XID 未传递检查远程调用链路、Feign 配置、线程池是否重新绑定 XID回滚后数据还是不一致undo_log 表缺失或字段不一致确认所有业务库都创建 undo_log 表全局锁等待超时并发冲突过大或前序事务过长优化大事务拆分扩大事务超时时间注解放在私有方法上不生效Spring AOP 代理失效确保GlobalTransactional放在 public 方法上且通过外部调用和自定义数据源冲突数据源没有被 DataSourceProxy 包装手动注入 DataSourceProxy 或用 Seata 的 starter 自动装配排查顺序也很重要。我习惯先看 TC 的日志再看业务服务的 Seata 日志。TC 日志里能看到全局事务的创建、分支事务注册、二阶段提交/回滚指令基本能定位到是协调层问题还是分支层问题。5.2 一些经验总结这些经验是反复踩坑后沉淀下来的尽量把全局事务控制在短事务。AT 模式在回滚前会持有全局锁事务越长锁冲突越严重。一个方法里最多放两到三次远程调用超过的话考虑合并或者异步化。全局事务里避免混合调用消息队列。Mq 消息的发送和事务提交很难保持一致如果一定要用请参考事务消息方案而不是简单塞进全局事务。幂等设计要提前做。虽然 Seata 能保证全局回滚但补偿过程中如果服务重启、重复调用没有幂等保护还是会出现重复数据。接口层建议用唯一键约束或幂等表。关注 TC 的存储。TC 默认会把事务会话和日志存储到本地文件生产环境建议配置成高可用模式并且做好存储目录的备份。TC 本身也会成为单点需要多节点部署。6. 写在最后的几点个人体会我从 Seata 早期版本一路用过来最大的感受是分布式事务没有银弹Seata 只是把“一致性协调”这件事标准化了。AT 模式确实省心但带来的是数据库层面更强的锁依赖以及额外的 undo log 开销TCC 虽然灵活但多一套接口设计业务开发要额外花时间。选择哪种模式本质是在“开发成本”和“运行性能”之间做取舍。如果你团队刚接触 Seata我的建议是先拿一个非核心交易链路上线比如内部后台的跨库更新跑通之后再推广到订单、支付等核心链路。不要一开始追求极端一致性先把 XID 传递、事务分组、undo log 这些基础设施搞明白再逐步扩展。最后再分享一个小技巧排查分布式事务问题前先手动模拟“下游抛异常”的情况观察各库数据是否回滚。如果这时候都不能正确回滚就别急着调并发和性能先把事务基础能力修好。这个习惯能帮你节省大量定位时间也是我对 Seata 最直接的落地忠告。
返回列表