springcloud Alibaba 笔记16 Seata 分布式事务(二)

接上文: https://mp.csdn.net/mp_blog/creation/success/124003885
SEATA:
Simple Extensible Autonomous Transaction Architecture,简单可扩展自治事务框架

4. 订单/库存/账户业务微服务准备

4.1 业务需求:

下订单->减库存->扣余额->改(订单)状态

以下数据库连接可以考虑用 MybatisPlus 接入

4.2 新建订单Order-Module

模块名称: seata-order-service2001

4.2.1 pom
     <dependencies>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--        nacos config-->
<!--        <dependency>-->
<!--            <groupId>com.alibaba.cloud</groupId>-->
<!--            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>-->
<!--        </dependency>-->

<!--        seata 依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2.2.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>1.4.0</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-all</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>1.4.0</version>
        </dependency>

<!--        上面 有可能会报错:
 Caused by: io.seata.common.exception.ShouldNeverHappenException: Can't find any object of class org.springframework.context.ApplicationContext
 ,换成下面的依赖即可,但是需要看版本对应-->
        <!--  seata 依赖  -->
<!--        <dependency>-->
<!--            <groupId>io.seata</groupId>-->
<!--            <artifactId>seata-spring-boot-starter</artifactId>-->
<!--            <version>1.4.0</version>-->
<!--        </dependency>-->
        <!--feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--web-actuator-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <!--mysql-druid-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>org.mybatis.spring.boot</groupId>-->
<!--            <artifactId>mybatis-spring-boot-starter</artifactId>-->
<!--            <version>2.0.0</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.3</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.pyh.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
    </dependencies>
4.2.2 yaml
server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
#    alibaba:
#      seata:
#        tx-service-group: seata-order-group
    nacos:
      discovery:
        #server-addr: 192.168.226.128:8848   # #Nacos服务注册中心地址
        # 换成 nginx 的 1111 端口 nginx - keepalived - nacos
        #serverAddr: 192.168.226.129:1111
        server-addr: 192.168.226.129:1111
        namespace: fc3baf0a-17c6-4bc4-809b-6fc947adb309
        #这里的名字就是registry.conf中 nacos的group名字
        group: SEATA_GROUP
        userName: "nacos"
        password: "nacos"
#      config: # 目前暂时用不到 nacos.config
#        server-addr: 192.168.226.129:1111   # Nacos 作为配置中心地址
#        file-extension: yaml          # 指定 yaml 格式的配置
#        group: DEFAULT_GROUP         # 默认 DEFAULT_GROUP  # DEV_GROUP
#        namespace:   # 不填写则默认default, 这个在nacos的配置中找  fc3baf0a-17c6-4bc4-809b-6fc947adb309
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
#    url: jdbc:mysql://192.168.226.129:3306/seata_order?serverTimezone=UTC
    url: jdbc:mysql://192.168.226.129:3306/seata_order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: 123456
feign:
  hystrix:
    enabled: false
logging:
  level:
    io:
      seata: info
management:
  endpoints:
    web:
      exposure:
        include: '*'
