4SEATA-TCC模式 · SpringCloud微服务实战 · 看云

导航

本节代码地址


1. 整体机制

一个分布式的全局事务,整体是两阶段提交的模型。全局事务是由若干分支事务组成的,分支事务要满足两阶段提交的模型要求,即需要每个分支事务都具备自己的:

  • 一阶段 prepare 行为
  • 二阶段 commit 或 rollback 行为

790248e075d73f588b828343b520574e_MD5.webp

根据两阶段行为模式的不同,我们将分支事务划分为Automatic (Branch) Transaction ModeTCC (Branch) Transaction Mode.

AT 模式基于支持本地 ACID 事务关系型数据库

  • 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
  • 二阶段 commit 行为:马上成功结束,自动异步批量清理回滚日志。
  • 二阶段 rollback 行为:通过回滚日志,自动生成补偿操作,完成数据回滚。

相应的,TCC 模式,不依赖于底层数据资源的事务支持:

  • 一阶段 prepare 行为:调用自定义的 prepare 逻辑。
  • 二阶段 commit 行为:调用自定义的 commit 逻辑。
  • 二阶段 rollback 行为:调用自定义的 rollback 逻辑。

所谓 TCC 模式,是指支持把自定义的分支事务纳入到全局事务的管理中。

2. TCC 实例演示

3. AT 实例演示

本模块中我们将新建两个模块,我们将模拟电商购物环节中的下单->发货的过程,两个服务通过Feign进行远程调用。两个模块的名称分别为fw-cloud-transaction-seata-tcc-orderfw-cloud-transaction-seata-tcc-send

3.1 maven 依赖

两个模块所使用的依赖也是相同的,这里可以看到,我们依赖了上面封装的fw-cloud-transaction-base-dao,不需要再自己写基本的mapper等,因为是基于Nacos服务发现和Feign 远程调用的,因此引入了响应的包。其中最重要的就是引入spring-cloud-starter-alibaba-seata包,否者不能实现和Seata Server的通信及事务控制。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- base-dao -->
    <dependency>
        <groupId>com.yisu.cloud</groupId>
        <artifactId>fw-cloud-transaction-base-dao</artifactId>
        <version>${version}</version>
    </dependency>
    <!--seata-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>

    <!--服务发现-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <!--feign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>${druid.version}</version>
    </dependency>
</dependencies>

并在MySql中新建fw_transaction_seata库,并执行以下脚本,供fw-cloud-transaction-seata-tcc-orderfw-cloud-transaction-seata-tcc-send使用,其中undo_log的作用前面有介绍。

CREATE TABLE `fw_trade_log` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `status` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '订单状态 1.待支付 2.待发货 3.待收货 4.订单完成 5.订单关闭',
  `status_dsc` varchar(100) CHARACTER SET utf8 DEFAULT '' COMMENT '状态描述',
  `product_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '商品id',
  `product_name` varchar(100) NOT NULL DEFAULT '' COMMENT '商品名称',
  `user_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户id',
  `order_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '订单id',
  `order_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '订单总额',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
