【Seata】SpringCloud集成Seatav1.6之TCC模式

目录

开始语

简介

创建数据库表

yml配置文件(包含nacos配置)

order-service-config.yaml

stock-service-config.yaml

代码编写

发起请求服务orders

被orders调用的服务stock

结果分析

本项目代码地址请到此处下载源码


开始语

一位普通的程序员,慢慢在努力变强!

没有安装seata的同学点击此处前往👈

没有安装nacos的同学点击此处前往👈

【Seata】SpringCloud集成Seatav1.6之XA模式👈

【Seata】SpringBoot集成Seata1.6-AT模式👈

简介

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

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

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

AT 模式(参考链接 TBD)基于 支持本地 ACID 事务关系型数据库

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

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

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

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

简单来讲:就是将seata自动控制事务的全局提交和全局回滚,改为手动提交和回滚!

创建数据库表

-- 数据库
CREATE
DATABASE seata_order DEFAULT CHARACTER SET utf8mb4;

-- seata_order.order_tbl definition
CREATE TABLE `order_tbl`
(
    `id`             int(11) NOT NULL AUTO_INCREMENT,
    `user_id`        varchar(255) DEFAULT NULL,
    `commodity_code` varchar(255) DEFAULT NULL,
    `count`          int(11) DEFAULT '0',
    `money`          int(11) DEFAULT '0',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=71 DEFAULT CHARSET=utf8;


-- seata_order.tcc_fence_log TCC事务日志表

CREATE TABLE `tcc_fence_log` (
`xid` varchar(128) NOT NULL COMMENT 'global id',
`branch_id` bigint(20) NOT NULL COMMENT 'branch id',
`action_name` varchar(64) NOT NULL COMMENT 'action name',
`status` tinyint(4) NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` datetime(3) NOT NULL COMMENT 'create time',
`gmt_modified` datetime(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`,`branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 数据库
CREATE
DATABASE seata_stock DEFAULT CHARACTER SET utf8mb4;

-- seata_stock.stock_tbl definition
CREATE TABLE `stock_tbl`
(
    `id`             int(11) NOT NULL AUTO_INCREMENT,
    `commodity_code` varchar(255) DEFAULT NULL,
    `count`          int(11) DEFAULT '0',
    PRIMARY KEY (`id`),
    UNIQUE KEY `commodity_code` (`commodity_code`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- seata_order.tcc_fence_log TCC事务日志表

CREATE TABLE `tcc_fence_log` (
`xid` varchar(128) NOT NULL COMMENT 'global id',
`branch_id` bigint(20) NOT NULL COMMENT 'branch id',
`action_name` varchar(64) NOT NULL COMMENT 'action name',
`status` tinyint(4) NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
`gmt_create` datetime(3) NOT NULL COMMENT 'create time',
`gmt_modified` datetime(3) NOT NULL COMMENT 'update time',
PRIMARY KEY (`xid`,`branch_id`),
KEY `idx_gmt_modified` (`gmt_modified`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

yml配置文件(包含nacos配置)

order-service-config.yaml

-- order-service-config.yaml

spring:
  # staet----------------------------mysql服务配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.233.143:3306/seata_order?allowMultiQueries=true
    username: root
    password: getianyu_ROOT_123
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 10
      min-idle: 10
      maxActive: 200
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      connectionErrorRetryAttempts: 3
      breakAfterAcquireFailure: true
      timeBetweenConnectErrorMillis: 300000
      asyncInit: true
      remove-abandoned: false
      remove-abandoned-timeout: 1800
      transaction-query-timeout: 6000
      filters: stat,wall,log4j2
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      stat-view-servlet:
        url-pattern: "/druid/*"
        allow:
        deny:
        reset-enable: false
        login-username: admin
        login-password: admin
# end----------------------------mysql服务配置

# staet----------------------------mybatis-plus服务配置
mybatis-plus:
  global-config:
    db-config:
      # 逻辑已删除值(默认为 1)
      logic-delete-value: 1
      # 逻辑未删除值(默认为 0)
      logic-not-delete-value: 0
  mapper-locations: classpath*:**/repository/xml/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 驼峰,_映射 app_name = appName
    map-underscore-to-camel-case: true
# end----------------------------mybatis-plus服务配置

logging:
  level:
    io.seata: debug

# staet----------------------------seata服务配置
seata:
  config:
    type: nacos
    nacos:
      server-addr: 192.168.233.180:8848
      namespace: seata-server
      group: SEATA_GROUP
      username: nacos
      password: Getring0817#.
  registry:
    type: nacos
    nacos:
      server-addr: 192.168.233.180:8848
      namespace: seata-server
      application: seata-server
      group: SEATA_GROUP
      username: nacos
      password: Getring0817#.
  service:
    vgroup-mapping:
      default_tx_group: default
    disable-global-transaction: false
    grouplist:
      default: 192.168.233.180:8091
  tx-service-group: default_tx_group
# end----------------------------seata服务配置

server:
  port: 9091

spring:
  application:
    name: order-service
  profiles:
    active: @project.active@
  cloud:
    nacos:
      config:
        server-addr: @nacos.addr@
        namespace: ${spring.profiles.active}
        username: nacos
        password: Getring0817#.
        refresh-enabled: true
        enabled: true
        file-extension: yaml
        shared-configs:
          - data-id: order-service-config.yaml
            refresh: true
      discovery:
        server-addr: @nacos.addr@
        namespace: ${spring.profiles.active}
        watch:
          enabled: true
    alibaba:
      seata:
        tx-service-group: default_tx_group

stock-service-config.yaml

spring:
  # staet----------------------------mysql服务配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.233.143:3306/seata_stock?allowMultiQueries=true
    username: root
    password: getianyu_ROOT_123
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 10
      min-idle: 10
      maxActive: 200
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20
      connectionErrorRetryAttempts: 3
      breakAfterAcquireFailure: true
      timeBetweenConnectErrorMillis: 300000
      asyncInit: true
      remove-abandoned: false
      remove-abandoned-timeout: 1800
      transaction-query-timeout: 6000
      filters: stat,wall,log4j2
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
      web-stat-filter:
        enabled: true
        url-pattern: "/*"
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*"
      stat-view-servlet:
        url-pattern: "/druid/*"
        allow:
        deny:
        reset-enable: false
        login-username: admin
        login-password: admin
# end----------------------------mysql服务配置

# staet----------------------------mybatis-plus服务配置
mybatis-plus:
  global-config:
    db-config:
      # 逻辑已删除值(默认为 1)
      logic-delete-value: 1
      # 逻辑未删除值(默认为 0)
      logic-not-delete-value: 0
  mapper-locations: classpath*:**/repository/xml/*.xml
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 驼峰,_映射 app_name = appName
    map-underscore-to-camel-case: true
# end----------------------------mybatis-plus服务配置

logging:
  level:
    io.seata: debug

# staet----------------------------seata服务配置
seata:
  config:
    type: nacos
    nacos:
      server-addr: 192.168.233.180:8848
      namespace: seata-server
      group: SEATA_GROUP
      username: nacos
      password: Getring0817#.
  registry:
    type: nacos
    nacos:
      server-addr: 192.168.233.180:8848
      namespace: seata-server
      application: seata-server
      group: SEATA_GROUP
      username: nacos
      password: Getring0817#.
  service:
    vgroup-mapping:
      default_tx_group: default
    disable-global-transaction: false
    grouplist:
      default: 192.168.233.180:8091
  tx-service-group: default_tx_group
# end----------------------------seata服务配置

server:
  port: 9092

spring:
  application:
    name: stock-service
  profiles:
    active: @project.active@
  cloud:
    nacos:
      config:
        server-addr: @nacos.addr@
        namespace: ${spring.profiles.active}
        username: nacos
        password: Getring0817#.
        refresh-enabled: true
        enabled: true
        file-extension: yaml
        shared-configs:
          - data-id: stock-service-config.yaml
            refresh: true
      discovery:
        server-addr: @nacos.addr@
        namespace: ${spring.profiles.active}
        watch:
          enabled: true
    alibaba:
      seata:
        tx-service-group: default_tx_group

代码编写

发起请求服务orders

/**
     * 下单:创建订单、减库存,涉及到两个服务
     *
     * @param userId
     * @param commodityCode
     * @param count
     * @param tag commit接口如果入参是[成功],不抛出异常,[失败],抛出异常
     */
    @Override
    @GlobalTransactional
    public void placeOrder(String userId, String commodityCode, Integer count,String tag) {
        log.info("OrderService XID = {}", RootContext.getXID());
        BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
        Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
                orderMoney);
        orderDAO.insert(order);

        // stock服务未报错,order服务报错
        try {
            stockFeignClient.deduct(commodityCode, count);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
        // 输入commit接口
        if("product-1".equals(commodityCode) && tag.equals("失败")){
            throw new RuntimeException("订单业务处理异常...");
        }

    }

被orders调用的服务stock

service接口层

package com.work.stock.service;

import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;

/**
 * @author 猿仁
 * @date 2023-03-11 15:00
 */
@LocalTCC
public interface StockService {


    /**
     * 减库存
     *
     * @param commodityCode
     * @param count
     */
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "busCommit", rollbackMethod = "busRollback", useTCCFence = true)
    void deduct(@BusinessActionContextParameter(paramName = "commodityCode") String commodityCode,
                @BusinessActionContextParameter(paramName = "count") int count);

    /**
     * 提交事务
     *
     * @param actionContext
     * @return
     */
    boolean busCommit(BusinessActionContext actionContext);

    /**
     * 回滚事务
     *
     * @param actionContext
     * @return
     */
    boolean busRollback(BusinessActionContext actionContext);
}

serviceimpl实现层

package com.work.stock.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.work.stock.entity.Stock;
import com.work.stock.repository.StockDAO;
import com.work.stock.service.StockService;
import io.seata.core.context.RootContext;
import io.seata.rm.tcc.api.BusinessActionContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

/**
 * demo模板来源于官网,此处进行了改造
 *
 * @author 猿仁
 * @date 2023/3/11 20:18
 */
@Slf4j
@Service
public class StockServiceImpl extends ServiceImpl<StockDAO, Stock> implements StockService {

    @Resource
    private StockDAO stockDAO;

    /**
     * 减库存
     *
     * @param commodityCode
     * @param count
     */
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED)
    public void deduct(String commodityCode, int count) {
        log.info("StockService XID = {}", RootContext.getXID());

        QueryWrapper<Stock> wrapper = new QueryWrapper<>();
        wrapper.setEntity(new Stock().setCommodityCode(commodityCode));
        Stock stock = stockDAO.selectOne(wrapper);
        stock.setCount(stock.getCount() - count);

        stockDAO.updateById(stock);
        if (commodityCode.equals("product-2")) {
            throw new RuntimeException("异常:模拟业务异常:stock branch exception");
        }
    }


    @Override
    public boolean busCommit(BusinessActionContext actionContext) {
        log.info("busCommit xid = " + actionContext.getXid() + "提交成功");
        return true;
    }

    @Override
    public boolean busRollback(BusinessActionContext actionContext) {
        // 编写对应的业务数据进行回滚
        String commodityCode = actionContext.getActionContext("commodityCode", String.class);
        int count = actionContext.getActionContext("count", Integer.class);
        LambdaQueryChainWrapper<Stock> eq = lambdaQuery().eq(Stock::getCommodityCode, commodityCode);
        Long count1 = eq.one().getCount();
        // 扣了多少数,需要重新添加回去
        lambdaUpdate().set(Stock::getCount, count + count1)
                .eq(Stock::getCommodityCode, commodityCode)
                .update();
        log.info("busRollback xid = " + actionContext.getXid() + "回滚成功");
        return true;
    }
}

结果分析

1. 成功、失败...等一系列问题,都会在tcc_fence_log表中做记录(当前案例为例,那么数据将会在TCC端进行存储,也就是说会存储在seata_stock.tcc_fence_log表中)

2. 最终结果全局事务为成功时,没有出现任何异常,那么就会调用StockServiceImpl.busCommit这个方法

3. 最终结果全局事务为失败时,不管那一方出现了问题,那么就会调用StockServiceImpl.busRollback这个方法回滚当前事务修改的数据(也就是我们所说的自定义逻辑

4. 最终调用方orders出现异常回滚,stock方出现异常也会进行回滚,从而达到一致性性

 

本项目代码地址

springboot-seata: springboot、cloud alibaba整合Seata分布式事务

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
Spring Boot + Spring Cloud Alibaba Seata 中配置 TCC 模式,需要进行以下步骤: 1. 引入 Seata TCC 的依赖: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-seata</artifactId> <version>2.0.3.RELEASE</version> </dependency> ``` 2. 配置 Seata TCC 的相关参数: ```yaml spring: application: name: demo-service # 应用名称 seata: service: group: my_test_tx_group # 事务分组名称 vgroup-mapping.my_test_tx_group: default # 分组所在的虚拟组 enable-degrade: false # 是否开启降级模式,默认为 false use-jdk-proxy: false # 是否使用 JDK 代理,默认为 false config: type: nacos # 配置中心类型,可以是 file、nacos、apollo、zk nacos: namespace: seata # 命名空间 server-addr: localhost:8848 # Nacos 服务地址 group: SEATA_GROUP # 配置组 username: nacos # 用户名 password: nacos # 密码 file: name: file.conf # 配置文件名 registry: type: nacos # 注册中心类型,可以是 file、nacos、eureka、consul、zk nacos: server-addr: localhost:8848 # Nacos 服务地址 namespace: seata # 命名空间 group: SEATA_GROUP # 注册组 username: nacos # 用户名 password: nacos # 密码 tx-service-group: my_test_tx_group # 事务分组名称 ``` 3. 在需要使用 TCC 模式的方法上使用 @Tcc 注解: ```java @Service public class DemoServiceImpl implements DemoService { @Autowired private AccountService accountService; @Autowired private StorageService storageService; /** * TCC 模式下的分布式事务实现 */ @Override @GlobalTransactional(timeoutMills = 300000, name = "demo-service-tx") public void tccTransaction(String userId, String commodityCode, Integer count) { // 第一步:减少库存 storageService.decrease(commodityCode, count); // 第二步:扣除账户余额 accountService.decrease(userId, count); // 第三步:确认(提交) // do nothing // 第四步:取消(回滚) // do nothing } } ``` 4. 在 Seata 控制台中配置相应的 TCC 事务分组。 以上就是在 Spring Boot + Spring Cloud Alibaba Seata 中配置 TCC 模式的步骤。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猿仁

多一分支持,多一分动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值