mybatis:
  #  config-location: classpath:mybatis/mybatis-config.xml    # 不能与mybatis.configuration同用
  mapper-locations: classpath*:mybatis/mapper/*.xml
  configuration:  #指定mybatis全局配置文件中的相关配置, mybatis-config.xml无效
    map-underscore-to-camel-case: true
mybatis-plus:     #myBatis plus的配置文件位置
  mapper-locations: classpath*:mybatis/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
seata:
  enabled: true
  application-id: ${spring.application.name}    #你的当前服务的application name
  #这里的名字与file.conf中vgroup_mapping.seata-order-group = "default"相同
  tx-service-group: seata-order-group
  enable-auto-data-source-proxy: true
  service:
    #这里的名字与file.conf中vgroup_mapping.seata-order-group = "default"相同
    vgroup-mapping:
      seata-order-group: default
    vgroupMapping:
      seata-order-group: default
    #这里的名字与file.conf中default.grouplist = "127.0.0.1:8091"相同
    default:
      grouplist: 192.168.226.128:8091
  config:
    type: nacos
    nacos:
      namespace: fc3baf0a-17c6-4bc4-809b-6fc947adb309
      server-addr: 192.168.226.129:1111
      #这里的名字就是registry.conf中 nacos的group名字
      group: SEATA_GROUP
      userName: "nacos"
      password: "nacos"
  registry:
    type: nacos
    nacos:
      application: seata-server
      #这里的地址就是你的nacos的地址,可以更换为线上
      server-addr: 192.168.226.129:1111
      #这里的名字就是registry.conf中 nacos的group名字
      group: SEATA_GROUP
      namespace: fc3baf0a-17c6-4bc4-809b-6fc947adb309
      userName: "nacos"
      password: "nacos"
4.2.3 file.conf(deprecated)

由于使用了那cos作为配置文件存放地址,本节点可忽略
注意:
1). vgroup_mapping.sentinel_tx_group = “default”
sentinel_tx_group 是在sentinel中配置的事务组名称
2). database store 填好sql的连接参数

transport {
  # tcp, unix-domain-socket
  type = "TCP"
  #NIO, NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the client batch send request enable
  enableClientBatchSendRequest = false
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThreadPrefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}

service {
  #vgroup->rgroup
  #vgroup_mapping.seata-order-group = "default"
  vgroupMapping.seata-order-group = "default"
  #only support single node
  #default.grouplist = "127.0.0.1:8091"
  default.grouplist = "192.168.226.128:8091"
  #degrade current not support
  enableDegrade = false
  #disable
  disable = false
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  max.commit.retry.timeout = "-1"
  max.rollback.retry.timeout = "-1"
  disableGlobalTransaction = false
}

## transaction log store, only used in server side
store {
  ## store mode: file、db
  mode = "db"
  ## file store property
  file {
    ## store location dir
    dir = "sessionStore"
    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
    maxBranchSessionSize = 16384
    # globe session size , if exceeded throws exceptions
    maxGlobalSessionSize = 512
    # file buffer size , if exceeded allocate new buffer
    fileWriteBufferCacheSize = 16384
    # when recover batch read size
    sessionReloadReadSize = 100
    # async, sync
    flushDiskMode = async
  }

  ## database store property
  db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
    datasource = "dbcp"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.cj.jdbc.Driver"
    #url = "jdbc:mysql://127.0.0.1:3306/seata"
    url = "jdbc:mysql://192.168.226.129:3306/seata?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false"
    user = "root"
    password = "123456"
    minConn = 5
    maxConn = 30
    globalTable = "global_table"
    branchTable = "branch_table"
    lockTable = "lock_table"
    queryLimit = 100
  }
}
## server configuration, only used in server side
server {
  recovery {
    #schedule committing retry period in milliseconds
    committingRetryPeriod = 1000
    #schedule asyn committing retry period in milliseconds
    asynCommittingRetryPeriod = 1000
    #schedule rollbacking retry period in milliseconds
    rollbackingRetryPeriod = 1000
    #schedule timeout retry period in milliseconds
    timeoutRetryPeriod = 1000
  }
  undo {
    logSaveDays = 7
    #schedule delete expired undo_log in milliseconds
    logDeletePeriod = 86400000
  }
  #check auth
  enableCheckAuth = true
  #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
  maxCommitRetryTimeout = "-1"
  maxRollbackRetryTimeout = "-1"
  rollbackRetryTimeoutUnlockEnable = false
}

## metrics configuration, only used in server side
metrics {
  enabled = false
  registryType = "compact"
  # multi exporters use comma divided
  exporterList = "prometheus"
  exporterPrometheusPort = 9898
}
4.2.4 registry.conf(deprecated)

由于使用了那cos作为配置文件存放地址,本节点可忽略
注意:
1). type=“nacos”
2). 填写好nacos的相关参数

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "nacos"

  nacos {
    # serverAddr = "localhost:8848"
    # serverAddr: 192.168.226.128:8848   # 配置 nacos 地址
    # 换成 nginx 的 1111 端口 nginx - keepalived - nacos
    serverAddr = "192.168.226.129:1111"
    namespace = ""
    cluster = "default"
    #username = "nacos"
    #password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = "0"
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  consul {
    cluster = "default"
    serverAddr = "127.0.0.1:8500"
  }
  etcd3 {
    cluster = "default"
    serverAddr = "http://localhost:2379"
  }
  sofa {
    serverAddr = "127.0.0.1:9603"
    application = "default"
    region = "DEFAULT_ZONE"
    datacenter = "DefaultDataCenter"
    cluster = "default"
    group = "SEATA_GROUP"
    addressWaitTime = "3000"
  }
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"

  nacos {
    serverAddr = "localhost"
    namespace = ""
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    app.id = "seata-server"
    apollo.meta = "http://192.168.1.204:8801"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    session.timeout = 6000
    connect.timeout = 2000
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}
4.2.5 bean 或者 entity 或者 domain

Order:

package com.pyh.springcloud.bean;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_order")
public class Order {

    private Long id;
    private Long orderId;
    private Long userId;
    private Long productId;
    private Integer count;
    private BigDecimal money;
    private Integer status; //订单状态:0:创建中;1:已完结
}

CommonResult:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    // 404  not found
    private Integer code;
    private String message;
    private T data;
//    上面配置了空参和全参, 这里自定义一个只有编码和消息的构造方法
    public CommonResult(Integer code, String message){
        this(code,message,null);
    }
}
4.2.6 Dao(或者Mapper) 及其实现

1). OrderDao:

package com.pyh.springcloud.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pyh.springcloud.bean.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/* mybatis plus中,继承BaseMapper,就可以拥有基础的CRUD方法 */
@Mapper
public interface OrderDao extends BaseMapper<Order> {
    // 1. 新建订单
    void createOrder (Order order);
    // 2. 修改订单状态, 从 0 改为 1
    void update(@Param("orderId") Long orderId, @Param("status") Integer status);
}

