ARTICLE DETAIL

资讯详情

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

Seata分布式事务实战:核心原理、安装部署与排障指南

Seata分布式事务实战:核心原理、安装部署与排障指南 做后端开发久了迟早会撞上分布式事务这堵墙。本地事务靠数据库的ACID就能搞定一旦拆成微服务跨库、跨服务的原子性就成了绕不开的难题。Seata 就是目前 Java 生态里最主流的分布式事务解决方案之一由阿里巴巴开源社区活跃度很高很多生产项目都在用它。这篇博文我打算把 Seata 的安装步骤和核心工作原理一次性讲透重点说说 TC、TM、RM 这三个角色到底各管什么事以及我在实际部署中踩过的坑。这篇内容适合正准备在项目里引入 Seata、或者已经引入但遇到问题不知道怎么排查的读者。我会先讲整体设计思路再拆解安装流程最后结合实操代码和常见问题给你一套可以直接抄作业的方案。1. 整体设计思路Seata 到底解决什么问题1.1 从一次跨库下单说起先举个最典型的场景用户下单订单服务写订单库库存服务扣库存库账户服务减余额。这三个操作分布在三个独立的数据库里任何一个失败其他两个都得回滚否则就会出现“订单下了但库存没扣”或者“钱扣了但订单没生成”的脏数据。本地事务在这里完全失效因为你没法在一个事务里同时控制三个数据库的连接。两阶段提交2PC协议倒是能解决这个问题但传统的 2PC 实现太笨重协调者单点、同步阻塞、资源锁定时间长在高并发场景下根本扛不住。Seata 的核心设计思路就是尽量规避这些毛病用一套轻量级的、对业务侵入极小的方案来完成分布式事务的协调。Seata 把整个分布式事务的协调过程拆成了三个独立角色TCTransaction Coordinator事务协调者、TMTransaction Manager事务管理器、RMResource Manager资源管理器。这个拆分非常关键它把“谁来发起事务”“谁来协调分支”“谁来执行分支”这三件事彻底解耦了后面我会逐个分析。1.2 Seata 与同类方案的对比选型市面上能选的分布式事务方案其实不少除了 Seata还有 MQ 消息事务、本地消息表、TCC 框架如 TCC-Transaction、tcc-transaction、Saga 等。我梳理了一下实际选型时的主要对比维度方便你结合自己的场景判断方案数据一致性侵入性吞吐量适合场景Seata AT 模式最终一致性默认低几乎无侵入高绝大多数跨库业务TCC强一致性高需要写 Confirm/Cancel中资金类、对一致性要求极高的场景MQ 消息事务最终一致性中需要额外消息表高异步解耦场景订单 - 通知Saga最终一致性高需编排状态机中长事务、流程复杂如果你是第一次接触这个领域我建议优先把 Seata 的 AT 模式吃透。它最大的优势在于对业务代码几乎无侵入你只需要在入口方法上打一个GlobalTransactional注解Seata 会在底层自动帮你完成分支事务的注册、提交和回滚。相比 TCC 那种需要手写三段逻辑的方案AT 模式的上手成本低太多了。2. 核心原理拆解TC、TM、RM 三者如何协作2.1 三个角色的职责定义先花点时间把角色理清楚因为后面所有配置和排障都离不开这三个概念。我直接用人话解释TCTransaction Coordinator事务协调者独立部署的服务端进程。它负责全局事务的开启、提交和回滚的决策是所有事务状态的“大脑”。通俗点说就像一个裁判掌握着比赛全局事务的最终判罚权。TMTransaction Manager事务管理器嵌在业务代码里。它负责向 TC 发起全局事务并告诉 TC 这个全局事务到底应该是 commit 还是 rollback。它就是业务方的“代表”向裁判汇报情况。RMResource Manager资源管理器同样嵌在业务代码里。它管理每个分支事务负责向 TC 注册分支、上报分支执行状态并接收 TC 的提交/回滚指令来操作本地事务。RM 就是实际干活的“运动员”。这三者的关系可以用一段简单的流程描述TM 通知 TC 开启全局事务 - 每个服务的 RM 在本地执行 SQL执行完向 TC 注册分支 - 所有分支都执行成功后TM 请示 TC 提交全局事务 - TC 逐个通知 RM 提交各自的分支事务如果中间任何一步失败TC 就通知所有 RM 回滚各自的分支事务。2.2 AT 模式的自动回滚实现原理AT 模式是 Seata 最核心的卖点它的名字其实是“Automatic Transaction”的缩写。它实现自动回滚的关键在于两个辅助表全局锁表和undo_log表。当 RM 执行一条业务 SQL 时比如UPDATE stock SET count count - 1 WHERE id 100Seata 会做三件事在业务 SQL 执行前解析 SQL生成前后镜像before image 和 after image。注意前镜像不是直接从数据库读的而是根据 SQL 类型、主键用 SELECT 查出来的。Seata 会先把这些镜像以 JSON 的形式记录到undo_log表里。执行业务 SQL并获取全局行锁防止其他事务并发修改同一行数据。分支事务提交时异步删除undo_log里的记录。如果后续某个分支回滚了TC 通知 RM 回滚RM 就根据undo_log里的前后镜像生成一个反向 SQL把 after image 改回 before image执行它来恢复原数据。这就是为什么表结构里必须建undo_log表很多初学者忘了建表一触发回滚就直接报错。这里有个非常精妙的设计Seata 没有用传统 2PC 的“锁定所有资源直到事务结束”策略而是用“先记录、后修改、再确认”的方式把锁的粒度大大缩小了从而在高并发下保持了较高的吞吐量。代价就是需要额外的存储和 SQL 解析开销以及极端情况下可能出现的脏读——不过 Seata 通过全局锁尽量避免了这个问题。2.3 全局事务的超时与异常处理流程全局事务的执行不是无限期的Seata 有默认的超时控制默认超时时间是 60 秒。这里有个容易踩坑的地方GlobalTransactional注解上有个timeoutMills参数你可以单独为某个业务指定超时时间但很多人忽略了服务端file.conf里的timeout全局默认值导致明明业务还在正常执行却被 TC 强制标记为超时回滚了。超时回滚的流程是这样的TC 在超过timeoutMills后还没收到 TM 的提交/回滚请求就会判定这个全局事务超时主动通知所有相关 RM 进行回滚。这个机制保证了分布式事务不会无限期悬挂但也要求你设置合理的超时时间不能拍脑袋。比如说一个包含文件处理、外部接口调用的业务可能要 90 秒但你全局默认 60 秒那高延迟时就会莫名其妙回滚。我的建议是把全局默认值调大一些比如 120 秒再在个别长任务上单独加注解超时。3. Seata Server 安装与环境准备3.1 版本选择与下载建议我强烈建议你先确认自己项目的 Spring Boot / Spring Cloud / 数据库版本再去选 Seata 版本。这里有个常见的坑不同版本的 Seata 配置文件差异很大老版本用registry.conffile.conf新版本1.4.0支持了application.yml配置再到 1.5.0 之后配置结构又变了一次。如果你直接拿网上旧教程的配置套新版本大概率起不来。目前生产环境用得比较多的稳定版本是 1.6.1 和 2.x 系列。就我的实测体验1.6.1 资料最多、坑最少2.0.0 换了配置文件结构拆分成了application.yml和数据库脚本集成配置管理方式更清晰了。如果你是用 Spring Cloud Alibaba强烈建议对照版本对应关系表否则客户端和服务端版本不一致会遇到各种奇怪的分支注册失败问题。我个人目前生产环境用的是 Seata Server 1.6.1 Seata Client 1.6.1Spring Cloud Alibaba 2021.0.4.0跑了一年多很稳定。下载 seata-server 时注意两个发行包seata-server-XXX.tar.gz是 Linux 的seata-server-XXX.zip是 Windows 的。解压后的目录结构很简单你需要的重点就两个子目录bin启动脚本和conf配置文件后面我会直接围绕它们讲。3.2 Nacos 注册中心与配置中心集成Seata Server 启动后需要知道去哪注册自己、去哪读取配置这就是conf下配置文件的用途。注册中心和配置中心可以选 Nacos、Consul、Etcd、Zookeeper 等国内项目用 Nacos 的占比最高。我先讲 Nacos 路线因为它的部署成本最低、界面友好。首要前提你有一个可访问的 Nacos不管是本地开发还是服务器生产环境。然后要让 Seata Server 使用 Nacos需要改两个地方在 Nacos 上创建 Seata 专用的命名空间可选强烈建议避免和其他配置混在一起。修改 Seata Server 的配置文件把registry.type改成nacos并填上 Nacos 地址、命名空间、集群名等。在 Nacos 配置列表里创建seataServer.properties配置文件把数据源、事务相关配置集中放进去。这样后续修改配置就不用再去服务器上改文件了维护成本低很多。需要特别留意的是从 1.4 版本开始Seata Server 的配置中心默认可以关闭如果你不想用配置中心可以把config.type设为file或留空。但生产环境我建议还是用 Nacos 统一管理毕竟一旦有多台 Server手改配置文件会把人逼疯。3.3 初始化数据库脚本与全局配置不管用哪种注册中心有一个步骤绝对绕不开初始化全局事务会话和锁记录所需的数据库表。Seata Server 在运行时需要把全局事务会话、全局锁等信息持久化到数据库里所以你得先建库、建表。在你的 MySQL 实例上新建一个数据库比如seata然后执行 Seata 安装包conf目录或源码script/server/db/mysql.sql中的脚本。这个脚本会创建global_table、branch_table、lock_table三张表分别用于存储全局事务会话、分支事务会话和全局锁记录。然后业务数据库里每个涉及分布式事务的库都必须额外建一张undo_log表。这张表前面已经说过是 AT 模式实现回滚的核心。不同数据库类型有对应的建表脚本script/client/sql/mysql.sql里面就有。注意这步是更容易忽略的很多人只初始化了服务端三张表忘了业务库里要建undo_log等到真正跑回滚时才发现数据库表缺失。初始化完表之后启动 Seata Server。Linux 下到bin目录执行sh seata-server.sh -p 8091 -h 127.0.0.1Windows 下执行seata-server.bat。参数含义后面我细说这里先记住默认端口 8091。启动后观察日志如果出现“Server started”之类的字样说明服务端好了。常见启动失败的原因就那么几个端口被占用、数据库连不上、Nacos 没配好。我在第 5 章会单独开一节集中讲排障。4. 客户端接入Spring Boot 项目整合 Seata4.1 Maven 依赖引入与版本锁定服务端就绪后客户端接入的流程就比较机械化了。我这里以 Spring Boot Spring Cloud Alibaba 为例因为这是目前最主流的搭配。首先在父工程的pom.xml里引入 Spring Cloud Alibaba 的 BOMBill of Materials依赖版本管理。BOM 的好处是帮你把 Seata 相关依赖的版本统一锁定避免你自己去纠结什么版本和什么版本匹配。dependencyManagement dependencies dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-alibaba-dependencies/artifactId version2021.0.4.0/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement然后在各个需要分布式事务的子模块里引入 Seata Starterdependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-seata/artifactId /dependency注意spring-cloud-starter-alibaba-seata会自动引入 Seata 客户端相关依赖不需要再单独加seata-all加重复了反而容易版本冲突。如果项目里之前手动加过 Seata 依赖建议先清理掉。4.2 Nacos 注册与事务分组配置依赖引入好了之后就是告诉客户端去哪找 TC、怎么分组。这部分是新手最容易懵的地方因为 1.4 之后的版本引入了“事务分组”的概念很多人搞不清楚它的作用。所谓事务分组就是给一组服务定义一个逻辑名称例如my_tx_group然后客户端启动时拿着这个逻辑名称去配置中心查询真实的 TC 集群地址。这样做的好处是如果 TC 的物理部署地址变了不需要改每个客户端应用的配置只需要改配置中心里的映射关系即可。理解了这个你就不会写错配置。在 Spring Boot 的application.yml里核心配置如下spring: cloud: alibaba: seata: application: order-service tx-service-group: my_tx_group enable: true同时你需要在 Nacos 的配置中心里维护一个service.vgroupMapping.my_tx_groupdefault的配置告诉 Seata“逻辑分组 my_tx_group 对应集群 default 里的 TC。” 如果集群配置为默认的default那么 Seata Server 启动时的集群名也必须是default否则客户端注册不上。这里有个细节我强烈提醒tx-service-group的逻辑名不要起得太随意建议用业务线命名比如order-tx-group、pay-tx-group。因为日志排查时你需要根据分组名快速定位是哪个业务集群。我之前踩过坑全部用默认my_test_tx_group线上排查问题时日志里全部一样根本分不清是哪条链路。4.3 使用 GlobalTransactional 正确开启全局事务客户端配置全部就绪最后一步就是在业务入口方法上加注解。这里有个关键点全局事务的入口应该在最外层调用的方法上通常是 Controller 调用的 Service 方法或者 Feign 调用的入口方法而不是每个内部子方法都加注解。给个示例订单服务创建订单后需要 Feign 调用库存服务扣库存。Service public class OrderServiceImpl { Autowired private StockFeignClient stockFeignClient; GlobalTransactional(name order-create-tx, timeoutMills 120000) public void createOrder(OrderDTO orderDTO) { // 本地事务插入订单记录 orderMapper.insert(orderDTO); // 远程调用扣减库存 stockFeignClient.deductStock(orderDTO.getProductId(), orderDTO.getCount()); } }GlobalTransactional注解的name属性不是必填的但我建议每个方法都起一个有意义的名字否则日志里只显示一个全局事务 ID排障时很难辨认是哪个业务。另外timeoutMills我习惯显式设置而不是依赖默认值这个习惯帮我少踩了很多超时回滚的坑。子服务库存服务不需要加GlobalTransactional它只需要接入 Seata 客户端、注册到同一个 TC 上就行。Seata 会通过 RPC 调用链的上下文传递机制自动把全局事务 ID 透传给子服务子服务里的 RM 会自动加入这个全局事务。4.4 业务库 undo_log 表的作用前面提了业务库需要建undo_log表这里展开说说。这张表在 AT 模式下是必须存在的它记录了每个分支事务修改数据前后的镜像。一旦回滚Seata 会从这张表读取镜像来生成反向 SQL恢复原始数据。表结构里有两个重要字段branch_id分支事务 ID和undo_log前后镜像 JSON。有个容易忽略的点undo_log表需要和应用的表放在同一个数据库里因为 Seata 的事务回滚是在同一个本地事务里执行的这样才能保证镜像记录和业务数据的一致性。如果你把undo_log放在另一个库回滚时会因为跨库问题直接失败。另外提一句生产环境下建议定期清理undo_log废弃数据否则这张表可能会积累大量历史镜像。虽然 Seata 在分支事务提交成功后会异步删除记录但极端情况下进程崩溃、网络中断会有残留。我现在的团队就是写了个定时任务每周清理一次超过 7 天的undo_log记录实测对性能没有影响。5. 常见问题与排查技巧实录5.1 事务不生效的根因分析“我加了GlobalTransactional注解但分布式事务就是不生效”这是我被问得最多的问题之一。遇到这种情况优先排查以下几件事是否引入了 Seata Starter而且版本和服务端一致是否配置了tx-service-group客户端能不能从 Nacos 拉到配置服务端 Seata Server 是否启动成功网络能不能通业务入口和子服务链路是否都在 Seata 的分布式事务上下文中还有一个很容易被忽略的坑GlobalTransactional注解如果加在私有方法上或者加在类内部调用链路上而非跨服务调用它可能不会触发全局事务。Spring AOP 是基于代理的类内部方法自调用不走代理注解就会失效。我遇到过一个真实案例用户把 Service 方法 A 调方法 BB 上加了注解A 没加结果 B 作为内部调用根本没被代理事务完全没开启排查了一下午才发现是这个基础问题。这个问题在 Spring 事务里也存在原理完全一样。5.2 分支事务注册失败的场景分析如果日志里频繁出现 “Could not found global transaction xid” 或 “Branch transaction register failed”通常意味着子服务没有正确传递全局事务 ID。可能的原因有几个子服务没有接入 Seata 客户端或者接入后还没连上同一个 TC。使用的 RPC 框架版本与 Seata 不兼容导致 xid 没有通过请求头透传。服务端的注册中心配置和客户端不一致导致客户端找不到正确的 TC 地址。排查思路先去子服务的日志里找是否有“transaction xid received”之类的记录。如果没有说明 RPC 上下文传递断了。这时检查两件事你是否用GlobalTransactional注解在入口方法上你的 Feign 拦截器有没有被 Seata 自动注册spring-cloud-starter-alibaba-seata会通过SeataFeignClient自动在 Feign 请求头里追加 xid但如果你自定义了 Feign 拦截器并直接覆盖了某些关键头信息可能导致 xid 丢失。我第一次遇到这个问题时就是在自定义拦截器里重写了整个请求头把 Seata 的TX_XID给挤掉了。5.3 超时导致的回滚问题我参与过一个比较典型的生产事故一批订单数据出现不一致排查到最后发现是全局事务超时回滚导致部分分支没有被提交。因为业务里调用了外部接口云上接口偶尔响应慢整体耗时超过了默认 60 秒的全局事务超时阈值TC 强制回滚了但外部接口那边数据已经写进去了产生了不一致。这类问题的解决方案有几种给长耗时业务显式使用timeoutMills把超时时间调整到合理范围。优化业务逻辑减少事务内耗时不必要的外部调用比如把非核心链路放到事务提交后异步执行。区分“快事务”和“慢事务”慢事务尽量用 TCC 或 Saga 模式而不是 AT避免长时间占用全局锁导致性能问题。我在实际设计中凡是涉及到外部接口同步调用的方法都会先评估耗时分布再设定timeoutMills。宁可设大一点也不能为了“看起来严谨”而用自己的主观判断把超时时间设短。超市逻辑要留余量这个道理在分布式事务里一样适用。5.4 回滚失败与脏读问题的排查最后一个容易出问题的点是回滚失败。回滚失败大多数情况下是数据校验失败也就是说你在前后镜像对比时发现数据被其他事务修改了。Seata 在处理回滚时会比较当前数据和 after image 是否一致如果不一致说明数据被污染它会选择抛出异常并记录Branch rollback failed日志。这种情况往往是并发场景下全局锁失效导致的。排障时需要关注全局锁表lock_table里是否出现了异常的锁记录以及所在表上的唯一索引、主键是否正确。Seata 的全局锁是依赖数据库资源实现的如果业务表的索引设计不合理比如缺少唯一索引并发插入时会发生隐式锁冲突导致回滚时镜像对比也失败。多查lock_table多和 DBA 沟通表索引设计能避免大部分这类问题。关于脏读AT 模式默认是读已提交隔离级别但没有做读锁定。也就是说在一个全局事务未提交时其他事务可能读到这个事务修改过但还没提交的数据这就需要业务在设计时考虑好隔离级别。如果确实需要更高隔离级别可以通过配置开启SELECT FOR UPDATE或SELECT的全局锁辅助但这会影响并发性能需要权衡。6. 综合建议与实践心得玩 Seata 这几年下来我最大的体会是不要上来就在核心账务系统里引入 AT 模式先在边缘业务跑通链路再逐步扩大范围。账务系统对一致性极其敏感一旦undo_log表缺失、全局锁冲突、镜像对比失败处理起来都是大事故。我个人是在订单、库存这类非资金敏感业务里跑稳定了一年之后才敢在积分、抵扣这种半资金场景引用的。资金核心链路目前还是用 TCC 自己把控心里踏实一点。还有一点补充Seata 默认情况下不会限制branch_table和global_table无限制增长。高并发情况下如果 TC 频繁开全局事务、短时间内大量分支注册数据库里这两张表的数据量会快速上升。我的做法是监控这两张表的行数超过阈值就清理同时把 TC 的全局事务会话超时参数调短确保会话不会长期支撑不释放。最后再分享一个小技巧用 Seata 做分布式事务日志配置一定要单独开 logger 或者调高 Seata 相关包的日志级别。Seata 打的日志本来就不多但关键信息xid、branch id、回滚原因都在里面。我习惯把io.seata的日志级别设为 DEBUG配合链路追踪中间件能非常高效地定位是哪个分支、哪条 SQL、哪种操作导致的问题。生产环境调回 INFO 也完全可以千万别一股脑全拒掉。
返回列表