1. 先从那个经典的下单场景说起
做微服务的同学几乎都遇到过同一个心电图一样的场景:用户在App上下了一单,订单服务往订单库插了一条记录,然后通过Feign调库存服务扣减库存,再调账户服务扣余额,最后再调积分服务加积分。单看每一个服务的本地事务,逻辑都处理得漂漂亮亮,可一旦某个下游接口网络抖动、数据库连接池打满,就会出现"订单创建了但库存没扣"或者"钱扣了但积分没到账"的尴尬局面。订单数据、库存数据、账户数据分属不同数据库,没有一套通用的机制保证它们要么都成功、要么都回滚,这就是分布式事务要解决的问题。
Seata就是专门解决这个问题的开源框架,我的理解它本质上是一个"跨数据库事务协调器":业务代码里几乎不用写提交流程或补偿逻辑,只要在入口方法上打一个事务注解,框架就会自动把后续调用链路里涉及的所有数据库操作纳入同一个全局事务,统一提交或者统一回滚。这套方案对Java后端、尤其是Spring Cloud / Spring Boot体系非常友好,很适合订单交易、金融账户、优惠券发放这类数据一致性要求高的场景。如果你所在项目正面临跨库写操作的一致性问题,又不想引入过于复杂的自制消息对账方案,Seata是当下性价比最高的选择之一。
下面这部分内容我不会照抄官方文档,而是站在一个真实接入过Seata的人的角度,把原理、模式选型、部署步骤和踩坑经验串联起来,尽量让对分布式事务还比较模糊的同学也能看懂,并且能照着落地。
1.1 一个订单背后到底牵动了多少个库
以最常见的电商下单链路为例。用户在客户端点击"提交订单"之后,后端至少会出现这样一串调用:订单服务写入一张订单主表和几张明细表(订单库),库存服务执行库存扣减(库存库),账户服务执行余额扣减(账户库),积分服务执行积分增加(积分库)。这四个库在物理上通常部署在不同主机上,每个服务自己维护数据库连接。如果按传统思路,每个服务方法内部加一个事务注解,只保证了各自库内的操作原子性,四个服务之间没有联动关系。
这里我把每个库里的关键写入动作列一下,方便后面讲原理时对照。
- 订单库:插入订单主表、插入订单明细表、更新订单状态
- 库存库:扣减商品的可用库存,可能还要写库存流水表
- 账户库:扣减用户余额,写入余额变动流水
- 积分库:增加用户积分,写入积分流水
问题就出在第三步。假设账户服务扣款成功之后,积分服务因为网络超时返回失败,如果订单服务把异常抛给上游、让用户看到下单失败,那订单、库存、账户这三个库已经提交的数据怎么办?在没有分布式事务的情况下,只能靠补偿逻辑——人工或脚本去把三个库的数据再"改回来"。这个补偿动作写起来很痛苦,要设计对账、要处理中间态、要保证补偿本身不重复执行。分布式事务框架的价值,就是把这一整套"要么全成、要么全撤"的机制系统化。
1.2 为什么本地事务救不了这个场景
你可能自然会想:我在订单服务里把扣库存、扣余额都写成本地SQL不就行了?问题在于,库存库和账户库根本不在订单服务这台机器上,订单服务只能通过RPC去访问,而RPC调用天然不在同一个数据库事务边界内。一个事务注解方法里如果发了三次Feign调用,第一次调用结束后,库存库的数据就提交了,第二次调用失败时,第一次的数据并不会因为方法抛出异常而自动回滚——每个库的事务已经各自结束了。
更深层的原因是资源隔离。数据库事务依赖连接持有,一个事务只能在一个数据库连接上执行,一个连接不能同时跨越三台数据库服务器。想让三个库进入同一事务,就必须有一个协调者,把每个库自己的事务挂到同一个全局事务下面,让它们按照统一的指令提交或回滚。这就是分布式事务协议(比如两阶段提交)存在的意义,也是Seata这类框架要解决的问题。
1.3 传统解法为什么让人头大
在Seata之前,大家处理这个问题的常见思路大概有三类。第一类是本地消息表,业务方在自己库里建一张消息表,把"待下游执行"的动作先写进消息表,事务提交后再通过定时任务把消息发给下游;这种做法实现简单,但为了确保下游一定执行成功,需要大量的状态判断、重试设计和定时扫描,代码量非常可观。第二类是事务消息,把"发送消息"和"本地事务"做原子绑定,这比本地消息表优雅,但要求团队对消息中间件有较深的理解,而且最终一致性的窗口期业务方必须接受。第三类是直接裸用两阶段提交协议,自己写事务管理器,对接每个资源的准备与提交接口——理论上可行,但工程实现极其复杂,一般团队扛不住这个复杂度。
这三类方案的共同痛点是:要么牺牲实时一致性,靠异步补偿兜底;要么把大量协调逻辑压到业务代码里,导致核心链路被一堆补偿、对账、状态机代码塞满。Seata的出现,正是把"怎么协调、怎么记录、怎么回滚"这层逻辑下沉到了框架层,业务方只在方法上打一个注解,心智负担小了很多,这也是它在近几年能快速普及的根本原因。
2. 认识Seata:TC、TM、RM三位一体的协调机制
Seata的核心设计可以用三个字母概括:TC(Transaction Coordinator)、TM(Transaction Manager)、RM(Resource Manager)。很多第一次接触Seata的人会被这三个名字绕晕,其实它们的分工非常朴素,完全可以类比成一个项目的三类角色。
TC是事务协调者,它是独立部署的服务端进程,作用是接收全局事务的开启与提交/回滚请求,管理全局事务状态,并驱动分支事务的提交和回滚。你可以把它理解成整个事务流程的"总指挥"。
TM是事务管理器,它嵌在发起全局事务的业务应用里。业务方法上标注全局事务注解后,TM就会在方法开始前向TC申请开启一个全局事务,拿到一个全局事务ID,也就是XID;到方法正常结束或抛出异常时,TM再通知TC进行全局提交或回滚。它只负责"发起"和"拍板"。
RM是资源管理器,它也嵌在业务应用里,但它管的是具体的数据库操作。RM负责把参与全局事务的数据库操作注册为分支事务,向TC上报分支状态,并在最终阶段执行TC下发的分支提交或回滚指令。一个全局事务可以挂多个RM,每个RM掌管一个数据库分支。
一句话总结:TM管全局事务的边界,RM管每个数据库分支的执行,TC管全局事务的最终裁决。
2.1 一个全局事务的完整生命周期
以AT模式为例,一个全局事务从开始到结束,大体会经历这么几步。第一步,TM在业务方法入口向TC发起全局事务注册,TC返回XID,这个XID通过调用链一路传递到后续所有服务。第二步,订单服务执行本地SQL插入订单表,RM会拦截这次数据库操作,在本地库完成数据写入的同时,生成一条对应的undo_log记录,并向TC注册分支事务。第三步,库存服务、账户服务、积分服务依次执行同样的流程:各自本地写库、各自生成undo_log、各自注册分支。
当最后一个服务执行完,方法正常返回,TM向TC发起全局提交请求。TC收到后,向所有已注册的RM发提交指令,各RM只需要删掉自己库里的undo_log记录、释放全局锁,整个事务就结束了。如果中间任何一个环节抛出异常,TM就会向TC发起全局回滚,TC向所有RM发回滚指令,各RM根据undo_log里记录的"修改前镜像"生成反向SQL,把数据还原到事务开始前的状态。
这个流程里最巧妙的一点是:每个服务只是在自己的数据库里多执行了一条"写日志"操作,业务SQL本身没有任何改动。所谓低侵入,就体现在这里。
2.2 XID如何跨服务传递
XID的传递是整个全局事务链路成立的前提。如果服务A开启全局事务后,调用服务B时XID没有跟着请求头传过去,服务B的数据库操作就无法被识别为该全局事务的分支,回滚时就会漏掉它。Seata对主流的RPC框架都做了支持:Feign场景下,Seata拦截器会把XID塞进HTTP请求头,服务端再取出来放进当前线程的上下文;Dubbo场景下则通过RPC隐式传参机制传递。如果你自己写的RPC框架或者HTTP客户端不走Seata的过滤器,那就必须手动在调用方把XID放入请求上下文、在被调用方从上下文取出并绑定到当前线程,否则会出现"全局事务少了一截"的诡异问题。
这里有个容易忽略的细节:XID不是只传给下游服务就完事了,还要保证它在线程池、异步任务里不丢。比如一个方法里用了异步注解或者CompletableFuture另起线程去执行数据库操作,子线程默认是没有Seata上下文里的XID的,这类分支操作照样不会被纳入全局事务。解决办法是在异步执行前手动把XID绑定到子线程,或者干脆让所有分支操作都在同步调用链上完成。
2.3 为什么说Seata对业务代码侵入小
侵入小主要体现在两个层面。第一,业务方几乎不需要写任何协调逻辑,没有散落的补偿代码,没有状态机,就是一个注解的事。第二,Seata通过代理数据源来接管数据操作,应用原本用DataSource、JdbcTemplate、MyBatis的方式都保持不变,框架在底层把普通数据库连接替换成了带事务感知能力的代理连接,事务CRUD操作在代理层被拦截、记录。这就意味着,接入Seata对已有DAO层代码几乎是透明的,改造成本被压缩到了最小。
不过这里也要提醒一句,"低侵入"不等于"无侵入"。虽然不需要改业务SQL,但业务库必须额外建一张undo_log表,而且对数据源类型有一定要求。这些细节我会在后面的落地章节展开,遇到地方再强调。
3. 四种事务模式,别再对着文档纠结
Seata提供四种事务模式:AT、TCC、Saga、XA。很多人一上来就想把四种模式都研究透,结果越看越晕。我的建议是:优先搞清楚AT,因为它覆盖了绝大多数常规业务;剩下三种了解清楚适用边界即可,等真有对应场景时再深入。下面逐个讲。
3.1 AT模式:自动补偿,日常首选
AT模式的全称是Automatic Transaction,它是Seata最核心、也最常被使用的模式。前面在讲全局事务生命周期时,实际上都是以AT模式为蓝本。它的核心机制是"用日志换可回滚性":每个分支事务在执行业务SQL前后,框架会分别记录数据修改前的镜像和修改后的镜像,存入undo_log表,并获取针对这条数据的全局锁;全局提交时只要删除镜像日志,全局回滚时则用镜像数据生成反向SQL恢复现场。
具体到一次UPDATE操作,流程是这样的:业务SQL执行前,RM先查询出目标记录的当前值,存成before image;执行业务UPDATE;再查询执行后的值,存成after image;然后把这组镜像和分支信息写入undo_log;最后在锁表里登记这条数据被哪个全局事务的哪个分支持有。如果链路后续失败,RM回滚时会先校验after image和当前库里的数据是否一致,一致才执行反向SQL;不一致说明数据被其他事务改过了,需要走人工介入或者重试策略。
AT模式对业务的侵入性最小,事务控制粒度可以精确到方法,而且可以同时参与多个表、多个库的操作。它的代价是每次数据操作都多了两轮查询和一份日志写入,性能上会有一定开销;加上全局锁的存在,在高并发写同一行数据的场景下容易产生锁等待。所以我的判断很直接:没有明显热点行竞争的业务,无脑选AT;热点行竞争严重的场景,再看TCC。
3.2 TCC模式:Try-Confirm-Cancel,把控制权握在手里
TCC模式是典型的业务侵入型方案,需要你为每个参与事务的业务动作手写三个方法:Try阶段预留资源,Confirm阶段最终确认提交,Cancel阶段释放预留资源。以账户扣款为例,Try时把用户余额中待扣的部分单独冻结,Confirm时真正扣除冻结额度,Cancel时解冻。这样做的本质是把每个分支变成一个"可预留、可确认、可取消"的业务动作,由TC统一调度。
TCC的好处很明显:不存在全局锁,不会像AT模式那样因为锁竞争拖慢并发;事务粒度由你控制,可以只锁定业务动作而非整行数据;适用于那种数据库记录本身不适合用undo_log做自动回滚的业务,比如操作了外部接口、需要自己做幂等和状态机的场景。代价同样明显:三个方法都要自己维护,要处理幂等、空回滚、悬挂等一系列异常情况,代码量比AT模式大一个量级。
我踩过的坑是TCC的空回滚问题:如果Try方法因为网络原因根本没执行到,但Cancel却被调度了,Cancel就要能识别"资源不存在"并安全跳过,否则会回滚一顿空气甚至报错。这类边界在写TCC时一定要提前设计,不能等压测才暴露。
3.3 Saga模式:为长事务而生
Saga模式解决的是链路特别长、整体耗时特别久的场景。它不再依赖锁,而是采用一系列正向事务加反向补偿事务的方式,每个环节执行完就提交自己的本地事务,如果后面某个环节失败,就逐个执行之前各环节的补偿动作。这相当于把"全局事务"变成了"可逆的业务流"。
Saga有两种实现方式:编排式和协同式。Seata主要支持的是基于状态机引擎的编排式实现,通过定义状态转移图来描述每个服务环节和对应的补偿服务。它的优点是适合跨越多个异构系统、包含人工审批或外部系统交互的长业务流程,事务间隙不会被长时间占用;缺点是事务隔离性很弱,中间状态对外可见,补偿逻辑同样要自己维护,而且业务方必须能接受"部分成功再逐步回退"的过程。
3.4 XA模式:数据库原生支持,最简单也最"贵"
XA模式是最贴近传统单库事务的一种模式,它直接使用数据库自带的XA协议,通过Seata进行分支事务的注册与协调。分支资源的prepare、commit、rollback都由数据库原生事务管理器来保证,所以数据一致性最强,业务入侵几乎为零。代价是资源在prepare后会被数据库长事务锁定,并发能力受到明显限制,而且对数据库类型和驱动程序有要求。
XA模式比较适合短事务、低并发、强一致要求的场景,比如清算类的批处理接口。在互联网业务里,如果并发量不低,我不太建议首选XA,因为锁资源的开销很容易成为瓶颈。
3.5 模式对比与选型建议
为了看得清楚,我把四种模式的关键差异整理成一张表。
| 模式 | 一致性强度 | 业务侵入 | 性能开销 | 适用场景 |
|---|---|---|---|---|
| AT | 强一致(隔离性稍弱) | 低,注解即可 | 中,多两次查询加日志写 | 常规增删改,大多数业务首选 |
| TCC | 强一致 | 高,手写三个方法 | 低,无全局锁 | 热点行并发高、需业务级预留 |
| Saga | 最终一致 | 高,补偿服务自维护 | 低 | 长链路、跨异构系统、容忍中间态 |
| XA | 强一致 | 低 | 高,数据库锁资源占用明显 | 低并发、强一致、短事务 |
选型时我一般按这个顺序问自己:业务能不能接受最终一致?如果必须实时一致且并发不高,看XA;如果并发高且热点明显,看TCC;如果链路很长老是超时,看Saga;如果都不是,直接用AT。多数交易类业务,AT已经足够,不用为了追求"高级"而上更复杂的模式。
4. 实操落地:从零把一个Spring Boot服务接入Seata
理论知识讲再多,不如亲手跑一遍。下面这部分我会以Seata 1.7版本为例,把一个Spring Boot项目接入Seata AT模式的完整过程拆开讲。环境方面假设你已经有一个正常的微服务项目,里面至少有两个服务通过Feign相互调用。
4.1 先把Seata Server搭起来
Seata Server是TC角色的独立进程,必须先启动。先去官方发布渠道下载seata-server压缩包,解压后重点关注配置文件。1.5版本之后,Server端的配置和集群信息统一放在conf目录下的application.yml里,不再像老版本那套复杂的两份配置。里面有两个关键配置:registry决定TC怎么注册自己,如果你们的微服务用Nacos这类开源注册中心做服务发现,就配置为nacos类型,并填上注册中心的地址;store决定TC把全局事务、分支事务、锁记录存在哪里,单机测试用file存储就够了,生产环境一定要切换成db存储,否则重启就丢状态,高可用也没法做。
如果你准备用db存储,需要在TC的数据库里先建三张表:global_table(全局事务表)、branch_table(分支事务表)、lock_table(全局锁表)。这三张表的建表SQL在官方文档的deploy目录里就有。建完改好配置后,直接执行启动脚本即可。启动成功后观察日志,如果输出了TC对外暴露的地址和端口,说明Server已经正常起来了。
4.2 应用侧依赖与配置清单
应用侧接入首先是加依赖。对于Spring Boot项目,官方提供了一个starter,坐标是io.seata:seata-spring-boot-starter,版本尽量和Server保持一致,比如都用1.7.0。这里有一个很多人踩过的坑:客户端和Server的版本号最好完全对齐,至少大版本要一致,否则客户端注册分支时可能会出现协议不兼容的报错,排查起来非常费劲。
依赖加好后,在application.yml里加一段Seata配置。核心有四块:应用标识、事务分组、注册中心、配置中心。事务分组是个容易迷惑的概念,它是一层逻辑映射,不是IP地址。实际请求链路是:业务方法指定事务分组,分组通过配置文件映射到一个TC集群名,集群名再从注册中心解析出具体的TC地址。我建议刚开始把所有服务的tx-service-group配置成同一个分组名,比如default_tx_group,这样调用链上所有服务都识别为同一个全局事务分组,不容易出问题。
seata: enabled: true application-id: order-service tx-service-group: default_tx_group registry: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: "" group: SEATA_GROUP config: type: nacos nacos: server-addr: 127.0.0.1:8848 namespace: ""如果你的项目没有注册中心,只想本地联调,也可以把registry配成file类型,并通过service下的分组映射配置直接指定TC地址。这是最快跑通的方式。
4.3 全局事务注解和调用链XID透传
接入的核心代码其实只有一步:在事务入口方法上加全局事务注解。
@Service public class OrderServiceImpl implements OrderService { @GlobalTransactional(timeoutMills = 30000) @Override public boolean createOrder(OrderCreateDTO dto) { // 1. 写订单库 orderMapper.insert(buildOrderEntity(dto)); // 2. 扣库存(Feign调用) inventoryClient.deduct(dto.getSkuId(), dto.getQuantity()); // 3. 扣余额(Feign调用) accountClient.decrease(dto.getUserId(), dto.getAmount()); // 4. 加积分(Feign调用) scoreClient.increase(dto.getUserId(), dto.getScore()); return true; } }这个注解会在方法开始时向TC注册全局事务,拿到XID;方法正常结束则触发全局提交,抛出异常则触发全局回滚。注意这个注解应该加在业务链路的最外层入口,而不是加在每个服务内部方法上。如果每个服务自己的方法也加全局事务注解,会导致重复注册全局事务、嵌套全局事务,反而把链路搞乱。
Seata的starter会自动为Feign和RestTemplate装配XID透传拦截器,所以只要你的下游服务同样引入了Seata依赖并开启了Seata,XID会自动从请求头传递并绑定到下游线程,下游执行的数据操作就会自动注册为同一个全局事务的分支。下游服务内部原有的本地事务注解不需要删除,它是Seata分支事务执行时的本地事务边界,两者分工不同,保留反而更安全。
4.4 别忘了给每个业务库都建undo_log表
AT模式有个硬性前提:每一个参与全局事务的业务数据库,都必须建立一张undo_log表。这张表用来存放分支事务执行时的数据镜像和回滚信息,回滚时靠它生成反向SQL。建表SQL长这样:
CREATE TABLE IF NOT EXISTS `undo_log` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT 'increment id', `branch_id` BIGINT NOT NULL COMMENT 'branch transaction id', `xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id', `context` VARCHAR(128) NOT NULL COMMENT 'undo_log context', `rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info', `log_status` INT NOT NULL COMMENT '0:normal status,1:defense status', `log_created` DATETIME NOT NULL COMMENT 'create datetime', `log_modified` DATETIME NOT NULL COMMENT 'modify datetime', UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = 'AT transaction mode undo table';有几个细节值得强调。第一,这张表要建在业务数据库里,不是建在Seata Server的库里;有多少个业务库参与,就要建多少张。第二,xid和branch_id的联合唯一索引一定不能少,它是定位每条分支回滚日志的依据。第三,如果你在多个环境(开发、测试、生产)都跑这套,每个环境的业务库都要单独检查这张表是否落到位,这种基础表的遗漏往往是最难排查的问题来源。
另外要提醒一句:Seata的AT模式建立在JDBC数据源代理之上,如果你的项目用了某些特殊数据源框架或者读写分离代理,要确认它是否兼容Seata的数据源代理,否则可能出现分支事务没被拦截、回滚失效的问题。
5. 实战撞墙记录:Seata最常见的几个坑
框架接入简单,不代表运行顺利。下面这些坑我在项目里都真实遇到过,有些是从报错信息里一点点定位出来的,有些是问了很多人才恍然大悟的。把它们记录下来,希望能帮你少走弯路。
5.1 报错找不到undo_log表,项目起不来
最常见的首坑:全局事务一执行,日志刷出一串找不到undo_log表之类的错误。原因几乎都是业务库漏建了undo_log表。尤其当你的微服务链路很长、涉及四五个业务库时,很容易只给其中一个库建了表,其他库漏掉。排查方法很简单,把所有参与全局事务的库逐个检查一遍,确认表都存在。另外,有些同学把表建到了Seata Server的库,那也是无效的,业务库必须自己建。
5.2 全局锁等待超时,接口被拖垮
AT模式在数据操作完成后要获取全局锁,这个锁存在TC的锁表里。如果两个全局事务同时修改同一行数据,后到的那个事务拿不到锁,就会一直等待,默认等待时间超过阈值后会抛全局锁等待超时的异常。我在一次压测里就撞上过这个问题:促销活动导致同一商品SKU的库存行被大量并发修改,AT模式的全局锁直接把接口打到了超时。
处理思路有几个层次。第一,把事务范围尽量缩小,别在一个全局事务里做太多无关操作,锁的持有时间越短,冲突概率越低。第二,保证被修改行的主键或索引效率,锁粒度基于行,排查有没有因为缺少索引退化成锁表级冲突。第三,热点行并发特别高的场景,就需要考虑切到TCC模式或者重试策略了。还有一个歪门但有效的办法:把热点行数据做拆分,比如把单个库存行拆成多个子库存段,把并发压力分散到不同行上,这类设计在真正的秒杀系统里很常见。
5.3 回滚失败,数据对不上
回滚阶段也会出幺蛾子。AT模式回滚时会比对after image与当前库数据是否一致,如果不一致,说明在全局事务执行期间,有别的本地事务修改了这条数据,此时Seata默认会拒绝自动回滚并抛出异常,避免覆盖别人的修改。这种"脏写"冲突暴露出来的通常是业务层的隔离性设计问题,所谓没有隔离级别、又没有锁机制保护,才导致回滚时发现数据早已面目全非。
解决方向:一是从业务上避免全局事务执行期间对同一批数据做并发修改,比如加业务状态字段做条件更新;二是如果实在无法避免,在回滚失败后要有报警和对账机制。TCC模式下还有空回滚、悬挂问题,需要自己在Try/Confirm/Cancel里做幂等保护,我在前面章节已经提到过。总之,分布式事务不是"加个注解就永不数据不一致",它解决的是协调问题,业务并发上的错误设计它兜不住。
5.4 版本不一致引发的怪问题
Seata客户端和Server端的版本如果相差较远,会出现各种奇怪的注册失败、分支事务异常。最初我用1.5的Server去接1.7的客户端,结果分支注册后一直报错,看日志也看不出明显原因,最后把两边版本统一到1.7才恢复正常。所以请务必把Server、所有客户端的Seata依赖版本对齐,升级时也要一起升级,这个习惯能为你节省大量排查时间。
还有一个和版本相关的点:不同大版本的配置项名称有变化。1.5之后把老版本的多套配置合并到了一起,如果你在网上搜到的是两年前的教程,照着老配置去改新版本,大概率会踩配置不生效的坑。建议以你实际使用的版本对应文档为准。
5.5 常见问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 找不到undo_log表 | 业务库漏建表 | 逐个业务库执行建表SQL |
| 全局锁等待超时 | 热点行全局锁冲突 | 缩小事务范围、优化索引、换TCC |
| XID为null,分支未归入全局事务 | 异步线程丢失上下文 | 手动绑定XID或避免异步分支 |
| 回滚失败,镜像校验不一致 | 事务期间数据被并发修改 | 业务条件更新、对账补偿机制 |
| 版本不兼容导致的注册异常 | Server与客户端版本不一致 | 统一版本号 |
| 配置文件不生效 | 新老版本配置项不同 | 按当前版本的官方配置模板修改 |
6. 最后分享几条实用心得
文章写到这里,核心的东西已经讲完了。最后说几句我在实际项目中反复体会到的经验,也许比前面那些技术细节更能帮助到你。
第一,分布式事务是"最后手段",不是"第一选择"。如果一个业务可以通过异步消息加对账解决,或者可以通过调整表结构、接口顺序避免跨库强一致,那就不必上Seata。全局锁、日志表的开销是实打实的,能不引入就别引入,这是省钱也是省心。第二,Seata的AT模式对大部分业务已经够用,不要因为网上有人吐槽AT性能就盲目切TCC,TCC的维护成本远超你的预期,我见过太多项目TCC三个方法写得稀烂,最后引入的bug比解决的问题还多。第三,生产环境一定要把Seata Server的存储从file切到db,并且给Server做集群部署,否则TC一挂,所有全局事务全部停摆,那场面比单点数据库宕机还难受。第四,上线前务必做一次回滚演练,别只在正常提交路径上跑通就上线,回滚路径才是分布式事务最容易出问题的部分。
如果你把Seata用起来了,我建议后续可以再花点时间研究它的监控指标,把全局事务提交率、分支注册失败数、全局锁等待时间这些指标接进告警。分布式事务的运作状态,比接口成功率更能反映系统的真实健康度。踩过的坑越多,越会发现,分布式事务框架解决的只是"如何一致"的问题,而"要不要强一致""能容忍多长时间的最终一致"这些问题的答案,永远在业务设计手里。