2). OrderMapper.xml: (resources文件夹下新建mapper文件夹后添加)
在这里插入图片描述

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pyh.springcloud.dao.OrderDao">
    <!-- 这个可以通过启用 yaml中的驼峰配置来省略, 但是还是写在这里以加强印象
            map-underscore-to-camel-case: true -->
    <resultMap id="BaseResultMap" type="com.pyh.springcloud.bean.Order">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="order_id" property="orderId" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="count" property="count" jdbcType="INTEGER"/>
        <result column="money" property="money" jdbcType="DECIMAL"/>
        <result column="status" property="status" jdbcType="INTEGER"/>
    </resultMap>

    <insert id="createOrder" useGeneratedKeys="true" keyProperty="id">
        insert into t_order (id, order_id, user_id, product_id, count, money, status )
        values (null, #{orderId}, #{userId}, #{productId}, #{count}, #{money}, 0 )
    </insert>

    <update id="update" >
        update t_order set status = 1 where order_id = #{orderId} and status = #{status}
    </update>
</mapper>
4.2.7 Service 接口及其实现

service 要创建3个service
OrderService:

package com.pyh.springcloud.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pyh.springcloud.bean.Order;

public interface OrderService extends IService<Order> {
    void create(Order order);
}

AccountService:

package com.pyh.springcloud.service;

import com.pyh.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

@FeignClient(value = "seata-account-service")
public interface AccountService {
    // 该用户账户减少指定额度
    @PostMapping("/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

StorageService:

package com.pyh.springcloud.service;

import com.pyh.springcloud.entities.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "seata-storage-service")
public interface StorageService{
    // 某个商品减少指定库存量
    @PostMapping("/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

OrderServiceImpl:

package com.pyh.springcloud.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pyh.springcloud.bean.Order;
import com.pyh.springcloud.dao.OrderDao;
import com.pyh.springcloud.service.AccountService;
import com.pyh.springcloud.service.OrderService;
import com.pyh.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements OrderService {
    @Resource
    OrderDao orderDao;
    @Resource
    AccountService accountService;
    @Resource
    StorageService storageService;

    /**
     * 下订单->减库存->扣余额->改(订单)状态
     */
    @Override
    public void create(Order order) {

        log.info("--->step1 开始新建订单");
        // 1. 新建订单
        orderDao.createOrder(order);

        log.info("--->step2 订单微服务开始调用库存微服务, 扣减数量 start");
        // 2. 扣减库存
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->step2 订单微服务开始调用库存微服务, 扣减数量 end");

        log.info("----->step3 订单微服务开始调用账户微服务, 扣减money start");
        // 3. 扣账户钱
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->step3 订单微服务开始调用账户微服务, 作扣减money end");

        log.info("----->step4 修改订单状态从0 到 1 ,代表完成 start");
        // 4. 修改订单状态为完成
        // 这里传0,是把订单状态从0 改到 1
        orderDao.update(order.getOrderId(),0 );
        log.info("----->step4 修改订单状态从0 到 1 ,代表完成 end");

        log.info("----->新建订单 完成!!!");
    }
}
4.2.8 controller 类
package com.pyh.springcloud.controller;

import com.pyh.springcloud.bean.Order;
import com.pyh.springcloud.entities.CommonResult;
import com.pyh.springcloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    public CommonResult create(Order order)
    {
        orderService.create(order);
        return new CommonResult(200,"订单创建成功");
    }
}
4.2.9 config 配置

MybatisPlusConfig:

package com.pyh.springcloud.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.pyh.springcloud.dao")
public class MyBatisPlusConfig {
    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
//    @Bean
//    public MybatisPlusInterceptor mybatisPlusInterceptor() {
//        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//
//        //这是分页拦截器
//        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
//        paginationInnerInterceptor.setMaxLimit(500L);   // 每次最大查询500条数据
//        paginationInnerInterceptor.setOverflow(true); //超出页数后回滚
//
//        interceptor.addInnerInterceptor(paginationInnerInterceptor);
//        return interceptor;
//    }
//    @Bean
//    public ConfigurationCustomizer configurationCustomizer() {
//        return configuration -> configuration.setUseDeprecatedExecutor(false);
//    }
}

MyDataSourceConfig:

package com.pyh.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import io.seata.rm.datasource.DataSourceProxy;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.sql.SQLException;

/**
 * 用 seata 数据源进行代理
 */
//@Deprecated
@Configuration      //引入了druid-spring-boot-starter之后,可以不需要该配置
public class MyDataSourceConfig {

    @Value("${mybatis-plus.mapper-locations}")
    private String mapperLocations;

//    @ConfigurationProperties("spring.datasource")
    @ConfigurationProperties(prefix = "spring.datasource")  // 与上面相同
    @Bean
    public DataSource dataSource() throws SQLException {
		## 只需新建DruidDataSource,其他通过配置实现
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

    /**
     * 官网信息:
     * Register DataSourceProxy bean by yourself is discouraged since 1.3.
     * If you are using seata starter, you don't need to care about DataSourceProxy(starter would process it automatically),
     * just register and use Datasource bean in your old way.
     * @param dataSource
     * @return
     * @throws Exception
     */
// 目前使用seata 1.4.0版本, 官网建议1.3版本开始,不用自定义注册DataSourceProxy了
    // seata datasource proxy
//    @Bean
//    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
//        return new DataSourceProxy(dataSource);
//    }
    //代理mybaits-plus
    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean;
    }

    //代理mybatis / mybatis-config
/*    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }*/
}
4.2.10 主启动类
@EnableDiscoveryClient
@EnableFeignClients
//取消数据源自动创建的配置, 使用我们自己的数据源管理配置  MyDataSourceConfig
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class, args);
    }
}

