一、CAP定理与分布式事务的哲学矛盾
在深入Seata之前,必须先理解CAP定理对分布式事务的根本性制约。CAP(Consistency一致性、Availability可用性、Partition Tolerance分区容错性)告诉我们:三选二。但在分布式系统中,分区是物理事实——网络 끊开、光纤被挖、交换机故障都是常态,不是例外。这意味着我们实际上只能在C和A之间做取舍。
强一致性(C)要求所有节点在同一时刻看到相同的数据,这需要同步等待——也就是分布式事务的核心代价。可用性(A)则要求每次请求都能在有限时间内返回有效响应,不允许因等待其他节点而超时。
所以,当我们在分布式系统中说"需要强一致性事务"时,实际上是在说:"我愿意牺牲可用性来换取一致性。"这不是技术问题,是业务决策——金融支付场景必须强一致,而商品浏览场景最终一致即可。
1.1 为什么说"分布式事务是伪命题"
传统ACID事务依赖于单一数据库实例内部的物理协调机制:锁管理器、Redo日志、Undo日志、WAL(Write-Ahead Logging)——这些全部运行在共享内存和磁盘上,延迟在微秒级。当我们把事务拆到多个节点上,网络往返延迟从微秒级跳到毫秒级(局域网1-5ms,广域网50-200ms),事务持有锁的时间成百上千倍增长,数据库连接池很快耗尽。更致命的是:在等待协调的过程中,任何一个节点超时都会导致整个事务失败。
1.2 BASE理论:务实的最终一致
BASE(Basically Available、Soft state、Eventually consistent)是对CAP中C和A矛盾的业务化解:允许系统在不同时间看到不同的数据,但最终会收敛到一致状态。这是互联网公司处理高并发场景的主流选择。
/**
* 业务场景对比:强一致 vs 最终一致
*
* 场景A:银行转账(强一致,必须ACID)
* - 从账户A扣100元 → 账户B加100元
* - 任何中间状态(只扣没加,或只加没扣)都是不可接受的
* - 允许:事务时间长、吞吐低、部分节点故障时整体不可用
*
* 场景B:电商下单扣库存(最终一致,可接受短时不一致)
* - 订单创建 → 库存预扣(本地事务)→ 异步消息确认
* - 允许:库存超卖5%以内(补货或赔偿解决)
* - 要求:最终状态必须收敛(库存不能无限扣)
*/
// 架构决策树:什么时候必须分布式事务?
// ├─ 是否跨服务? 否 → 本地事务(数据库ACID)
// ├─ 是否接受短时不一致? 是 → 最终一致(Saga/消息队列)
// ├─ 是否是金融级场景? 是 → Seata AT或TCC(接受性能代价)
// └─ 是否必须强一致且高性能? → 重新设计业务边界(避免跨库事务)
二、2PC与3PC:协调协议的原点
Two-Phase Commit(2PC)是分布式事务的教科书方案,也是Seata所有模式的底层基础。理解2PC的缺陷,才能理解Seata为什么要做AT/TCC/Saga三种模式。
2.1 2PC的执行流程
/**
* 2PC两个阶段:
*
* Phase1(Prepare):协调者向所有参与者发送Prepare请求
* - 参与者执行本地事务(Undo日志已写入),但不提交
* - 持有所需的全部锁
* - 返回YES(可提交)或NO(有约束违反)
*
* Phase2(Commit/Rollback):协调者根据所有参与者的响应决定
* - 全YES → 发送Commit,所有参与者提交,释放锁
* - 任何NO → 发送Rollback,所有参与者回滚,释放锁
*
* 关键点:Prepare阶段参与者已经持有了所有行锁!
*/
/**
* 2PC的三个致命缺陷:
*
* 缺陷1:同步阻塞(Blocking)
* - 从Prepare到Commit期间,所有参与者的行锁都被持有
* - 其他事务必须等待,极易引发死锁和连接池耗尽
* - 参与者数量越多,持有锁的时间越长
*
* 缺陷2:单点故障(Single Point of Failure)
* - 协调者崩溃在Phase2刚发出Commit之前
* - 参与者不知道该Commit还是Rollback,会一直持有锁等待指令
* - 这就是"未知状态"问题:事务处于中间态,不知道该何去何从
* - 唯一的恢复手段是:等待协调者重启(30秒超时后?)
*
* 缺陷3:数据不一致(Inconsistent State)
* - 如果协调者发送Commit后崩溃,只有一部分参与者收到
* - 收到Commit的提交了,没收到的回滚了——永久不一致
* - 这在网络分区场景下几乎必然发生
*/
// 伪代码模拟2PC协调者
public class TwoPhaseCoordinator {
private List<Participant> participants = new ArrayList<>();
public boolean execute(Transaction tx) throws Exception {
// Phase1: Prepare
List<Future<Vote>> futures = new ArrayList<>();
for (Participant p : participants) {
// 异步发送Prepare给每个参与者
futures.add(executor.submit(() -> p.prepare(tx)));
}
// 收集所有投票(设置超时防止无限等待)
boolean allYes = true;
for (Future<Vote> f : futures) {
Vote v = f.get(prepareTimeout, TimeUnit.SECONDS);
if (v == Vote.NO || v == null) { // null = 超时/异常
allYes = false;
break;
}
}
// Phase2: 根据结果决定
if (allYes) {
broadcastCommit(tx); // 部分Commit失败 = 数据不一致!
} else {
broadcastRollback(tx);
}
return allYes;
}
}
2.2 3PC的改进与局限
Three-Phase Commit(3PC)试图通过引入"PreCommit"阶段来解决2PC的阻塞问题:
/**
* 3PC的三个阶段:
*
* Phase1 (CanCommit):协调者询问参与者是否可以提交
* - 不执行事务,只是探路
* - 参与者在超时后默认Abort(不无限等待)
*
* Phase2 (PreCommit):协调者发送PreCommit,所有参与者执行Undo日志
* - 仍然不提交,但已做好提交准备
* - 释放了部分锁(只保留关键资源的锁)
*
* Phase3 (DoCommit):协调者发送DoCommit,参与者提交
* - 如果协调者在Phase3崩溃,参与者等待DoCommit超时后自动提交
* - 这解决了2PC的无限阻塞问题
*
* 3PC的局限性:
* - 仍然无法解决数据不一致(Phase3网络分区)
* - 增加了网络往返,性能反而更差
* - 在asynchronous网络模型下仍然不是"完美"的
* - 工业界实际使用极少(只有部分研究性数据库使用)
*/
// 对比表格
// 特性 | 2PC | 3PC
// ----------------|------------|---------------
// 阻塞 | 是(严重) | 改善(但不完美)
// 单点故障 | 严重 | 改善(超时机制)
// 数据不一致 | 可能 | 可能(Phase3网络分区)
// 网络往返 | 2次 | 3次
// 工业使用 | XA/JDBC | 极少
三、Seata AT模式:无入侵的自动补偿
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里开源的分布式事务解决方案,目前在Apache孵化。AT模式是Seata的核心模式,它的核心思想是:用本地事务+全局锁替代全局锁管理器,实现无侵入的事务协调。
3.1 AT模式的全局锁机制
AT模式拆掉了传统2PC中"协调者持有一把全局锁"的架构,改为:
- TC(Transaction Coordinator):Seata服务端,负责管理全局事务生命周期和全局锁
- TM(Transaction Manager):业务侧,管理事务边界(通过@GlobalTransactional注解)
- RM(Resource Manager):资源管理器,嵌入到每个数据库中,负责分支事务
/**
* AT模式的工作原理:
*
* 1. TM发起全局事务(TM向TC注册,获取XID)
* 2. 每个本地业务SQL执行前,RM自动:
* a. 向TC申请行级X锁(如果拿不到则等待或失败)
* b. 记录"前镜像"(Before Image):执行SQL前的数据快照
* c. 执行SQL
* d. 记录"后镜像"(After Image):执行SQL后的数据快照
* e. 记录Undo Log(用于回滚)
* 3. 全部成功后,全局提交:TC异步清理行锁和Undo Log
* 4. 任一失败,全局回滚:TC通知所有RM用Undo Log回滚
*
* 关键设计:全局锁的获取是在数据库行锁层面实现的
* 锁信息保存在TC(Seata Server),但通过唯一ID(XID+Branch ID)管理
* 避免了2PC中"全局锁管理器"这个单点
*/
/**
* 快照机制详解(前镜像/后镜像)
*
* 前镜像(Before Image):SELECT快照
* 执行 DELETE FROM account WHERE id = 100;
* 前镜像:SELECT * FROM account WHERE id = 100; → {id:100, balance:1000}
*
* 后镜像(After Image):执行后快照
* 执行后:SELECT * FROM account WHERE id = 100; → {id:100, balance:0}
*
* Undo Log记录:
* {
* tableName: "account",
* beforeImage: [{id:100, balance:1000}],
* afterImage: [{id:100, balance:0}],
* sqlType: "DELETE",
* primaryKeys: ["id"]
* }
*
* 回滚时:将beforeImage反向执行即可恢复
*/
// AT模式的隔离级别
// Seata默认提供"读已提交"级别的全局隔离
// 全局锁机制保证:在全局事务执行期间,
// 其他全局事务无法修改同一行数据(防止脏写)
// 但读未提交(脏读)仍然可能发生——这是AT模式与生俱来的限制
3.2 Spring Boot + Seata AT模式实战
// ============ Step1: 项目依赖 ============
// pom.xml
<dependencies>
<!-- Spring Boot starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- Seata AT模式:自动装配 + DataSource代理 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 数据库驱动(这里用MySQL) -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- 连接池(Seata需要能够包装DataSource) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
// ============ Step2: application.yml 配置 ============
/**
* 关键配置说明:
* - tx-service-group:事务分组,用于客户端发现TC服务器
* - vgroup-mapping:事务分组 → TC集群地址的映射
* Seata支持基于事务分组的负载均衡,一个分组可以对应多个TC节点
* - enable-auto-data-source-proxy:自动代理DataSource(核心!不加=AT不生效)
* - client.rm.report-success-enable:事务成功时是否上报TC(关闭可减少网络开销)
*/
@Configuration
public class SeataConfig {
// 如果使用Nacos作为配置中心,可以在这里注入配置
// Seata支持Nacos/Apollo/ZooKeeper等多种配置中心
}
// ============ Step3: 全局事务业务代码 ============
/**
* @GlobalTransactional 注解的原理:
* 1. TM向TC注册全局事务,获取XID(全局事务ID)
* 2. XID通过Dubbo的RpcContext或Feign的RequestHeader传播到所有服务
* 3. 每个RM通过DataSourceProxy拦截SQL,自动注册分支事务
* 4. TM所在服务执行完业务后,通知TC提交或回滚
*/
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignClient accountFeignClient; // Feign调用账户服务
@Autowired
private StorageFeignClient storageFeignClient; // Feign调用库存服务
/**
* 分布式事务场景:创建订单
* - 扣减账户余额(远程调用)
* - 扣减商品库存(远程调用)
* - 创建本地订单记录(本地数据库)
*
* 任何一个环节失败,全局回滚!
*/
@Override
@GlobalTransactional(rollbackFor = Exception.class, name = "create-order")
public void createOrder(OrderDTO orderDTO) {
// 1. 远程调用:扣减账户余额
// Feign拦截器会自动将XID传递到账户服务
accountFeignClient.deductBalance(orderDTO.getUserId(), orderDTO.getTotalAmount());
// 2. 远程调用:扣减库存
// 库存服务通过GlobalTransactional或者全局锁保护库存行
storageFeignClient.deductStock(orderDTO.getProductId(), orderDTO.getQuantity());
// 3. 本地事务:创建订单记录
// Seata DataSourceProxy自动拦截此SQL,注册分支事务
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setQuantity(orderDTO.getQuantity());
order.setTotalAmount(orderDTO.getTotalAmount());
order.setStatus("CREATED");
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
// 4. 人为模拟失败(测试回滚用)
// throw new RuntimeException("模拟业务异常,观察全局回滚!");
}
}
四、Seata TCC模式:业务层的精控三阶段
TCC(Try-Confirm-Cancel)是Seata的第二种模式,与AT模式的核心区别在于:TCC不依赖Undo Log,而是在业务层通过代码手动实现补偿逻辑。这让它适合任何类型的存储系统(MySQL、Redis、MongoDB、HTTP服务等)。
4.1 Try-Confirm-Cancel三阶段设计
/**
* TCC三阶段详解:
*
* Try(预留/检查阶段)
* - 尝试执行所有业务操作
* - 进行资源检查(如库存是否充足)
* - 预留资源(如冻结库存:将可用库存-1,冻结库存+1)
* - 这一步是"假执行",只是做检查和标记
* - 任何资源预留失败,立即执行Cancel释放
*
* Confirm(确认阶段)
* - 真正提交业务
* - 使用Try阶段预留的资源(如将冻结库存变为真正的扣减)
* - 只处理Try阶段成功的情况
* - 如果Confirm失败?TC会无限重试(幂等性保证)
*
* Cancel(取消/回滚阶段)
* - 释放Try阶段预留的资源(如解冻库存)
* - 补偿Try阶段的操作
* - 如果Cancel失败?TC也会无限重试
*
* TCC的核心挑战:业务侵入性强(需要业务方实现三个方法)
* 但这换来的是:性能高(无Undo Log)、适用范围广(任何系统)
*/
/**
* TCC与AT的本质区别:
*
* AT模式:
* - 侵入性低:自动代理SQL,生成Undo Log
* - 性能好(无分布式锁):TC只记录锁,不阻塞SQL执行
* - 局限:只支持有SQL语义的场景
*
* TCC模式:
* - 侵入性高:需要业务方实现三个方法
* - 性能最优:无全局锁等待,无Undo Log生成
* - 适用范围:任意存储系统、远程服务
*/
// TCC事务参与者接口
public interface TccAction {
/**
* Try阶段:预留资源
* @param businessKey 业务键(用于定位这次预留)
* @param reserveInfo 预留信息(如扣款金额)
* @return true=预留成功,进入Confirm;false=预留失败,进入Cancel
*/
boolean tryReserve(String businessKey, Object reserveInfo);
/**
* Confirm阶段:确认执行
* @param businessKey 与Try阶段对应
* @param confirmInfo 确认信息
* @return 必须返回true(失败会重试直到成功)
*/
boolean confirm(String businessKey, Object confirmInfo);
/**
* Cancel阶段:回滚/释放预留
* @param businessKey 与Try阶段对应
* @param cancelInfo 取消原因
* @return 必须返回true(失败会重试直到成功)
*/
boolean cancel(String businessKey, Object cancelInfo);
}
4.2 TCC模式实战:账户服务扣款
// ============ TCC账户服务实现 ============
/**
* TCC模式下的账户服务设计:
*
* Try阶段:检查余额是否充足,如果充足则"冻结"相应金额
* 账户表增加 frozen_amount 字段
* UPDATE account SET frozen_amount = frozen_amount + ? WHERE user_id = ?
* AND (balance - frozen_amount) >= ?
*
* Confirm阶段:真正扣款
* UPDATE account SET balance = balance - frozen_reduce,
* frozen_amount = frozen_amount - frozen_reduce
* WHERE user_id = ?
*
* Cancel阶段:解冻金额
* UPDATE account SET frozen_amount = frozen_amount - frozen_reduce
* WHERE user_id = ?
*/
@Service
@LocalTCCAnnotation // 标识为本地TCC参与者(供TM调用)
public class AccountTccServiceImpl implements AccountTccAction {
@Autowired
private AccountMapper accountMapper;
@Override
@TwoPhaseBusinessAction( // 标识这是一个TCC的Try方法
name = "deductAccountTcc",
commitMethod = "confirm",
rollbackMethod = "cancel"
)
public boolean tryReserve(
// actionContext会由Seata框架注入,包含XID/BranchID等事务上下文
BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
) {
String xid = actionContext.getXid();
Long branchId = actionContext.getBranchId();
log.info("[TCC Try] xid={}, branchId={}, userId={}, amount={}", xid, branchId, userId, amount);
try {
// 冻结金额:检查余额,冻结amount
// 核心SQL:更新 frozen_amount = frozen_amount + amount
// 条件:balance - frozen_amount >= amount(可用余额足够)
int updated = accountMapper.freezeAmount(userId, amount);
return updated > 0; // 返回true表示Try成功
} catch (Exception e) {
log.error("[TCC Try] 冻结失败: userId={}", userId, e);
return false; // Try失败,TC会调用Cancel
}
}
@Override
public boolean confirm(BusinessActionContext actionContext) {
String userId = actionContext.getActionContext("userId", String.class);
BigDecimal amount = actionContext.getActionContext("amount", BigDecimal.class);
log.info("[TCC Confirm] userId={}, amount={}", userId, amount);
// 真正扣款:从balance扣除,从frozen_amount释放
accountMapper.confirmDeduct(userId, amount);
return true; // Confirm必须返回true,失败会重试
}
@Override
public boolean cancel(BusinessActionContext actionContext) {
String userId = actionContext.getActionContext("userId", String.class);
BigDecimal amount = actionContext.getActionContext("amount", BigDecimal.class);
log.info("[TCC Cancel] userId={}, amount={}", userId, amount);
// 解冻:释放冻结的金额
accountMapper.unfreezeAmount(userId, amount);
return true; // Cancel必须返回true,失败会重试
}
}
// ============ Mapper层 ============
@Mapper
public interface AccountMapper {
/**
* Try阶段:冻结金额
* available_balance = balance - frozen_amount
*/
@Update("UPDATE account SET frozen_amount = frozen_amount + #{amount} " +
"WHERE user_id = #{userId} AND (balance - frozen_amount) >= #{amount}")
int freezeAmount(@Param("userId") String userId, @Param("amount") BigDecimal amount);
/**
* Confirm阶段:确认扣款(真正从余额扣除)
*/
@Update("UPDATE account SET " +
"balance = balance - #{amount}, " +
"frozen_amount = frozen_amount - #{amount} " +
"WHERE user_id = #{userId}")
int confirmDeduct(@Param("userId") String userId, @Param("amount") BigDecimal amount);
/**
* Cancel阶段:解冻金额(退还冻结)
*/
@Update("UPDATE account SET frozen_amount = frozen_amount - #{amount} " +
"WHERE user_id = #{userId}")
int unfreezeAmount(@Param("userId") String userId, @Param("amount") BigDecimal amount);
}
// ============ 调用方:全局事务编排 ============
@Service
public class OrderTccServiceImpl {
@Autowired
private GlobalTransactionalScanner seataScanner; // 引入Seata自动配置
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrderWithTcc(OrderDTO order) {
// 账户服务使用TCC
// 通过RPC框架(Dubbo/Feign)调用远程TCC服务
// Seata的TCC接口支持通过RPC传播XID
accountTccClient.tryReserve(order.getUserId(), order.getAmount());
// 库存服务也使用TCC
inventoryTccClient.tryReserve(order.getProductId(), order.getQuantity());
// 本地订单创建
orderMapper.insert(toOrder(order));
}
}
五、Saga模式:长事务的补偿链
Saga模式是1987年提出的分布式事务模式,其核心思想是:将长事务拆成一系列本地事务,每个本地事务有对应的补偿操作。当某个步骤失败时,按照倒序依次执行补偿操作(逆向补偿)。
/**
* Saga vs TCC vs AT 的对比:
*
* Saga模式特点:
* - 无锁设计:不获取全局锁,并发性能高
* - 长事务友好:适合业务流程很长(10+步骤)的场景
* - 无Undo Log:通过正向执行+逆向补偿来恢复
* - 适用场景:业务流程系统(订单→支付→物流→库存→积分→通知)
*
* Saga与TCC的核心区别:
* - TCC有Try阶段(预检),Saga没有(直接执行)
* - TCC通过冻结来预留,Saga通过补偿来恢复
* - Saga适合"可补偿"的业务,TCC适合"可预留"的业务
*/
/**
* Saga的两种补偿策略:
*
* 1. 编排式Saga(Choreography)
* - 每个服务订阅上一个服务的事件,决定是否执行
* - 去中心化,适合少量服务
* - 缺点:服务间耦合增加,事务状态分散
*
* 2. 集中式Saga(Orchestration)
* - 由Saga Orchestrator统一指挥各参与者的执行顺序
* - 中心化协调,事务状态清晰
* - Seata Saga采用这种模式
*/
// Seata Saga状态机配置示例(JSON/JSON5格式)
/**
* Seata Saga的状态机设计器通过JSON描述事务流程:
*
* 状态机 = 一系列状态(ServiceState/ChoiceState/SubStateMachine)
* 状态之间通过流转(transitions)连接
* 异常通过CompensationTrigger触发补偿
*/
{
"Name": "orderSagaStateMachine",
"Comment": "订单创建Saga长事务",
"StartState": "CreateOrder",
"States": {
"CreateOrder": {
"Type": "ServiceTask", // 调用服务
"ResourceName": "orderService:create", // 服务标识
"CompensationMethod": "orderService:cancel", // 补偿方法
"Next": "PayOrder",
"Status": {
"Success": "SU", // 成功则流转
"Fail": "FA", // 失败则补偿
"Compensate": "CO"
},
"Input": [
"$.#context.xid", // 引用事务上下文中的xid
"$.#context.userId",
"$.#context.orderInfo"
]
},
"PayOrder": {
"Type": "ServiceTask",
"ResourceName": "paymentService:pay",
"CompensationMethod": "paymentService:refund",
"Next": "DeductInventory",
"Catch": [ // 异常处理
{
"Next": "CompensateOrder",
"Exceptions": ["*"] // 捕获所有异常
}
]
},
"DeductInventory": {
"Type": "ServiceTask",
"ResourceName": "inventoryService:deduct",
"CompensationMethod": "inventoryService:restore",
"Next": "GrantCredits"
},
"GrantCredits": {
"Type": "ServiceTask",
"ResourceName": "creditService:grant",
"CompensationMethod": "creditService:revoke",
"End": true // 正常结束
}
}
}
六、Seata高可用部署与性能优化
生产环境中,Seata Server(TC)必须是集群部署,否则TC就是整个系统的单点。
6.1 TC集群部署架构
/**
* Seata TC高可用部署模式:
*
* 架构:TC集群(多节点)+ DB(全局事务会话存储)
*
* 注册中心选型:
* - Nacos(推荐):支持服务发现+配置中心,适合阿里技术栈
* - Eureka:纯服务发现,适合Spring Cloud体系
* - ZooKeeper:CP模型,数据一致性好
* - Consul:HashiCorp生态,运维友好
*
* 数据库选型:
* - MySQL(推荐):Seata原生支持,事务数据量适中
* - PostgreSQL:适合数据量大的场景,支持JSON类型
* - Redis(实验性):超高性能,但需要接受数据丢失风险
*
* 会话存储(Transaction Store):
* - Seata在TC内存中维护全局事务状态
* - 定期持久化到DB(默认5秒),用于故障恢复
* - 极端情况下最多丢失最近5秒的事务(可配置)
*/
/**
* Seata集群部署配置示例(registry.conf)
*/
// registry.conf - 使用Nacos作为注册/配置中心
registry {
type = "nacos" // 注册中心类型
nacos {
application = "seata-server"
serverAddr = "nacos-headless.namespace:8848" // Kubernetes内用Headless Service
group = "SEATA_GROUP"
namespace = "seata-namespace"
cluster = "default"
// Nacos集群模式下,serverAddr填写VIP或DNS
}
}
config {
type = "nacos"
nacos {
serverAddr = "nacos-headless.namespace:8848"
namespace = "seata-namespace"
group = "SEATA_GROUP"
dataId = "seataServer"
}
}
// ============ application.yml(TC服务端) ============
spring:
application:
name: seata-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://mysql-master:3306/seata?useUnicode=true&rewriteBatchedStatements=true
username: seata
password: ${MYSQL_PASSWORD}
hikari:
maximum-pool-size: 50 // TC数据库连接池不宜过大
minimum-idle: 10
server:
port: 8091
# TC核心性能配置
seata:
server:
# TC的Netty通信线程池(处理RM/TM的请求)
# 并发高时需要调大,默认CPU核数*2
max-concurrent-client-sessions: 16
# Raft集群通信端口(TC间心跳/选主)
raft-server-port: 9091
# 会话信息持久化间隔(越小恢复越快,但DB压力大)
store-session-port: 5000
#undo_log表分片(高并发写入时避免单表热点)
#在store.db横表模式下,表名支持sharding表达式
# 客户端连接数限制(防止RM过多导致TC过载)
server.max-connection-idle-timeout: 30
server.max-connection-idle-times: 3
6.2 客户端(TM/RM)性能调优
/**
* Seata客户端(业务服务)性能调优:
*/
// application.yml(业务服务)
seata:
# 事务分组:客户端通过事务分组发现TC
# 一个事务分组可以包含多个TC节点,实现客户端侧负载均衡
tx-service-group: my-tx-group
# 事务分组 → TC集群的映射关系
# 格式:tx-service-group:cluster-name
service:
vgroup-mapping:
my-tx-group: default # TC集群名(在Nacos中注册的名字)
# 全局事务超时时间(默认60秒)
# 业务执行超过这个时间,TC会强制回滚
# 设置太短会导致长查询被误杀,设置太长会导致锁持有时间过长
global-branch-scheduling-try-times: 3 # 分支调用重试次数
# RM端的undo_log表优化(AT模式必须)
client:
rm:
# 异步删除undo_log(事务提交后不立即删除,减少主事务延迟)
# 删除任务由后台线程异步执行
async-commit: true
# undo_log表保留时间(秒),用于异常排查
undolog-delete-records-time: 604800 # 7天
# 报告成功的全局事务是否上报TC(关闭可减少约50%的TC通信)
# 适用场景:读多写少、全局事务成功率高的场景
report-success-enable: false
# 并发连接数(同一TC节点上的RM连接数)
lock-retry-interval: 10ms # 申请全局锁失败后的重试间隔
lock-retry-times: 30 # 申请全局锁的最大重试次数
# TM端
tm:
# 批量处理全局事务注册(减少TC通信次数)
defer-client-handler-append: true
/**
* 高并发场景下的AT模式性能问题排查:
*
* 问题1:全局锁竞争激烈
* 表现:多个全局事务同时修改同一行数据,lock-retry频繁
* 解决:业务层面拆分热点数据(分桶、分片)
*
* 问题2:undo_log写入成为瓶颈
* 表现:数据库CPU高,undo_log表写入量大
* 解决:undo_log异步批量写入、清理频率调高
*
* 问题3:TC成为单点瓶颈
* 表现:全局事务响应时间长,TC进程CPU高
* 解决:TC水平扩容(集群),客户端vgroup-mapping配置多个节点
*/
七、实战:订单+库存+账户的分布式事务场景
完整的分布式事务场景:用户下单购买商品,
// ============ 场景描述 ============
/**
* 业务场景:
* 用户下单 -> 扣减账户余额(远程)-> 扣减库存(远程)-> 创建本地订单
*
* 技术选型:Seata AT模式
* - 账户服务:远程Feign调用,AT模式保护远程数据修改
* - 库存服务:远程Feign调用,AT模式保护远程数据修改
* - 订单服务:本地数据库,AT模式自动代理DataSource
*
* 全局事务边界:createOrder() 方法
* - TM向TC注册全局事务,获取XID
* - XID通过Feign Header自动传播到 account-service 和 inventory-service
* - 任一环节失败,全局回滚
*/
// ============ 订单服务 ============
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountClient accountClient;
@Autowired
private InventoryClient inventoryClient;
/**
* 创建订单(全局事务入口)
*
* 事务流程:
* 1. @GlobalTransactional 拦截,生成XID
* 2. account-client 调用账户服务:TM注册分支事务,AT代理扣款SQL
* 3. inventory-client 调用库存服务:TM注册分支事务,AT代理扣库存SQL
* 4. orderMapper.insert() 本地事务:Seata DataSourceProxy代理INSERT
* 5. 全部成功:TC异步清理全局锁
* 6. 任一失败:TC通知所有RM回滚(使用Undo Log)
*/
@GlobalTransactional(rollbackFor = Exception.class, name = "create-order-tx")
public Order createOrder(CreateOrderRequest request) {
// ① 扣减账户余额(跨服务)
// Feign拦截器注入 X-TTID 请求头到HTTP Header
// 账户服务Filter解析XID,注册分支事务到TC
log.info("开始扣减账户余额: userId={}, amount={}", request.getUserId(), request.getTotalAmount());
accountClient.deductBalance(request.getUserId(), request.getTotalAmount());
// ② 扣减商品库存(跨服务)
// 库存服务通过 Seata GlobalLock 机制保证行锁
log.info("开始扣减库存: productId={}, quantity={}", request.getProductId(), request.getQuantity());
inventoryClient.deductStock(request.getProductId(), request.getQuantity());
// ③ 创建本地订单记录(本地事务)
// Seata DataSourceProxy 自动拦截 INSERT SQL
// 生成前镜像(空)-> 执行INSERT -> 生成后镜像 -> 写入undo_log
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setTotalAmount(request.getTotalAmount());
order.setStatus("PAID");
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);
log.info("订单创建成功: orderId={}", order.getId());
return order;
}
}
// ============ 账户服务(远程,AT模式) ============
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
/**
* 扣减余额接口
* Seata的GlobalLock+AT模式保护
*
* 与本地AT的区别:需要显式获取全局锁(SELECT FOR UPDATE)
* 通过@GlobalTransactional或@GlobalLock注解声明
*/
@GlobalTransactional(rollbackFor = Exception.class)
@PostMapping("/deduct")
public Result<Void> deductBalance(@RequestParam String userId,
@RequestParam BigDecimal amount) {
accountService.deduct(userId, amount);
return Result.success();
}
}
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 扣减余额(AT模式)
* SQL:UPDATE account SET balance = balance - ? WHERE user_id = ? AND balance >= ?
*
* Seata DataSourceProxy 拦截此SQL:
* 1. 前镜像:SELECT balance FROM account WHERE user_id = ?(记录扣款前余额)
* 2. 执行:UPDATE account SET balance = balance - ?
* 3. 后镜像:SELECT balance FROM account WHERE user_id = ?(记录扣款后余额)
* 4. 写入undo_log:{before: 1000, after: 900}
*
* 全局提交:异步删除undo_log(不阻塞)
* 全局回滚:反向执行UPDATE(从900恢复到1000)
*/
public void deduct(String userId, BigDecimal amount) {
int affected = accountMapper.deductBalance(userId, amount);
if (affected == 0) {
throw new BizException("余额不足或账户不存在: " + userId);
}
}
}
@Mapper
public interface AccountMapper {
// AT模式自动拦截:生成前/后镜像 + 写入undo_log
@Update("UPDATE account SET balance = balance - #{amount} " +
"WHERE user_id = #{userId} AND balance >= #{amount}")
int deductBalance(@Param("userId") String userId, @Param("amount") BigDecimal amount);
}
// ============ 库存服务(远程,AT模式) ============
@Service
public class InventoryService {
@Autowired
private InventoryMapper inventoryMapper;
/**
* 扣减库存(AT模式)
* SQL:UPDATE inventory SET stock = stock - ? WHERE product_id = ? AND stock >= ?
*/
public void deductStock(String productId, Integer quantity) {
int affected = inventoryMapper.deductStock(productId, quantity);
if (affected == 0) {
throw new BizException("库存不足或商品不存在: " + productId);
}
}
}
@Mapper
public interface InventoryMapper {
@Update("UPDATE inventory SET stock = stock - #{quantity} " +
"WHERE product_id = #{productId} AND stock >= #{quantity}")
int deductStock(@Param("productId") String productId, @Param("quantity") Integer quantity);
}
八、架构选型决策树
最后总结一下如何在不同场景下做技术选型:
/**
* 分布式事务选型决策树:
*
* Q1: 业务是否可以接受最终一致?
* 是 → Q2
* 否 → 必须强一致,参考以下
*
* Q2: 涉及的系统类型是什么?
* 纯数据库操作(MySQL/PostgreSQL)→ AT模式
* 混合系统(DB+Redis+MQ+外部API)→ TCC模式
* 长流程业务(10+步骤)→ Saga模式
*
* Q3: 性能要求?
* 高并发(万级TPS+)→ TCC > Saga > AT
* 普通业务(千级TPS)→ AT模式(开发效率最高)
* 金融级 → TCC + 多IDC容灾
*
* Q4: 开发成本能接受多少?
* 低(快速迭代)→ AT模式(无业务侵入)
* 中等(核心链路)→ TCC模式(关键业务)
* 高(长流程)→ Saga模式(业务流程引擎)
*/
// 模式对比总结
// 特性 | AT模式 | TCC模式 | Saga模式
// -----------------|--------------|---------------|------------
// 业务侵入性 | 无 | 高 | 中等
// 性能 | 中等 | 高 | 高
// 全局锁 | 有 | 无 | 无
// 回滚方式 | Undo Log | 手动补偿 | 补偿服务
// 适用场景 | 数据库操作 | 任意系统 | 长流程
// 失败恢复 | 自动 | 手动幂等 | 状态机驱动
// 开发成本 | 低 | 高 | 中等
// 理论成熟度 | 成熟 | 成熟 | 成熟
// 生态 | Apache顶级项目| Apache顶级项目| Apache顶级项目