分布式事务seata框架下at/tcc/sage模式代码
下载 seata: https://github.com/apache/incubator-seata/releases#release-v2.6.0
启动seata: window执行 seata-server.bat,linux执行 seata-server.sh
AT模式代码:
创建订单模块,账户模块,库存模块,每个模块有自己的数据库,订单模块调另外两个模块,每个模块要建立undo_log表用于回滚
package com.example.order.service; import com.example.order.entity.Order; import com.example.order.mapper.OrderMapper; import io.seata.spring.annotation.GlobalTransactional; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import java.util.HashMap; import java.util.Map; /** * 下单服务。 * <p> * 这里的 @GlobalTransactional 就是「TM(事务管理器)」的标志: * 它负责开启 / 结束全局事务,并向 TC 发起全局提交或全局回滚。 * 同时本服务也操作自己的订单库,所以它同时也是「RM(资源管理器)」。 */ @Service @RequiredArgsConstructor public class OrderService { private final OrderMapper orderMapper; private final RestTemplate restTemplate; @Value("${remote.storage-url}") private String storageUrl; @Value("${remote.account-url}") private String accountUrl; /** * 下单:创建订单 + 扣库存 + 扣余额,三步必须一起成功,否则一起回滚。 * 注意:这里不需要 @Transactional,@GlobalTransactional 已接管整个链路。 */ @GlobalTransactional(name = "create-order-tx", rollbackFor = Exception.class) public void createOrder(Order order) { // ① 本地分支事务:创建订单(本服务 = RM,操作 seata_order 库) orderMapper.insert(order); // ② 远程分支事务:扣库存(storage-service = RM,操作 seata_storage 库) Map<String, Object> storageParam = new HashMap<>(); storageParam.put("productId", order.getProductId()); storageParam.put("count", order.getCount()); restTemplate.postForObject(storageUrl + "/storage/deduct", storageParam, String.class); // ③ 远程分支事务:扣余额(account-service = RM,操作 seata_account 库) Map<String, Object> accountParam = new HashMap<>(); accountParam.put("userId", order.getUserId()); accountParam.put("money", order.getMoney()); restTemplate.postForObject(accountUrl + "/account/deduct", accountParam, String.class); // 三步全部成功,方法正常返回 → TM 通知 TC 全局提交 // 任何一步抛异常(如库存不足、余额不足)→ 异常向上传播 → TM 通知 TC 全局回滚 } }
-
TC (事务协调器):独立的服务端,是全局的决策中心,负责协调和管理所有分支事务。就是单独启动的seata服务
-
TM (事务管理器):嵌入在发起全局事务的应用中,负责开启、提交或回滚全局事务。就是加了@GlobalTransactional注解的服务
-
RM (资源管理器):嵌入在每个参与事务的应用中,负责执行本地事务,并向 TC 注册分支事务和上报状态。每个分支事务
重要概念XID:
在 Seata 中,XID 是一个全局唯一的事务标识符,当 TM(事务管理器)向 TC(事务协调器)发起开启全局事务请求时,TC 会生成这个 XID,并返回给发起方。后续无论是 TM 提交/回滚全局事务,还是 RM 注册分支事务,都需要携带这个 XID,TC 通过它来唯一识别和管理这个全局事务实例。
在微服务调用链中,XID 必须透传给下游服务。发起方会将 XID 塞入调用链的上下文(如 RPC 的隐式参数、HTTP 的 Header)中。下游服务接收到请求后,会解析出 XID 并绑定到本地线程变量(RootContext)。这样,原本孤立的本地服务就被“拉入”了同一个全局事务组中,确保所有跨服务的调用都属于同一个分布式事务。
RM 在执行本地事务时,会生成 SQL 执行前后的镜像数据,并插入到数据库的 UNDO_LOG 表中。UNDO_LOG 表里会专门有一个字段记录当前的 XID。当全局事务需要回滚时,TC 发送回滚请求并带上 XID,RM 根据这个 XID 在本地数据库中查询对应的 UNDO_LOG 记录,从而精准恢复数据,而不会错误地回滚其他事务的数据。
项目中resttemplate拦截xid并传给下游服务代码
package com.example.order.config; import io.seata.core.context.RootContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpRequest; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpResponse; import org.springframework.web.client.RestTemplate; import java.io.IOException; import java.util.Collections; /** * RestTemplate 配置:关键点是拦截器把全局事务 XID 放进 HTTP 头, * 这样下游服务(storage/account)才能知道自己属于哪个全局事务。 */ @Configuration public class RestTemplateConfig { @Bean public RestTemplate restTemplate() { RestTemplate restTemplate = new RestTemplate(); restTemplate.setInterceptors(Collections.singletonList(new SeataXidInterceptor())); return restTemplate; } /** 出站拦截器:把当前线程的 XID 放到请求头 TX_XID */ static class SeataXidInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String xid = RootContext.getXID(); if (xid != null) { request.getHeaders().add(RootContext.KEY_XID, xid); } return execution.execute(request, body); } } }
order服务调下游服务获取xid时的断点