启动 nacos, 启动seata, 并开始测试

cd /usr/local/seata_1.4.0/seata/bin/
sh seata-server.sh -h 192.168.226.128 -p 8091

4.3 踩坑说明

4.3.1 踩坑1 版本不匹配

错误如下:

Caused by: io.seata.common.exception.ShouldNeverHappenException: Can't find any object of class org.springframework.context.ApplicationContext

此类报错多是版本不匹配,解决方案是修改seata的版本问题:
参考资料: https://blog.csdn.net/qq_21959403/article/details/116135702
修改内容如下:

 	<!--  seata 依赖  -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        <version>2.2.5.RELEASE</version>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-spring-boot-starter</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-spring-boot-starter</artifactId>
        <version>1.4.0</version>
        <exclusions>
            <exclusion>
                <groupId>io.seata</groupId>
                <artifactId>seata-all</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <version>1.4.0</version>
    </dependency>
4.3.2 踩坑2 nacos config可看情况配置

maven 写了多余的nacos config starter依赖

Caused by: com.alibaba.nacos.api.exception.NacosException: endpoint is blank

参考资料:https://blog.csdn.net/gxy03/article/details/111463882
解决方案:如果不用config,删除即可

4.3.3 踩坑3
The following method did not exist: org.apache.ibatis.session.Configuration.getLanguageDriver(Ljava/lang/Class;)Lorg/apache/ibatis/scripting/LanguageDriver;

原因: Mybatis-plus与Mybatis依赖冲突问题
参考资料:https://blog.csdn.net/Octopus21/article/details/114879758

处理方案:看上方参考资料

