楼层: 首页/ 软件技术/ Spring Cloud 微服务/ 分布式事务:Seata
九

分布式事务:Seata

Distributed Transactions with Seata

第六个终极难题:跨服务转账,A 扣了钱 B 没加上怎么办?单体里一个 @Transactional 全搞定。微服务里 A 银行扣钱和 B 银行加钱是两个服务、两个数据库,本地事务管不了跨库。这就是分布式事务,业界方案一堆,阿里开源的 Seata 是国内用得最多的。

论先懂 CAP,再谈 Seata

CAP:一致性(Consistency)、可用性(Availability)、分区容错(Partition tolerance)。分布式系统网络一定会断,所以 P 必须保,剩下 C 和 A 二选一。分布式事务就是在这个取舍下找平衡。

BASE:基本可用(Basically Available)、软状态(Soft State)、最终一致(Eventually Consistent)。大部分互联网业务不用强一致,接受"最后一致"就行——钱扣了,过 2 秒对方加上,用户能接受。

Seata 四种模式

模式原理适合场景
AT自动生成回滚 SQL,对业务代码零侵入(加个注解就行)。Spring Cloud 业务服务,最常用。
TCCTry/Confirm/Cancel 三步,业务自己实现三个接口。对性能和一致性要求高的资金类业务。
Saga长事务,每步失败就反向执行补偿动作。跨系统、流程长的业务(订票、审批)。
XA数据库层面的两阶段提交,强一致但性能差。传统企业、数据库支持 XA 的场景。

Seata 三个角色

Seata 架构就三个角色,记住缩写就行:

TC(Transaction Coordinator) → 事务协调者
独立部署的 Seata Server,管全局事务的状态。
大白话:大管家,谁开始事务、谁提交、谁回滚,它都记着。
TM(Transaction Manager) → 事务管理器
发起全局事务的那个服务,比如下单服务。
大白话:项目经理,跟 TC 说"我要开一个全局事务了"。
RM(Resource Manager) → 资源管理器
每个参与分支事务的服务,管自己那部分数据库。
大白话:干活的,听 TC 指挥提交或回滚自己那一笔。

业务代码用 AT 模式,就加一个注解 @GlobalTransactional

import io.seata.spring.annotation.GlobalTransactional; @Service public class OrderService { // 开一个全局事务:整个方法要么全成功,要么全回滚 @GlobalTransactional public void createOrder(Order order) { // 1. 订单服务自己插订单 orderDao.insert(order); // 2. 远程调账户服务扣钱(Feign) accountClient.deduct(order.getUserId(), order.getMoney()); // 3. 远程调库存服务扣库存 storageClient.deduct(order.getProductId(), order.getCount()); // 任何一步抛异常,Seata 自动把前面做的全回滚 } }

AT 模式内部怎么工作(搞懂就不慌)

AT 模式是 Seata 最常用的,对业务零侵入。它的原理是"两阶段提交 + 自动补偿":

阶段一:业务 SQL 执行,同时记镜像
每个分支事务提交本地事务前,Seata 自动解析你写的 SQL,把变更前的数据(before image)和变更后的数据(after image)记到 undo_log 表里,然后本地事务提交。
大白话:动手前先拍两张照片——改之前长啥样、改之后长啥样,存着备用。
阶段二:全部成功就提交,有失败就回滚
所有分支都成功,TC 通知各 RM 删 undo_log(因为不需要回滚了)。任何一个分支失败,TC 通知各 RM 拿 before image 反向生成 UPDATE 语句,把数据改回去。
大白话:全成了就把照片删了;有一个环节搞砸了,照着"改之前"那张照片把数据还原。

每个参与 Seata 的业务库,必须建一张 undo_log 表(Seata 官方脚本里有)

-- Seata AT 模式要求每个分支库都建这张表 CREATE TABLE undo_log ( branch_id BIGINT NOT NULL COMMENT '分支事务 ID', xid VARCHAR(128) NOT NULL COMMENT '全局事务 ID', context VARCHAR(128) NOT NULL, rollback_info LONGBLOB NOT NULL COMMENT '前后镜像', log_status INT NOT NULL, log_created DATETIME DEFAULT CURRENT_TIMESTAMP, log_modified DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT 'Seata AT 回滚日志表';
Seata 回滚失败的常见原因

AT 模式靠"记录前后镜像、失败时反向 UPDATE"来回滚。回滚失败通常是:① 分支事务提交后,这条记录又被别的业务改了(脏写,镜像对不上);② 数据库隔离级别太高,回滚 SQL 拿不到锁;③ TC 服务挂了,全局事务状态丢了。生产环境一定要配 TC 集群 + 持久化到数据库,别用内存模式。

本章面试题

面试快答 · 分布式事务

Q1. CAP 三选二,分布式注册中心为什么放弃 C?

参考答案

网络分区(P)无法避免必须保。注册发现场景下,分区时宁可各节点返回(可能略旧的)可用实例(A),也不能为了强一致整个注册中心停摆(牺牲 A)。所以 Nacos/Eureka 默认 AP;配置中心这类需要强一致的才切 CP。

Q2. Seata AT 模式为什么对业务无侵入?

参考答案

它拦截你的 SQL,自动在前后记录 before/after 镜像到 undo_log;阶段二全成功就删日志,失败就用 before 镜像反向生成 UPDATE 回滚。业务代码只写正常 SQL,加个 @GlobalTransactional 就行,不用手写 Try/Confirm/Cancel。

Q3. TCC 和 AT 怎么选?

参考答案

AT 无侵入、性能好、适合大多数互联网业务;TCC 要手写 Try/Confirm/Cancel 三个接口、开发成本高,但不受全局锁、更灵活,适合资金、库存扣减这种对并发和一致性要求极高的核心链路。

Q4. 本地消息表 / 最终一致了解吗?

参考答案

不用 Seata 的另一派:业务库建一张"消息表",本地事务里同时写业务数据和待发消息;后台任务扫表把消息发到 MQ,下游消费。靠"本地事务保证业务+消息同生共死"+MQ 重试实现最终一致。比 Seata 重业务,但不依赖 TC。