CREATE TABLE `undo_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',
  `branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
  `xid` varchar(100) NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',
  `rollback_info` longblob NOT NULL COMMENT 'rollback info',
  `log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
  `log_created` datetime NOT NULL COMMENT 'create datetime',
  `log_modified` datetime NOT NULL COMMENT 'modify datetime',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

3.2 fw-cloud-transaction-seata-tcc-order模块

3.2.1 订单接口创建

再订单模块中创建一个订单接口,并在接口中设置支付成功的方法


public interface OrderService{

    
    void saveAndPayOrder(String productName);

}

3.2.2 订单接口实现

订单在支付成功以后会远程调用发货服务进行发货,需要在Service 上加上@GlobalTransactional,开启AT模式的分布式事务控制,并且设置订单的状态为待支付


@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private FwTradeLogService fwTradeLogService;

    @Autowired
    private RemoteSendServiceFeign remoteSendServiceFeign;

    
    @GlobalTransactional
    @Override
    public void saveAndPayOrder(String productName) {

        FwTradeLog fwTradeLog =new FwTradeLog(StatusEnum.TWO);
        fwTradeLog.setProductId(System.currentTimeMillis());
        fwTradeLog.setProductName(productName);
        fwTradeLogService.save(fwTradeLog);
        log.info("[订单状态{}]=>{},当前商品id=>{},商品名称=>{}",fwTradeLog.getOrderId(), StatusEnum.TWO.getDesc(),fwTradeLog.getProductId(),fwTradeLog.getProductName());

        remoteSendServiceFeign.sendOrder(fwTradeLog);

    }

}

3.2.3 Feign 接口配置

这里我们配置远程调用的服务名称是fw-transaction-seata-tcc-send,并且设置了fallback,并且指定了远程调用的方法是sendOrder。

@FeignClient(value = "fw-transaction-seata-tcc-send", fallbackFactory = RemoteSendServiceFallback.class)
public interface RemoteSendServiceFeign {

    @PostMapping("send")
    void sendOrder(@RequestBody  FwTradeLog tradeLog);
}

3.2.4 Fallback 接口配置

在fallback输出异常日志

@Component
@Slf4j
public class RemoteSendServiceFallback implements FallbackFactory<RemoteSendServiceFeign> {

    @Override
    public RemoteSendServiceFeign create(Throwable throwable) {

        return tradeLog -> log.error("远程调用失败",throwable);
    }
}

3.2.5 Controller 控制层

配置订单服务对外的接口,方便演示


@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    
    @GetMapping("saveOrder")
    public FwResult saveOrder(String productName){
         orderService.saveAndPayOrder(productName);
         return FwResult.ok();
    }


}

3.2.6 启动类配置

这里需要加上@EnableFeignClients开启Feign的远程调用,加上@EnableDiscoveryClient开启服务的注册和发现


@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class FwTransactionSeataTccOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(FwTransactionSeataTccOrderApplication .class, args);
    }


}

3.2.7 应用配置

配置中需要配置数据库,以及Nacos的连接信息和开启Feign的远程调用,其中dbIp 作为变量传入,其中tx-service-group: fwcloud_tx_group需要和file.conf里面配置的一样。

server:
  port: 9002
spring:
  application:
    name: fw-transaction-seata-tcc-order
  #数据库配置  start
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      url: jdbc:mysql://${dbIp}:3306/fw_transaction_seata?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: fwcloud_tx_group
feign:
  hystrix:
    enabled: true
hystrix:
  shareSecurityContext: true

另外需要将Seata 里面的file.conf和registry.conf 复制到如下图所示的地方
96b0990e0b9c06b4a4a5c9fb5c828a6d_MD5.png

3.3 fw-cloud-transaction-seata-tcc-send模块

3.3.1 发货接口创建

配置发货接口,并且创建发货的方法,需要在接口上面加上@LocalTCC的注解,并且在原有的方法上面设置@TwoPhaseBusinessAction(name = "sendOrderTccAction", commitMethod = "commit", rollbackMethod = "cancel"),设置这个业务方法的两阶段提交。


@LocalTCC
public interface SendService {

    
    @TwoPhaseBusinessAction(name = "sendOrderTccAction", commitMethod = "commit", rollbackMethod = "cancel")
    void sendOrder(FwTradeLog fwTradeLog);

    boolean commit(BusinessActionContext businessActionContext);

    boolean cancel(BusinessActionContext businessActionContext);

}

3.3.2 发货接口实现

这里需要实现接口中定义的业务方法和commit、cancel方法,并且设置订单的状态为待收货,唯一的事务id保存在ConcurrentHashMap,保证线程安全


@Service
@Slf4j
public class SendServiceImpl implements SendService {

    private static final String LOGIC_PRIMARY_ID = "TRADE_LOG_ID";
    private ConcurrentHashMap<String, Long> hashMap = new ConcurrentHashMap<>();

    @Autowired
    private FwTradeLogService fwTradeLogService;


    @Override
    public void sendOrder(FwTradeLog fwTradeLog) {
        fwTradeLog.setStatus(StatusEnum.THREE.getValue());
        fwTradeLog.setStatusDsc(StatusEnum.THREE.getDesc());
        fwTradeLogService.save(fwTradeLog);
        log.info("[订单状态{}]=>{},当前商品id=>{},商品名称=>{}",fwTradeLog.getOrderId(), StatusEnum.THREE.getDesc(),fwTradeLog.getProductId(),fwTradeLog.getProductName());
        hashMap.put(LOGIC_PRIMARY_ID, fwTradeLog.getId());
    }

    @Override
    public boolean commit(BusinessActionContext businessActionContext) {
        log.info("当前LOGIC_PRIMARY_ID:{},事务执行成功",LOGIC_PRIMARY_ID);
        hashMap.remove(LOGIC_PRIMARY_ID);
        return true;
    }

    @Override
    public boolean cancel(BusinessActionContext businessActionContext) {
        Long tradeLogId = hashMap.get(LOGIC_PRIMARY_ID);
        log.info("事务执行失败,回滚LOGIC_PRIMARY_ID:{}的数据", tradeLogId);
        fwTradeLogService.removeById(tradeLogId);
        hashMap.remove(LOGIC_PRIMARY_ID);
        return true;
    }
}

3.3.3 发货控制层实现

提供给订单模块调用的接口,注意方法名和请求方式要保持一致


@RestController
public class SendController {

    @Autowired
    private SendService sendService;

    
    @PostMapping("send")
    public void sendOrder(@RequestBody FwTradeLog tradeLog) {
         sendService.sendOrder(tradeLog);
    }
}

3.3.4 启动类

需要加上@EnableDiscoveryClient注解开启服务的注册与发现


@SpringBootApplication
@EnableDiscoveryClient
public class FwTransactionSeataAtSendApplication {
    public static void main(String[] args) {
        SpringApplication.run(FwTransactionSeataAtSendApplication .class, args);
    }


}

3.3.5 应用配置

配置中需要配置数据库,以及设置了Nacos的连接信息和开启Feign的远程调用,其中tx-service-group: fwcloud_tx_group需要和file.conf里面配置的一样。

server:
  port: 9003
spring:
  application:
    name: fw-transaction-seata-at-send
  #数据库配置  start
  datasource:
    druid:
      url: jdbc:mysql://${dbIp}:3306/fw_transaction_seata?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    alibaba:
      seata:
        tx-service-group: fwcloud_tx_group

另外需要将Seata 里面的file.conf和registry.conf 复制到如下图所示的地方
96b0990e0b9c06b4a4a5c9fb5c828a6d_MD5.png

5.4 启动服务

分别启动Nacos、Seata Server、fw-transaction-seata-at-send、fw-transaction-seata-at-order
aba6d854a6332ab8ad032f67fb93f7a7_MD5.png

Nacos可以看到两个服务和Seata Server 注册上来了
65a2ae69fb0094ce2a449b4d9b1f1686_MD5.png

可以看到订单服务的日志如下

2020-04-10 23:37:31.558  INFO 17704 --- [nio-9002-exec-2] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [10.88.12.39:8091:2040260847]
2020-04-10 23:37:32.075  INFO 17704 --- [nio-9002-exec-2] c.y.t.s.t.o.s.impl.OrderServiceImpl      : [订单状态19718]=>待发货,当前商品id=>1586533051559,商品名称=>Mac pro 2020款
2020-04-10 23:37:32.602  INFO 17704 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=10.88.12.39:8091:2040260847,branchId=2040260849,branchType=AT,resourceId=jdbc:mysql://192.168.0.102:3306/fw_transaction_seata,applicationData=null
2020-04-10 23:37:32.603  INFO 17704 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 10.88.12.39:8091:2040260847 2040260849 jdbc:mysql://192.168.0.102:3306/fw_transaction_seata null
2020-04-10 23:37:32.603  INFO 17704 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed
2020-04-10 23:37:33.019  INFO 17704 --- [nio-9002-exec-2] i.seata.tm.api.DefaultGlobalTransaction  : [10.88.12.39:8091:2040260847] commit status: Committed


发货服务的日志如下

2020-04-10 23:37:32.335  INFO 22124 --- [nio-9003-exec-7] c.y.t.s.t.s.s.impl.SendServiceImpl       : [订单状态19718]=>待收货,当前商品id=>1586533051559,商品名称=>Mac pro 2020款
2020-04-10 23:37:32.763  INFO 22124 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=10.88.12.39:8091:2040260847,branchId=2040260853,branchType=TCC,resourceId=sendOrderTccAction,applicationData={"actionContext":{"sys::rollback":"cancel","sys::commit":"commit","action-start-time":1586533052081,"host-name":"10.88.12.39","sys::prepare":"sendOrder","actionName":"sendOrderTccAction"}}
2020-04-10 23:37:32.763  INFO 22124 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch committing: 10.88.12.39:8091:2040260847 2040260853 sendOrderTccAction {"actionContext":{"sys::rollback":"cancel","sys::commit":"commit","action-start-time":1586533052081,"host-name":"10.88.12.39","sys::prepare":"sendOrder","actionName":"sendOrderTccAction"}}
2020-04-10 23:37:32.763  INFO 22124 --- [tch_RMROLE_1_16] c.y.t.s.t.s.s.impl.SendServiceImpl       : 当前LOGIC_PRIMARY_ID:TRADE_LOG_ID,事务执行成功
2020-04-10 23:37:32.763  INFO 22124 --- [tch_RMROLE_1_16] io.seata.rm.AbstractResourceManager      : TCC resource commit result :true, xid:10.88.12.39:8091:2040260847, branchId:2040260853, resourceId:sendOrderTccAction
2020-04-10 23:37:32.763  INFO 22124 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch commit result: PhaseTwo_Committed


数据库记录两条记录
e80eccc7d8ac4ce3d8b27b524c1d6b14_MD5.png

现在让订单服务的方法抛出异常
直接在方法中加一个抛异常的语句int i=1/0;,如下所示

@GlobalTransactional
@Override
public void saveAndPayOrder(String productName) {

    FwTradeLog fwTradeLog =new FwTradeLog(StatusEnum.TWO);
    fwTradeLog.setProductId(System.currentTimeMillis());
    fwTradeLog.setProductName(productName);
    fwTradeLogService.save(fwTradeLog);
    log.info("[订单状态{}]=>{},当前商品id=>{},商品名称=>{}",fwTradeLog.getOrderId(), StatusEnum.TWO.getDesc(),fwTradeLog.getProductId(),fwTradeLog.getProductName());

    remoteSendServiceFeign.sendOrder(fwTradeLog);

    int i=1/0;
}

重启订单服务,删除之前fw_trade_log表的数据
Postman输入localhost:9002/saveOrder
f15a6f74d633c63562e5c42743b75573_MD5.png

可以看到订单服务的日志如下因为异常TCC 回滚了

2020-04-10 23:34:41.550  INFO 17456 --- [nio-9002-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : Begin new global transaction [10.88.12.39:8091:2040260820]
2020-04-10 23:34:42.050  INFO 17456 --- [nio-9002-exec-3] c.y.t.s.t.o.s.impl.OrderServiceImpl      : [订单状态169291]=>待发货,当前商品id=>1586532881551,商品名称=>Mac pro 2020款
2020-04-10 23:34:42.933  INFO 17456 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=10.88.12.39:8091:2040260820,branchId=2040260822,branchType=AT,resourceId=jdbc:mysql://192.168.0.102:3306/fw_transaction_seata,applicationData=null
2020-04-10 23:34:42.933  INFO 17456 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 10.88.12.39:8091:2040260820 2040260822 jdbc:mysql://192.168.0.102:3306/fw_transaction_seata
2020-04-10 23:34:43.013  INFO 17456 --- [tch_RMROLE_1_16] i.s.r.d.undo.AbstractUndoLogManager      : xid 10.88.12.39:8091:2040260820 branch 2040260822, undo_log deleted with GlobalFinished
2020-04-10 23:34:43.024  INFO 17456 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked
2020-04-10 23:34:43.336  INFO 17456 --- [nio-9002-exec-3] i.seata.tm.api.DefaultGlobalTransaction  : [10.88.12.39:8091:2040260820] rollback status: Rollbacked
2020-04-10 23:34:43.337 ERROR 17456 --- [nio-9002-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause


可以看到发货日志也TCC回滚了

2020-04-10 23:34:42.438  INFO 22124 --- [nio-9003-exec-5] c.y.t.s.t.s.s.impl.SendServiceImpl       : [订单状态169291]=>待收货,当前商品id=>1586532881551,商品名称=>Mac pro 2020款
2020-04-10 23:34:42.673  INFO 22124 --- [tch_RMROLE_1_16] i.s.core.rpc.netty.RmMessageListener     : onMessage:xid=10.88.12.39:8091:2040260820,branchId=2040260826,branchType=TCC,resourceId=sendOrderTccAction,applicationData={"actionContext":{"sys::rollback":"cancel","sys::commit":"commit","action-start-time":1586532882068,"host-name":"10.88.12.39","sys::prepare":"sendOrder","actionName":"sendOrderTccAction"}}
2020-04-10 23:34:42.675  INFO 22124 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacking: 10.88.12.39:8091:2040260820 2040260826 sendOrderTccAction
2020-04-10 23:34:42.676  INFO 22124 --- [tch_RMROLE_1_16] c.y.t.s.t.s.s.impl.SendServiceImpl       : 事务执行失败,回滚LOGIC_PRIMARY_ID:60的数据
2020-04-10 23:34:42.702  INFO 22124 --- [tch_RMROLE_1_16] io.seata.rm.AbstractResourceManager      : TCC resource rollback result :true, xid:10.88.12.39:8091:2040260820, branchId:2040260826, resourceId:sendOrderTccAction
2020-04-10 23:34:42.704  INFO 22124 --- [tch_RMROLE_1_16] io.seata.rm.AbstractRMHandler            : Branch Rollbacked result: PhaseTwo_Rollbacked


因此数据库里并没有数据
317c6b67db6b4e8a1d1dc325d084941a_MD5.png