升级mybatis包,由于引入的mybatis-plus-boot-starter已经集成了MyBatis包,所以这里需要首先将其exclude,然后引入单独的较高版本的MyBatis包依赖

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.mybatis</groupId>
                    <artifactId>mybatis</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.3</version>
        </dependency>
4.3.4 踩坑4 切记要开防火墙

这个问题卡了两个晚上!!!

firewall-cmd --permanent --add-port=8091/tcp

firewall-cmd --reload

4.4 新建库存Storage-Module

4.4.1 pom

同 4.2.1

4.4.2 yaml

同 4.2.2, 以下部分修改,其余相同

server:
  port: 2002

spring:
  application:
    name: seata-storage-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    #    url: jdbc:mysql://192.168.226.129:3306/seata_storage?serverTimezone=UTC
    url: jdbc:mysql://192.168.226.129:3306/seata_storage?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: 123456  
4.4.3 file.conf(deprecated)
4.4.4 registry.config(deprecated)
4.4.5 bean 或者 entity 或者 domain
package com.phy.springcloud.bean;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_storage")
public class Storage {
    private Long id;
    // 产品id
    private Long productId;
    //总库存
    private Integer total;
    //已用库存
    private Integer used;
    //剩余库存
    private Integer residue;
}

CommonResult.java 放在公共模块里面

4.4.6 Dao(或者Mapper) 及其实现

1). StorageDao

package com.phy.springcloud.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.phy.springcloud.bean.Storage;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * mybatis plus中,继承BaseMapper,就可以拥有基础的CRUD方法
 */
@Mapper
public interface StorageDao extends BaseMapper<Storage> {
    // 扣减库存
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

2). StorageMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.phy.springcloud.dao.StorageDao">

    <!-- 这个可以通过启用 yaml中的驼峰配置来省略, 但是还是写在这里以加强印象
            map-underscore-to-camel-case: true
    -->
    <resultMap id="BaseResultMap" type="com.phy.springcloud.bean.Storage">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="product_id" property="productId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="INTEGER"/>
        <result column="used" property="used" jdbcType="INTEGER"/>
        <result column="residue" property="residue" jdbcType="INTEGER"/>
    </resultMap>

     <update id="decrease" >
        update t_storage set used = used + ${count},residue = residue - ${count} where product_id = #{productId}
    </update>
</mapper>
4.4.7 Service 接口及其实现

1). StorageService:

package com.phy.springcloud.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.phy.springcloud.bean.Storage;

public interface StorageService extends IService<Storage> {
    /**
     * 扣减库存
     * @param productId
     * @param count
     */
    void decrease(Long productId, Integer count);
}

2).StorageServiceImpl

package com.phy.springcloud.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.phy.springcloud.bean.Storage;
import com.phy.springcloud.dao.StorageDao;
import com.phy.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class StorageServiceImpl extends ServiceImpl<StorageDao, Storage> implements StorageService {

    @Resource
    StorageDao storageDao;
    @Override
    public void decrease(Long productId, Integer count) {
        log.info("-----> StorageService库存微服务, 扣减库存 start");
        storageDao.decrease(productId, count);
        log.info("-----> StorageService库存微服务, 扣减库存 end");
    }
}
4.4.8 controller 类

StorageController:

package com.phy.springcloud.controller;

import com.phy.springcloud.service.StorageService;
import com.pyh.springcloud.entities.CommonResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class StorageController {
    @Resource
    StorageService storageService;
    // 某个商品减少指定库存量
    /**
     * 具体项目中还需考虑单号等数据
     * @param productId
     * @param count
     * @return
     */
    @PostMapping("/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count){
        storageService.decrease(productId,count);
        return new CommonResult(200,"扣减库存成功");
    }
}
4.4.9 config 配置
package com.phy.springcloud.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.phy.springcloud.dao")
public class MyBatisPlusConfig {
}

MyDataSourceConfig:

package com.phy.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.sql.SQLException;

/**
 * 用 seata 数据源进行代理
 */
//@Deprecated
@Configuration      //引入了druid-spring-boot-starter之后,可以不需要该配置
public class MyDataSourceConfig {

    @Value("${mybatis-plus.mapper-locations}")
    private String mapperLocations;

    //    @ConfigurationProperties("spring.datasource")
    @ConfigurationProperties(prefix = "spring.datasource")  // 与上面相同
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

