分布式事务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时的断点

image

 此时order库的undo_log表

image

 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的正向和补偿操作要自己保证幂等/空回滚/防悬挂

 
posted @ 2026-08-15 22:51  杨吃羊  阅读(6)  评论(0)    收藏  举报