此时order库的undo_log表

seata下的at模式每个分布式事务都先执行各自的提交,如果有RM需要回滚,TC再通知所有RM统一回滚,如果不需要回滚就通知RM删除undo_log数据
TCC模式
对订单/库存/账户表的变更:订单表添加status,库存/账户表添加freeze
每个库都添加tcc_fence_log表,用于幂等/防悬挂
CREATE TABLE `t_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`count` int NOT NULL COMMENT '购买数量',
`money` decimal(10,2) NOT NULL COMMENT '订单金额',
`status` int NOT NULL DEFAULT '0' COMMENT '订单状态 0=INIT(已预留) 1=CONFIRMED 2=CANCELLED',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表'
CREATE TABLE `t_storage` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_id` bigint NOT NULL COMMENT '商品ID',
`total` int NOT NULL COMMENT '总库存',
`used` int NOT NULL DEFAULT '0' COMMENT '已用库存',
`residue` int NOT NULL COMMENT '剩余库存',
`freeze` int NOT NULL DEFAULT '0' COMMENT '冻结库存(TCC Try 预留,Confirm 转 used,Cancel 退回 residue)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='库存表';
CREATE TABLE `t_account` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL COMMENT '用户ID',
`total` decimal(10,2) NOT NULL COMMENT '总额度',
`used` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '已用额度',
`residue` decimal(10,2) NOT NULL COMMENT '剩余额度',
`freeze` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '冻结余额(TCC Try 预留,Confirm 转 used,Cancel 退回 residue)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='账户表'
CREATE TABLE `tcc_fence_log` (
`xid` varchar(128) NOT NULL COMMENT '全局事务ID',
`branch_id` bigint NOT NULL COMMENT '分支事务ID',
`action_name` varchar(64) NOT NULL COMMENT '分支动作名(@TwoPhaseBusinessAction.name)',
`status` tinyint NOT NULL COMMENT '状态 1=try 2=confirm 3=cancel 4=悬挂',
`gmt_create` datetime(3) NOT NULL COMMENT '创建时间',
`gmt_modified` datetime(3) NOT NULL COMMENT '修改时间',
PRIMARY KEY (`xid`,`branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Seata TCC 防悬挂/幂等日志表'
订单模块下单,远程调用库存/账户模块扣减库存和余额
@GlobalTransactional(name = "create-order-tcc-tx", rollbackFor = Exception.class) public void createOrder(Order order) { // ① 本地 TCC 分支:预下单(status=0) orderTccAction.prepare(null, order); // ② 远程 TCC 分支:冻结库存 Map<String, Object> storageParam = new HashMap<>(); storageParam.put("productId", order.getProductId()); storageParam.put("count", order.getCount()); restTemplate.postForObject(storageUrl + "/storage/tcc/prepare", storageParam, String.class); // ③ 远程 TCC 分支:冻结余额 Map<String, Object> accountParam = new HashMap<>(); accountParam.put("userId", order.getUserId()); accountParam.put("money", order.getMoney()); restTemplate.postForObject(accountUrl + "/account/tcc/prepare", accountParam, String.class); }
@GlobalTransactional标识订单模块是TM和RM,name用于seata-service记录日志,可以省略不写
order模块代码:
/** * 订单 TCC 接口。 * <p> * {@link LocalTCC} 让 Seata 自动为该接口的实现类生成代理; * {@link TwoPhaseBusinessAction} 声明 Try 是 prepare,Confirm 是 commit,Cancel 是 rollback。 * name 全局唯一,是分支资源标识。 * useTCCFence=true 开启 tcc_fence_log 防悬挂/幂等。 */ @LocalTCC public interface OrderTccAction { @TwoPhaseBusinessAction(name = "orderTccAction", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true) boolean prepare(BusinessActionContext actionContext, Order order); boolean commit(BusinessActionContext actionContext); boolean rollback(BusinessActionContext actionContext); }
package com.example.tccorder.tcc; @Service @RequiredArgsConstructor public class OrderTccActionImpl implements OrderTccAction { private final OrderMapper orderMapper; /** Try:预下单,订单状态置 0(预留),不真正确认。 */ @Override @Transactional(rollbackFor = Exception.class) public boolean prepare(BusinessActionContext actionContext, Order order) { order.setStatus(0); orderMapper.insert(order); // 关键:把回填的订单 ID 塞进上下文,供 commit/rollback 更新状态用。 actionContext.addActionContext("orderId", String.valueOf(order.getId())); return true; } /** Confirm:全局提交时,把订单状态置 1。 */ @Override @Transactional(rollbackFor = Exception.class) public boolean commit(BusinessActionContext actionContext) { Long orderId = toLong(actionContext.getActionContext("orderId")); orderMapper.updateStatus(orderId, 1); return true; } /** Cancel:全局回滚时,把订单状态置 2(逻辑取消,保留记录)。 */ @Override @Transactional(rollbackFor = Exception.class) public boolean rollback(BusinessActionContext actionContext) { Long orderId = toLong(actionContext.getActionContext("orderId")); orderMapper.updateStatus(orderId, 2); return true; } private Long toLong(Object value) { return value == null ? null : Long.valueOf(String.valueOf(value)); } }
库存模块代码:
package com.example.tccstorage.tcc; @LocalTCC public interface StorageTccAction { @TwoPhaseBusinessAction(name = "storageTccAction", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true) boolean prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "productId") Long productId, @BusinessActionContextParameter(paramName = "count") Integer count); boolean commit(BusinessActionContext actionContext); boolean rollback(BusinessActionContext actionContext); }
@LocalTCC标识的接口,会向seata注册分支事务,成为RM,
@TwoPhaseBusinessAction的name表示分支事务名,不可重复,commitMethod /rollbackMethod 标识提交和回滚时执行的方法
useTCCFence 是 @TwoPhaseBusinessAction 的一个开关属性,默认 false。设为 true 就是开启 Seata 的「TCC 防悬挂/幂等」机制,这个机制靠 tcc_fence_log 表来工作
防悬挂/幂等/空回滚逻辑不可自定义,都是seata靠状态字段完成的,如果不符合自己的业务,就设成false,自己写逻辑保证幂等tcc的好处是可以支持无事务的库,因为回滚/提交是靠自己写的commit/rollback接口完成.缺点是不能对旧项目进行改造,需要在一开始就建好冻结字段.
saga:
每个子分支必须写一个正向操作和一个对应的补偿操作,比如连续的正向操作是z1,z2,z3,对应的补偿操作是b1,b2,b3,如果z3时出错,就执行b2,b1进行补偿.
正向操作和补偿操作执行流程写在状态机配置文件中,是一个json文件,类似工作流.
创建订单代码:
package com.example.saga.controller; import com.example.saga.dto.OrderRequest; import io.seata.saga.engine.StateMachineEngine; import io.seata.saga.statelang.domain.ExecutionStatus; import io.seata.saga.statelang.domain.StateMachineInstance; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.util.HashMap; import java.util.Map; import java.util.UUID; @RestController @RequiredArgsConstructor public class SagaController { private final StateMachineEngine stateMachineEngine; @PostMapping("/order/create") public String create(@RequestBody OrderRequest request) { // 业务主键:既用于幂等,也存进状态机实例表 String businessKey = UUID.randomUUID().toString(); Map<String, Object> params = new HashMap<>(); params.put("businessKey", businessKey); params.put("userId", request.getUserId()); params.put("productId", request.getProductId()); params.put("count", request.getCount()); params.put("money", request.getMoney()); // 同步执行状态机,返回最终实例 StateMachineInstance inst = stateMachineEngine.startWithBusinessKey( "orderSaga", null, businessKey, params);//orderSaga 对应状态机配置文件的name,表示开始的节点
if (ExecutionStatus.SU.equals(inst.getStatus()) && inst.getCompensationStatus() == null) { return "下单成功(saga),businessKey=" + businessKey; } else if (ExecutionStatus.SU.equals(inst.getCompensationStatus())) { return "下单失败,已成功补偿,businessKey=" + businessKey; } else { return "下单结果未知,businessKey=" + businessKey + ",status=" + inst.getStatus() + ",compensationStatus=" + inst.getCompensationStatus(); } } }
{ "Name": "orderSaga",//代码里配置的入口 "Comment": "下单 saga:创建订单 -> 扣库存 -> 扣余额,失败自动逆序补偿", "StartState": "CreateOrder", "Version": "0.0.1", "States": { "CreateOrder": { "Type": "ServiceTask", "ServiceName": "orderAction",//表示类名 "ServiceMethod": "createOrder",//方法名 "CompensateState": "CompensateCreateOrder",//对应的补偿 "Next": "ReduceStock",//如果执行成功下一步 "Input": [ "$.[businessKey]", "$.[userId]", "$.[userId]", "$.[productId]", "$.[count]", "$.[money]" ], "Output": { "createOrderResult": "$.#root" }, "Status": { "#root == true": "SU", "#root == false": "FA", "$Exception{java.lang.Throwable}": "UN" } }, "ReduceStock": { "Type": "ServiceTask", "ServiceName": "storageAction", "ServiceMethod": "reduceStock", "CompensateState": "CompensateReduceStock", "Next": "ReduceBalance", "Input": [ "$.[businessKey]", "$.[productId]", "$.[count]" ], "Output": { "reduceStockResult": "$.#root" }, "Status": { "#root == true": "SU", "#root == false": "FA", "$Exception{java.lang.Throwable}": "UN" }, "Catch": [ { "Exceptions": ["java.lang.Throwable"], "Next": "CompensationTrigger" } ] }, "ReduceBalance": { "Type": "ServiceTask", "ServiceName": "accountAction", "ServiceMethod": "reduceBalance", "CompensateState": "CompensateReduceBalance", "Next": "Succeed", "Input": [ "$.[businessKey]", "$.[userId]", "$.[money]" ], "Output": { "reduceBalanceResult": "$.#root" }, "Status": { "#root == true": "SU", "#root == false": "FA", "$Exception{java.lang.Throwable}": "UN" }, "Catch": [ { "Exceptions": ["java.lang.Throwable"], "Next": "CompensationTrigger" } ] }, "CompensateCreateOrder": { "Type": "ServiceTask", "ServiceName": "orderAction", "ServiceMethod": "cancelOrder", "Input": [ "$.[businessKey]" ] }, "CompensateReduceStock": { "Type": "ServiceTask", "ServiceName": "storageAction", "ServiceMethod": "restoreStock", "Input": [ "$.[businessKey]", "$.[productId]", "$.[count]" ] }, "CompensateReduceBalance": { "Type": "ServiceTask", "ServiceName": "accountAction", "ServiceMethod": "restoreBalance", "Input": [ "$.[businessKey]", "$.[userId]", "$.[money]" ] }, "CompensationTrigger": { "Type": "CompensationTrigger", "Next": "Fail" }, "Succeed": { "Type": "Succeed" }, "Fail": { "Type": "Fail", "ErrorCode": "ORDER_FAILED", "Message": "下单失败,已触发补偿" } } }
saga的正向和补偿操作要自己保证幂等/空回滚/防悬挂
浙公网安备 33010602011771号