    /**
     * 官网信息:
     * Register DataSourceProxy bean by yourself is discouraged since 1.3.
     * If you are using seata starter, you don't need to care about DataSourceProxy(starter would process it automatically),
     * just register and use Datasource bean in your old way.
     * @param dataSource
     * @return
     * @throws Exception
     */
    // seata datasource proxy
//    @Bean
//    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
//        return new DataSourceProxy(dataSource);
//    }
    //代理mybaits-plus
    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean;
    }

    //代理mybatis / mybatis-config
/*    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }*/
}
4.4.10 主启动类

SeataStorageMainApp2002.java

package com.phy.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@EnableDiscoveryClient
@EnableFeignClients
//取消数据源自动创建的配置, 使用我们自己的数据源管理配置  MyDataSourceConfig
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataStorageMainApp2002 {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageMainApp2002.class,args);
    }
}

4.5 新建账户Account-Module

4.5.1 pom

同 4.2.1

4.5.2 yaml

除以下部分修改,其余同4.2.2

server:
  port: 2003

spring:
  application:
    name: seata-account-service
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    #    url: jdbc:mysql://192.168.226.129:3306/seata_storage?serverTimezone=UTC
    url: jdbc:mysql://192.168.226.129:3306/seata_account?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: root
    password: 123456  
4.5.3 file.conf(deprecated)
4.5.4 registry.conf(deprecated)
4.5.5 bean 或者 entity 或者 domain
package com.pyh.springcloud.bean;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_account")
public class Account {
    private Long id;
    // 用户id
    private Long userId;
    // 用户总余额
    private Double total;
    // 用户已用余额
    private Double used;
    // 用户剩余余额;
    private Double residue;
}
4.5.6 Dao(或者Mapper) 及其实现

1)AccountDao.java

package com.pyh.springcloud.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pyh.springcloud.bean.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;
/**
 * mybatis plus中,继承BaseMapper,就可以拥有基础的CRUD方法
 */
@Mapper
public interface AccountDao extends BaseMapper<Account> {
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

2). AccountMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pyh.springcloud.dao.AccountDao">

    <!-- 这个可以通过启用 yaml中的驼峰配置来省略, 但是还是写在这里以加强印象
            map-underscore-to-camel-case: true
    -->
    <resultMap id="BaseResultMap" type="com.pyh.springcloud.bean.Account">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="user_id" property="userId" jdbcType="BIGINT"/>
        <result column="total" property="total" jdbcType="DECIMAL"/>
        <result column="used" property="used" jdbcType="DECIMAL"/>
        <result column="residue" property="residue" jdbcType="DECIMAL"/>
    </resultMap>

    <update id="decrease" >
        update t_account set used = used + ${money},residue = residue - ${money} where user_id = #{userId}
    </update>
</mapper>
4.5.7 Service 接口及其实现

AccountService:

package com.pyh.springcloud.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pyh.springcloud.bean.Account;

import java.math.BigDecimal;

public interface AccountService extends IService<Account> {
    void decrease(Long userId, BigDecimal money);
}

AccountServiceImpl:

package com.pyh.springcloud.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pyh.springcloud.bean.Account;
import com.pyh.springcloud.dao.AccountDao;
import com.pyh.springcloud.service.AccountService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.math.BigDecimal;

@Service
@Slf4j
public class AccountServiceImpl extends ServiceImpl<AccountDao, Account> implements AccountService {

    @Resource
    AccountDao accountDao;
    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("-----> StorageService库存微服务, 扣减库存 start");
        accountDao.decrease(userId,money);
        log.info("-----> StorageService库存微服务, 扣减库存 end");
    }
}
4.5.8 controller 类
package com.pyh.springcloud.controller;

import com.pyh.springcloud.entities.CommonResult;
import com.pyh.springcloud.service.AccountService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.math.BigDecimal;

@RestController
public class AccountController {
    @Resource
    AccountService accountService;
    // 该用户账户减少指定额度
    @PostMapping("/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
        accountService.decrease(userId, money);
        return new CommonResult(200, "用户账户扣减成功");
    }
}
4.5.9 config 配置

MyBatisPlusConfig:

package com.pyh.springcloud.config;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.pyh.springcloud.dao")
public class MyBatisPlusConfig {
}

MyDataSourceConfig:

package com.pyh.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.sql.SQLException;

/**
 * 用 seata 数据源进行代理
 */
//@Deprecated
@Configuration      //引入了druid-spring-boot-starter之后,可以不需要该配置
public class MyDataSourceConfig {

