ARTICLE DETAIL

资讯详情

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

Seata四种分布式事务模式详解:从AT到TCC选型与实战

Seata四种分布式事务模式详解:从AT到TCC选型与实战 做后端的人迟早会碰上这么一个问题明明下单接口在本地环境跑得好好的一上微服务就成了“薛定谔的订单”——订单表里有一条记录库存却还是满的。单体时代这种事根本不存在一个数据库事务包起来要么全成功要么全失败。但服务一拆订单服务和库存服务各管各的库本地事务只能约束自己那一段跨服务的数据一致性一下就没了抓手。这也是为什么 Spring Cloud 体系里Seata 几乎是分布式事务的默认答案。这篇文章不讲理论八股直接以“订单创建 库存扣减”这个最经典的业务场景为例把 Seata 的 AT、TCC、Saga、XA 四种模式挨个拆开讲包括它们怎么落地、怎么选型以及我在生产环境里踩过的坑。适合正在做 Spring Cloud 微服务、被数据一致性问题困扰的同学花二十分钟读一遍至少能少走一个月的弯路。1. 微服务拆分之后事务到底断在了哪里1.1 从“一个事务”变成“一串操作”单体应用里下单的逻辑很简单开启事务插入订单扣减库存更新余额提交事务。所有操作都在同一个数据库连接上执行数据库的 ACID 帮我们兜底代码里甚至不用写太多补偿逻辑。微服务拆开之后订单和库存变成了两个独立的服务、两个独立的数据库原来那一个事务被拆成了两个乃至多个本地事务。下单接口先调订单服务写订单再通过 OpenFeign 调库存服务扣库存这两个操作之间没有任何机制能保证“要么都成功要么都失败”。很多团队在初期设计时根本意识不到这是问题直到线上出现库存被超卖、订单状态和实际扣减对不上才回过头来补课。从本质上看微服务架构把“本地事务”换成了“跨服务调用链条”之后事务的原子性边界被打破了。本地事务管不到对端对端的异常对本地来说只是一个 RPC 响应码本地事务只能决定自己提交还是回滚没办法替对端做决定。这就是事务断层的根源。1.2 分布式事务维护的到底是什么要理解 Seata 在做什么可以先想清楚一个问题分布式事务维护的其实是“多个数据源之间的最终一致性”。单体事务追求的是强一致提交那一刻所有数据要么全可见、要么全不可见。微服务场景下大多数业务其实可以接受短时间内的数据不一致——订单先落库、库存暂未扣减只要最终能对上账就行这个过程就叫最终一致性BASE 模型里的“软状态 最终一致”。分布式事务框架的核心工作就是在这段“不一致窗口期”里做协调和兜底如果全部服务都成功那就把数据慢慢收敛到最终状态如果有任一分支失败就触发回滚或补偿把已经写出去的数据改回来。Seata 的四种模式本质上是在“一致性强度”和“性能损耗”之间做不同的取舍。AT 和 XA 偏向强一致TCC 和 Saga 偏向业务可控的最终一致。后面会具体展开。1.3 不是所有场景都该上分布式事务这里必须先泼一盆冷水分布式事务是有成本的性能和开发复杂度都不低不要一遇到多个服务写数据就无脑引入 Seata。我见过不少项目明明可以通过接口设计规避比如“先扣库存再生成订单”、“扣库存只发消息让订单服务异步落库”硬是上了分布式事务结果全局锁拖慢了接口上线第一天就压测不过。什么时候真的需要我的判断标准很简单跨服务、跨库写操作在同一业务请求里必须保持最终一致且无法通过异步削峰或业务流程重排解决。典型的如订单创建扣库存、支付回调后更新订单与账务、资金转账。如果只是同步一个查询缓存、写一个日志表完全没必要引入 Seata。先把方案设计做对再谈框架选型这是我第一个建议。2. 先把 Seata 的整体骨架讲透TC、TM、RM 与全局事务流转2.1 Seata 的三个角色和部署形态Seata 的架构里有三个角色初次上手的人特别容易搞混。TCTransaction Coordinator是事务协调器也就是我们要单独部署的 Seata Server负责全局事务的创建、提交、回滚决策TMTransaction Manager是事务管理器嵌入在发起全局事务的那个服务里一般就是指标了 GlobalTransactional 的方法所在的服务RMResource Manager是资源管理器负责管理各分支事务的资源比如订单服务里操作数据库的那段逻辑就是一个 RM 分支。部署形态上Seata Server 是一个独立的 Java 进程默认监听 8091 端口可以注册到 Nacos、Consul、Eureka 等注册中心。我这边生产环境用的拓扑是Nacos 做注册中心和配置中心Seata Server 以集群方式部署事务会话和全局锁信息存到 MySQL 里客户端服务通过注册中心发现 Server 地址。这种部署方式的好处是客户端不用硬编码 Server 地址Server 挂了还能靠集群内部的选举机制切换。2.2 一次 GlobalTransactional 的完整生命周期拿“订单创建 库存扣减”来说TM 在订单服务的 createOrder 方法上加了 GlobalTransactional整个流程可以拆成四个阶段。第一阶段TM 向 TC 发起全局事务注册TC 生成一个全局唯一的 XID并把 XID 放入调用链上下文通过 Feign 的请求头透传给库存服务。第二阶段订单服务执行本地事务插入订单库存服务收到 XID 后执行本地事务扣减库存这两个本地事务各自提交同时告诉 TC“我这个分支干完了”。第三阶段TC 收到所有分支的结果后做决策。如果都成功TC 通知所有分支提交全局事务这里的“提交”在不同模式下含义不同AT 模式下就是把之前生成的 undo_log 清理掉。第四阶段如果有分支失败TC 通知所有分支回滚各分支根据 undo_log 生成反向 SQL 恢复数据。整个流程就是典型的“两阶段提交”思想但 Seata 在细节上做了大量优化让大部分分支事务在一阶段就提交了本地事务释放数据库行锁只在需要回滚时才做补偿操作。2.3 分支事务、全局锁与 undo_log 各自干什么很多同学初次看 Seata 源码会觉得晕其实只要抓住三个概念就行。分支事务是全局事务里每个参与者的本地事务每个分支都有独立的 Branch ID最终结果由 TC 汇总。全局锁是 Seata 在 AT 模式下引入的一种分布式锁用来防止两个全局事务并发修改同一行数据时互相覆盖。undo_log 是每个参与 AT 模式的业务库必须要建的一张表用来记录数据变更前后的快照。把这三个概念串起来理解 AT 模式的回滚原理一阶段业务修改数据前Seata 通过 DataSourceProxy 拦截 SQL把当前数据“前镜像”和“后镜像”写入 undo_log如果二阶段需要回滚就根据 undo_log 里的镜像数据生成反向 SQL把数据恢复成修改前的样子。这也解释了为什么 AT 模式对业务代码几乎没有侵入——它是在数据源层面做的代理拦截业务只需要加一个注解。3. AT 模式实战订单服务 库存服务落地全过程3.1 环境准备Seata Server 与注册配置中心AT 模式是 Seata 最主打、也是我用得最多的模式先把环境搭好。我用的是 Seata 1.8.x 主线版本搭配 Spring Cloud Alibaba 2.2.x注册中心和配置中心都走 Nacos。Seata Server 部署有两种常见方式一种是下载官方发行包后执行 seata-server.sh 启动# 指定端口、注册方式、事务日志存储方式 ./bin/seata-server.sh -p 8091 -h 127.0.0.1 -m file另一种是 Docker 方式适合快速验证docker run --name seata-server \ -p 8091:8091 \ -e SEATA_IP127.0.0.1 \ -e STORE_MODEfile \ apache/seata-server:1.8.0我建议不要把事务日志存默认的 file 模式直接上生产至少换成 DB 模式把 global_table、branch_table、lock_table 三张表建到 MySQL 里否则 Seata Server 一重启正在执行中的全局事务信息就全丢了。这里有个细节DB 模式下需要在 store 配置里指定数据库驱动、连接串、用户名密码不同版本的建表 SQL 略有差异最好到对应版本的源码目录里拿。3.2 业务库准备undo_log 表与数据源代理AT 模式下每个参与分支事务的业务数据库都必须建 undo_log 表这是最容易漏的一步漏了之后分支回滚会直接报错。MySQL 下的建表语句是官方标准模板CREATE TABLE undo_log ( id bigint(20) NOT NULL AUTO_INCREMENT, branch_id bigint(20) NOT NULL, xid varchar(100) NOT NULL, context varchar(128) NOT NULL, rollback_info longblob NOT NULL, log_status int(11) NOT NULL, log_created datetime NOT NULL, log_modified datetime NOT NULL, ext varchar(100) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY ux_undo_log (xid,branch_id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;这里要补充两个实战心得。第一undo_log 的 unique key 用的是 (xid, branch_id)保证每个分支事务只有一条回滚记录重复执行时会冲突这也是幂等性的一层保障。第二字符集一定要用 utf8不要图省事用 utf8mb4 之外自建的字集否则特殊字符写入镜像数据时可能出问题。表建好之后还要把数据源换成 Seata 代理数据源低版本需要自己定义 DataSourceProxy Bean高版本 seata-spring-boot-starter 在配置了 seata.data-source-proxy-mode: AT 之后会自动包装我这边用的就是自动代理省了不少样板代码。3.3 核心实现Feign 调用 GlobalTransactional 注解环境就绪后业务代码反而很简洁。订单服务里库存扣减通过 OpenFeign 调用核心方法标注 GlobalTransactionalService public class OrderService { Resource private OrderMapper orderMapper; Resource private InventoryClient inventoryClient; GlobalTransactional(name create-order-tx, rollbackFor Exception.class) public void createOrder(OrderDTO orderDTO) { // 第一步写订单主表和明细本地事务 orderMapper.insert(orderDTO); // 第二步远程扣减库存库存服务同样参与同一个全局事务 inventoryClient.deduct(new DeductRequest(orderDTO.getProductId(), orderDTO.getCount())); } }库存服务这边的扣减接口就是一个普通的业务方法但从调用链上下文里能拿到 XID因此它自己执行数据库操作时同样会被纳入全局事务管理。这里有个关键配置Feign 调用必须开启 Seata 的上下文传递Spring Cloud Alibaba 的集成包默认支持不需要写额外拦截器但要注意别在远程调用前把线程池、异步任务里的 XID 搞丢异步线程里如果也需要参与事务得手动做 XID 的绑定和恢复。实际部署时还会遇到一个高频问题GlobalTransactional 的 name 属性对同一个服务里的不同方法要取不同名字并且 rollbackFor 务必要写 Exception.class。Spring 的默认回滚策略只针对 RuntimeException 和 Error如果 Feign 接口抛出的是 checked Exception不设置 rollbackFor 全局事务不会回滚这是新手踩得最多的坑之一。3.4 AT 模式回滚的关键细节与注意事项AT 模式看起来无侵入但落地时有三条红线必须守住。第一业务 SQL 必须能被 Seata 正确解析简单的 INSERT、UPDATE、DELETE 都没问题但如果 SQL 里带了函数、聚合、子查询、LIMIT或者更新条件没有走唯一索引Seata 生成前后镜像时可能解析失败或锁范围变大回滚时也有风险。第二不要在同一条数据上跨多个全局事务并发修改AT 模式靠全局锁保证隔离性两个事务同时抢同一行时后到的一方会重试等待重试超时报锁冲突接口变慢。第三分支事务执行完后本地事务一定要提交别自己手动 rollback否则 undo_log 和数据状态对不上。我生产上的一个真实案例库存服务扣减逻辑用了 UPDATE stock SET count count - ? WHERE product_id ? AND count ?这条 SQL 本身没问题但因为 product_id 在线上库不是唯一索引全局锁退化成范围锁并发一高就频繁锁冲突。后来把库存表的主键改成 product_id并把扣减条件简化锁冲突明显下降。这类问题在压测之前很难暴露所以建议所有参与 AT 模式的表主键和更新条件都设计成唯一的能少踩很多坑。4. TCC / Saga / XA 三种模式怎么上手4.1 TCC 模式三方法手动管控以库存冻结为例TCC 模式把每个分支事务拆成 Try、Confirm、Cancel 三个方法框架负责按顺序调用Try 阶段做资源检查和预留Confirm 阶段做正式提交Cancel 阶段做补偿回滚。相比 AT 模式的自动生成镜像TCC 把业务规则完全交给你自己灵活度最高但也意味着每个参与方都要写三套逻辑。库存扣减用 TCC 落地时我的做法是在业务表之外再加一张冻结表先把要扣的数量“冻住”LocalTCC public interface InventoryTccAction { TwoPhaseBusinessAction(name deductStockTcc, commitMethod confirmDeduct, rollbackMethod cancelDeduct) boolean tryDeduct( BusinessActionContextParameter(paramName productId) Long productId, BusinessActionContextParameter(paramName count) Integer count); boolean confirmDeduct(BusinessActionContext context); boolean cancelDeduct(BusinessActionContext context); }实现类里Try 阶段先校验库存余量然后执行 UPDATE stock SET freeze_count freeze_count ? WHERE product_id ? AND stock_count ?成功就插入一条冻结记录Confirm 阶段把冻结转成真实扣减也就是把 stock_count、freeze_count 同步调整Cancel 阶段把冻结数量释放回去。三个方法都要保证幂等Confirm 和 Cancel 绝不能因为重复调用产生叠加效果。TCC 最大的好处是性能好事务粒度可控最大的坏处是开发成本高而且空回滚Try 还没成功就执行了 Cancel、悬挂Cancel 先于 Try 到达这类问题都要自己防一般用一张控制表记录 XID 和分支状态来解决。4.2 Saga 模式状态机编排的长事务补偿Saga 模式是四种模式里最适合长事务的它把一个全局事务拆成一串本地事务每个本地事务都配一个补偿事务。执行到第 N 步失败时从第 N-1 步开始反向执行补偿。Seata 的 Saga 主要基于状态机引擎实现事务流程定义在一个 JSON 文件里由引擎按定义逐步调用服务方法。比如先创建订单再扣减库存再扣减余额每个节点都配一个 compensateState{ name: create-order-saga, start: { stateId: createOrder }, states: { createOrder: { type: ServiceTask, serviceName: orderService, serviceMethod: createOrder, compensateState: compensateOrder, next: deductStock }, deductStock: { type: ServiceTask, serviceName: inventoryService, serviceMethod: deductStock, compensateState: compensateDeduct, next: end }, compensateOrder: { type: ServiceTask, serviceName: orderService, serviceMethod: compensateOrder }, compensateDeduct: { type: ServiceTask, serviceName: inventoryService, serviceMethod: compensateDeduct } } }调用入口就一个方法把状态机名字和业务参数丢进去即可。Saga 模式不需要全局锁长事务也不会长时间占住数据库资源这是它最大的优势。代价是什么它牺牲了隔离性别的请求在补偿完成前可能读到中间状态的数据而且补偿逻辑的业务规则要自己设计不像 AT 那样自动生成。我一般只在跨系统、跨公司接口的长流程里用 Saga比如聚合支付平台的多渠道结算。4.3 XA 模式把一致性交给数据库XA 模式是四种模式里最标准的强一致方案它直接复用数据库原生支持的 XA 两阶段提交协议。MySQL 的 InnoDB、Oracle、PostgreSQL 都支持 XASeata 要做的就是把多个数据源纳入同一个全局事务让数据库自己完成 prepare、commit、rollback 三个过程。XA 是真正的强一致不存在中间脏数据业务代码侵入比 TCC 小得多只需要在配置里把数据源代理模式切到 XAseata: >
返回列表