    @Value("${mybatis-plus.mapper-locations}")
    private String mapperLocations;

    //    @ConfigurationProperties("spring.datasource")
    @ConfigurationProperties(prefix = "spring.datasource")  // 与上面相同
    @Bean
    public DataSource dataSource() throws SQLException {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

    /**
     * 官网信息:
     * Register DataSourceProxy bean by yourself is discouraged since 1.3.
     * If you are using seata starter, you don't need to care about DataSourceProxy(starter would process it automatically),
     * just register and use Datasource bean in your old way.
     * @param dataSource
     * @return
     * @throws Exception
     */

    // seata datasource proxy
//    @Bean
//    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
//        return new DataSourceProxy(dataSource);
//    }
    //代理mybaits-plus
    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean;
    }

    //代理mybatis / mybatis-config
/*    @Bean
    public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSourceProxy);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
        return sqlSessionFactoryBean.getObject();
    }*/
}
4.5.10 主启动类
package com.pyh.springcloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

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

4.6 测试

4.6.1 正常情况使用postman:
http://localhost:2001/order/create?orderId=22222222&userId=5555&productId=9999&money=100&count=2
返回:
{
    "code": 200,
    "message": "订单创建成功",
    "data": null
}
4.6.2 异常情况:

为openfeign设置超时弹出异常,用其来测试
三个工程 yaml 添加如下配置

feign:
  client:
    config:
      default: # feign 全局日志
        #      cloud-payment-service:     #也可以指定某个 feign 服务名称
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: FULL  # 日志级别 FULL / headers / basic / none  默认
#      fiegnName: # fiegnName服务请求的配置,优先defalut配置。
#      loggerLevel: FULL   # 日志级别 FULL / headers / basic / none  默认
#      connectTimeout: 5000 # 链接超时时间
#      readTimeout: 5000  # 请求
#      errorDecoder: com.example.SimpleErrorDecoder #异常处理
#      retryer: com.example.SimpleRetryer # 重试策略
#      defaultQueryParameters: # 默认参数条件
#        query: queryValue
#      defaultRequestHeaders:  # 默认默认header
#        header: headerValue
#      requestInterceptors:    # 默认拦截器
#        - com.example.FooRequestInterceptor
#        - com.example.BarRequestInterceptor
#      decode404: false   #404响应 true-直接返回,false-抛出异常
#      encoder: com.example.SimpleEncoder  #传输编码
#      decoder: com.example.SimpleDecoder  #传输解码
#      contract: com.example.SimpleContract #传输协议
4.6.3 超时异常,没加@GlobalTransactional
	//AccountServiceImpl添加超时  
    @Override
    public void decrease(Long userId, BigDecimal money) {
        log.info("-----> StorageService库存微服务, 扣减库存 start");
        // 模拟超时异常,全局事务回滚
        accountDao.decrease(userId,money);
        try {
            TimeUnit.SECONDS.sleep(20);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("-----> StorageService库存微服务, 扣减库存 end");
    }
//当库存和账户余额扣减后,订单状态并没有设置为已经完成,没有从零改为1
//而且由于feign的重试机制,账户余额还有可能被多次扣减

在这里插入图片描述

4.6.4 超时异常,添加@GlobalTransactional
	// OrderServiceImpl.java 添加超时  
	// 发生异常则回滚
    @GlobalTransactional(name = "svs-create-order", rollbackFor = Exception.class)
    public void create(Order order) { 
		xxxx
	}

-----------下单失败后数据库数据并没有任何改变

5. 总结

再看TC/TM/RM三大组件

5.1 分布式事务的执行流程:

1. TM开启分布式事务(TM向TC注册全局事务记录)
2. 换业务场景,编排数据库,服务等事务内资源(RM向TC汇报资源准备状态)
3. TM结束分布式事务,事务一阶段结束(TM通知TC提交/回滚分布式事务)
4. TC汇总事务信息,决定分布式事务是提交还是回滚
5. TC通知所有RM提交/回滚资源,事务二阶段结束。

5.2 AT模式如何做到对业务的无侵入

5.2.1 seata 是什么

在这里插入图片描述
在这里插入图片描述

5.2.2 一阶段加载

在这里插入图片描述
在这里插入图片描述

5.2.3 二阶段提交

在这里插入图片描述

5.2.4 二阶段回滚

在这里插入图片描述
在这里插入图片描述

5.3 补充

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值