SpringCloud-2.0-周阳(24. 分布式事务 - Seata)学习笔记

上一篇 :23. 熔断降级 - Sentinel

下一篇 :25. 简述 Seata 的原理

1. 分布式事务的问题

  • 在分布式之前

是一台电脑上包含所有的东西 —— 所有的数据、程序所有的内容 ……

  • 慢慢向分布式演变

从 1 对 1 (一个程序对应一个数据库)
到 1 对 N (分库,一个程序对应多个数据库)
再 N 对 N (分布式微服务,多个微服务对应多个数据库)

  • 分布式之后

举例:
在这里插入图片描述

  • 单体应用被拆分成微服务应用,原来的三个模块被拆分成三个独立的应用,分别使用三个独立的数据源,业务操作需要调用三个服务来完成。
  • 此时每个服务内部的数据一致性由本地事务来保证,但是全局的数据一致性问题没法保证
  • 一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题

2. Seata 简介

  • 官网地址 :http://seata.io/zh-cn/

  • Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务

  • 一个 ID + 三个组件

  • Transaction ID XID :全局唯一的事务ID
  • Transaction Coordinator(TC)事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚;
  • Transaction Manager™控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议;
  • Resource Manager(RM)控制分支事务,负责分支注册,状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚;
  • 分布式事务处理过程

在这里插入图片描述

  1. TM向TC申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的XID
  2. XID在微服务调用链路的上下文中传播
  3. RM向TC注册分支事务,将其纳入XID对应全局事务的管辖
  4. TM向TC发起针对XID的全局提交或回滚决议;
  5. TC调度XID下管辖的全部分支事务完成提交或回滚请求。

3. Seata-Server 安装

  1. 下载地址 :https://github.com/seata/seata/releases
    在这里插入图片描述

  2. 将 seata-server 解压到指定目录

  3. 修改 conf 目录下的 file.conf 文件

    先对配置文件进行备份

    在这里插入图片描述

  4. 在 Mysql 中新建一个数据库 :seata
    在这里插入图片描述
    在这里插入图片描述

  5. 初始化数据库

    运行 conf 目录下的 db_store.sql 文件

    如果没有,请看,SQL 脚本文件地址(选择自己的数据库) :https://github.com/seata/seata/tree/develop/script/server/db
    复制下来去 MySQL 执行

    在这里插入图片描述

  6. 修改 conf 目录下的 registry.conf 文件
    在这里插入图片描述

  7. 启动 Nacos

  8. 启动 seata-server

    双击 seata\bin\seata-server.bat

4. 数据库环境搭建

  • 下面就按照该框架图进行设计

在这里插入图片描述
这里我们会创建三个服务 —— 一个订单服务,一个库存服务,一个账户服务。

  • 当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,
  • 再通过远程调用账户服务来扣减用户账户里面的余额,
  • 最后在订单服务中修改订单状态为已完成。

该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

  1. 启动 Nacos 、Seata

  2. 创建业务数据库

    seata_order: 存储订单的数据库
    seata_storage:存储库存的数据库
    seata_account: 存储账户信息的数据库

    建表SQL :

    CREATE DATABASE seata_order;
    CREATE DATABASE seata_storage;
    CREATE DATABASE seata_account;
    

    在这里插入图片描述

  3. seata_order 库下建 t_order 表

    CREATE TABLE t_order(
        `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
        `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
        `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
        `count` INT(11) DEFAULT NULL COMMENT '数量',
        `money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
        `status` INT(1) DEFAULT NULL COMMENT '订单状态:0:创建中; 1:已完结'
    ) ENGINE=INNODB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
     
    SELECT * FROM t_order;
    
  4. seata_storage 库下建 t_storage 表

    CREATE TABLE t_storage(
        `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
        `product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
        `total` INT(11) DEFAULT NULL COMMENT '总库存',
        `used` INT(11) DEFAULT NULL COMMENT '已用库存',
        `residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
    ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
     
    INSERT INTO seata_storage.t_storage(`id`,`product_id`,`total`,`used`,`residue`)
    VALUES('1','1','100','0','100');
     
    SELECT * FROM t_storage;
    
  5. seata_account 库下建 t_account 表

    CREATE TABLE t_account(
        `id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
        `user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
        `total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
        `used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
        `residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
    ) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
     
    INSERT INTO seata_account.t_account(`id`,`user_id`,`total`,`used`,`residue`) VALUES('1','1','1000','0','1000')
    
    SELECT * FROM t_account;
    

    在这里插入图片描述

  6. 建立回滚日志表

    找到 Seat 中回滚日志建表 SQL 脚本 :\seata\conf\db_undo_log.sql

    三个数据库都需要执行该脚本

    CREATE TABLE `undo_log` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `branch_id` bigint(20) NOT NULL,
      `xid` varchar(100) NOT NULL,
      `context` varchar(128) NOT NULL,
      `rollback_info` longblob NOT NULL,
      `log_status` int(11) NOT NULL,
      `log_created` datetime NOT NULL,
      `log_modified` datetime NOT NULL,
      `ext` varchar(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    

    在这里插入图片描述

5. 代码环境搭建

5.1 订单模块

  • 项目目录:

在这里插入图片描述

  1. 新建模块 :seata-order-service-2001

  2. 修改 POM

    <dependencies>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>seata-all</artifactId>
                    <groupId>io.seata</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.seata</groupId>
            <artifactId>seata-all</artifactId>
            <version>0.9.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>
            <version>5.1.37</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.10</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</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>
    </dependencies>
    
  3. 编写 YML

    server:
      port: 2001
    
    spring:
      application:
        name: seata-order-service
      cloud:
        alibaba:
          seata:
            #自定义事务组名称需要与seata-server中的对应
            tx-service-group: fsp_tx_group
        nacos:
          discovery:
            server-addr: localhost:8848
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seata_order?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        username: root
        password: 123456
    
    feign:
      hystrix:
        enabled: false
    
    logging:
      level:
        io:
          seata: info
    
    mybatis:
      mapper-locations: classpath:mapper/*.xml
      type-aliases-package: com.demo.springcloud.pojo
    
  4. 编写 file.conf 文件

    transport {
      # tcp udt unix-domain-socket
      type = "TCP"
      #NIO NATIVE
      server = "NIO"
      #enable heartbeat
      heartbeat = true
      #thread factory for netty
      thread-factory {
        boss-thread-prefix = "NettyBoss"
        worker-thread-prefix = "NettyServerNIOWorker"
        server-executor-thread-prefix = "NettyServerBizHandler"
        share-boss-worker = false
        client-selector-thread-prefix = "NettyClientSelector"
        client-selector-thread-size = 1
        client-worker-thread-prefix = "NettyClientWorkerThread"
        # netty boss thread size,will not be used for UDT
        boss-thread-size = 1
        #auto default pin or 8
        worker-thread-size = 8
      }
      shutdown {
        # when destroy server, wait seconds
        wait = 3
      }
      serialization = "seata"
      compressor = "none"
    }
    
    service {
    
      vgroup_mapping.fsp_tx_group = "default"
    
      default.grouplist = "127.0.0.1:8091"
      enableDegrade = false
      disable = false
      max.commit.retry.timeout = "-1"
      max.rollback.retry.timeout = "-1"
      disableGlobalTransaction = false
    }
    
    
    client {
      async.commit.buffer.limit = 10000
      lock {
        retry.internal = 10
        retry.times = 30
      }
      report.retry.count = 5
      tm.commit.retry.count = 1
      tm.rollback.retry.count = 1
    }
    
    ## transaction log store
    store {
      ## store mode: file、db
      mode = "db"
    
      ## file store
      file {
        dir = "sessionStore"
    
        # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
        max-branch-session-size = 16384
        # globe session size , if exceeded throws exceptions
        max-global-session-size = 512
        # file buffer size , if exceeded allocate new buffer
        file-write-buffer-cache-size = 16384
        # when recover batch read size
        session.reload.read_size = 100
        # async, sync
        flush-disk-mode = async
      }
    
      ## database store
      db {
        ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
        datasource = "dbcp"
        ## mysql/oracle/h2/oceanbase etc.
        db-type = "mysql"
        driver-class-name = "com.mysql.jdbc.Driver"
        url = "jdbc:mysql://127.0.0.1:3306/seata"
        user = "root"
        password = "123456"
        min-conn = 1
        max-conn = 3
        global.table = "global_table"
        branch.table = "branch_table"
        lock-table = "lock_table"
        query-limit = 100
      }
    }
    lock {
      ## the lock store mode: local、remote
      mode = "remote"
    
      local {
        ## store locks in user's database
      }
    
      remote {
        ## store locks in the seata's server
      }
    }
    recovery {
      #schedule committing retry period in milliseconds
      committing-retry-period = 1000
      #schedule asyn committing retry period in milliseconds
      asyn-committing-retry-period = 1000
      #schedule rollbacking retry period in milliseconds
      rollbacking-retry-period = 1000
      #schedule timeout retry period in milliseconds
      timeout-retry-period = 1000
    }
    
    transaction {
      undo.data.validation = true
      undo.log.serialization = "jackson"
      undo.log.save.days = 7
      #schedule delete expired undo_log in milliseconds
      undo.log.delete.period = 86400000
      undo.log.table = "undo_log"
    }
    
    ## metrics settings
    metrics {
      enabled = false
      registry-type = "compact"
      # multi exporters use comma divided
      exporter-list = "prometheus"
      exporter-prometheus-port = 9898
    }
    
    support {
      ## spring
      spring {
        # auto proxy the DataSource bean
        datasource.autoproxy = false
      }
    }
    
  5. 编写 registry.conf

    registry {
      # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
      type = "nacos"
    
      nacos {
        serverAddr = "localhost:8848"
        namespace = ""
        cluster = "default"
      }
      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"
      }
    }
    
  6. 实体类

  • CommonResult

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class CommonResult<T> {
    	private Integer code;
    	private String  message;
    	private T       data;
    
    	public CommonResult(Integer code, String message)
    	{
    		this(code,message,null);
    	}
    }
    
  • Order

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Order {
    	private Long id;
        
    	/*用户id*/
    	private Long userId;
        
    	/*产品id*/
    	private Long productId;
        
    	/*数量*/
    	private Integer count;
        
    	/*金额*/
    	private BigDecimal money;
        
    	/*订单状态:0:创建中; 1:已完结*/
    	private Integer status;
    }
    
  1. Mapper / Dao 层实现
  • TOrderMapper

    @Mapper
    public interface TOrderMapper {
    	/**
         * 查询所有记录
         *
         * @return 返回集合,没有返回空List
         */
    	List<Order> listAll();
    
    	/**
         * 新增,插入所有字段
         *
         * @param order 新增的记录
         * @return 返回影响行数
         */
    	int insert(Order order);
    	
    	/**
         * 修改
         */
    	int update(@Param("id") Long id, @Param("status") Integer status);
    }
    
  • TOrderMapper.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.demo.springcloud.mapper.TOrderMapper">
        
        <resultMap id="BaseResultMap" type="Order">
            <result column="id" property="id" 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>
        
        <!-- 表字段 -->
        <sql id="baseColumns">
             t.id
            , t.user_id
            , t.product_id
            , t.count
            , t.money
            , t.status
            </sql>
        
        <!-- 查询全部 -->
        <select id="listAll" resultMap="BaseResultMap">
            SELECT
            <include refid="baseColumns"/>
            FROM t_order t
        </select>
        
        <!-- 插入不为NULL的字段 -->
        <insert id="insert" parameterType="Order"
                keyProperty="id" keyColumn="id" useGeneratedKeys="true"
        >
            INSERT INTO t_order
            <trim prefix="(" suffix=")" suffixOverrides=",">
                <if test="id != null">
                    id,
                </if>
                <if test="userId != null">
                    user_id,
                </if>
                <if test="productId != null">
                    product_id,
                </if>
                <if test="count != null">
                    count,
                </if>
                <if test="money != null">
                    money,
                </if>
                    status
            </trim>
            <trim prefix="VALUES (" suffix=")" suffixOverrides=",">
                <if test="id != null">
                    #{id},
                </if>
                <if test="userId != null">
                    #{userId},
                </if>
                <if test="productId != null">
                    #{productId},
                </if>
                <if test="count != null">
                    #{count},
                </if>
                <if test="money != null">
                    #{money},
                </if>
                    0
            </trim>
        </insert>
        
        <!-- 更新不为NULL的字段 -->
        <update id="update" >
            UPDATE t_order
            <set>
                <if test="status != null">
                    status=#{status},
                </if>
            </set>
            WHERE id = #{id}
        </update>
    </mapper>
    
  1. Service 接口及实现类
  • TOrderService

    @Service
    public interface TOrderService {
    
        /**
         * 查询所有记录
         *
         * @return 返回集合,没有返回空List
         */
        public List<Order> listAll();
    	
    	public int insertTest(Order order);
        /**
         * 新增,插入所有字段
         *
         * @param order 新增的记录
         * @return 返回影响行数
         */
        public int insert(Order order);
    	
        /**
         * 修改
         */
        int update(Long id,Integer status);
    }
    
  • TStorageService

    // 配置需要调用的微服务
    @FeignClient(value = "seata-storage-service")
    public interface TStorageService {
        @GetMapping(value = "/storage/decrease")
        CommonResult decrease(@RequestParam("productId")Long productId, @RequestParam("count")Integer count);
    }
    
  • TAccountService

    // 配置需要调用的微服务
    @FeignClient(value = "seata-account-service")
    public interface TAccountService {
        @GetMapping(value = "/account/decrease")
        CommonResult decrease(@RequestParam("userId")Long userId, @RequestParam("money") BigDecimal money);
    }
    
  • TOrderServiceImpl

    @Service
    @Slf4j
    public class TOrderServiceImpl implements TOrderService {
    
        @Autowired
        private TOrderMapper tOrderMapper;
        @Autowired
        private TAccountService accountService;
        @Autowired
        private TStorageService storageService;
    
        /**
         * 查询所有记录
         *
         * @return 返回集合,没有返回空List
         */
        @Override
        public List<Order> listAll() {
        	return tOrderMapper.listAll();
        }
    
        /**
         * 用于测试本模块的是否搭建成功
         *
         * @param order
         * @return
         */
        @Override
        public int insertTest(Order order) {
            return tOrderMapper.insert(order);
        }
    	
        /**
         * 新增,插入所有字段
         *
         * @param order 新增的记录
         * @return 返回影响行数
         */
        @Override
        public int insert(Order order) {
            System.out.println(order.toString());
            log.info("--->>>> 开始新建订单 <<<< ");
            int ordering = tOrderMapper.insert(order);
    
            log.info("--->>>> 订单微服务开始调用 库存,做扣减 <<<< ");
            CommonResult storageResult = storageService.decrease(order.getProductId(), order.getCount());
            Integer storage = (Integer) storageResult.getData();
            log.info("---<<<< 库存 扣减完成 >>>> ");
    
            log.info("--->>>> 订单微服务开始调用 账户,做扣减 <<<< ");
            CommonResult accountResult = accountService.decrease(order.getUserId(), order.getMoney());
            Integer account = (Integer) accountResult.getData();
            log.info("---<<<< 账户 扣减完成 >>>> ");
    
            log.info("--->>>> 修改订单状态 <<<< ");
            int update = tOrderMapper.update(order.getId(), 1);
            log.info("---<<<< 状态 修改完成 >>>> ");
    
            if (ordering > 0 && storage > 0 && account > 0 && update > 0){
                log.info("---<<<< 订单生成成功 (*^_^*) >>>> ");
                return 1;
            }
            return 0;
        }
    
        /**
         * 修改
         */
        @Override
        public int update(Long id, Integer status) {
            return tOrderMapper.update(id, status);
        }
    }
    
  1. Controller:TOrderController

    @RestController
    public class TOrderController {
    
        @Autowired
        private TOrderService tOrderService;
    
        /**
         * 查询所有记录
         *
         * @return 返回集合,没有返回空List
         */
        @GetMapping("/list")
        public CommonResult getAll(){
            return new CommonResult(200, tOrderService.listAll().toString());
        }
    
        /**
         * 用于测试本模块的是否搭建成功
         *
         * @param order
         * @return
         */
        @GetMapping("/test")
        public CommonResult insertTest(Order order) {
            tOrderService.insertTest(order);
            return new CommonResult(200, tOrderService.listAll().toString());
        }
    
        @GetMapping("/order")
        public CommonResult order(Order order) {
            int insert = tOrderService.insert(order);
            if (insert > 0){
                return new CommonResult(200, "订单创建完成");
            }
            return new CommonResult(444, "订单创建失败");
        }
        
    }
    
  2. 配置数据源,替换掉 SpringBoot 默认的,换上 Seata 的

  • MyBatisConfig

    @Configuration
    @MapperScan({"com.demo.springcloud.mapper"})
    public class MyBatisConfig {}
    
  • DataSourceProxyConfig

    @Configuration
    public class DataSourceProxyConfig {
        @Autowired
        private Environment env;
    
        @Value("${mybatis.mapper-locations}")
        private String mapperLocations;
    
        static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
    
        @Bean
        @ConfigurationProperties(prefix = "spring.datasource")
        public DataSource druidDataSource() {
            return new DruidDataSource();
        }
    
    
        @Bean
        public DataSourceProxy dataSourceProxy(DataSource dataSource) {
            return new DataSourceProxy(dataSource);
        }
    
        /**
         * 用于配置 Mybatis 的别名
         * @param typeAliasesPackage
         * @return
         */
        public static String setTypeAliasesPackage(String typeAliasesPackage)
        {
            ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
            MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
            List<String> allResult = new ArrayList<String>();
            try
            {
                for (String aliasesPackage : typeAliasesPackage.split(","))
                {
                    List<String> result = new ArrayList<String>();
                    aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                            + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
                    Resource[] resources = resolver.getResources(aliasesPackage);
                    if (resources != null && resources.length > 0)
                    {
                        MetadataReader metadataReader = null;
                        for (Resource resource : resources)
                        {
                            if (resource.isReadable())
                            {
                                metadataReader = metadataReaderFactory.getMetadataReader(resource);
                                try
                                {
                                    result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                                }
                                catch (ClassNotFoundException e)
                                {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                    if (result.size() > 0)
                    {
                        HashSet<String> hashResult = new HashSet<String>(result);
                        allResult.addAll(hashResult);
                    }
                }
                if (allResult.size() > 0)
                {
                    typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
                }
                else
                {
                    throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
                }
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            return typeAliasesPackage;
        }
    
        @Bean
        public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
            String typeAliasesPackage = env.getProperty("mybatis.type-aliases-package");
            typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
    
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSourceProxy);
            sqlSessionFactoryBean.setTypeAliasesPackage(typeAliasesPackage);
            sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
            sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
            return sqlSessionFactoryBean.getObject();
        }
    }
    
  1. 主启动类

    @EnableDiscoveryClient
    @EnableFeignClients
    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)//取消数据源自动创建的配置
    public class SeataOrderMainApp2001{
        public static void main(String[] args) {
            SpringApplication.run(SeataOrderMainApp2001.class, args);
        }
    }
    
  2. 启动测试

    访问 :http://localhost:2001/test?id=1&userId=1&productId=1&count=22&money=66

    在这里插入图片描述

5.2 库存模块

  1. 新建模块 :seata-order-service-2002

  2. 修改 POM

    和 2001 的 POM 依赖一样

    除了 —— 将 spring-boot-starter-actuator 去掉

  3. 编写 YML

    2001 模块一样,除了端口号和服务名

  4. 配置 file.conf 、registry.conf

    和 2001 模块的一样

  5. 实体类

  • CommonResult
    和 2001 模块的一样

  • Storage

    @Data
    public class Storage {
        private Long id;
        // 产品id
        private Long productId;
        //总库存
        private Integer total;
        //已用库存
        private Integer used;
        //剩余库存
        private Integer residue;
    }
    
  1. Mapper / Dao
  • StorageMapper

    @Mapper
    public interface StorageMapper {
        //扣减库存信息
        int decrease(@Param("productId") Long productId, @Param("count") Integer count);
    }
    
  • 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.demo.springcloud.mapper.StorageMapper">
        <resultMap id="BaseResultMap" type="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>
    
  1. Service 接口及实现类
  • StorageService

    @Service
    public interface StorageService {
        // 扣减库存
        int decrease(Long productId, Integer count);
    }
    
  • StorageServiceImpl

    @Service
    @Slf4j
    public class StorageServiceImpl implements StorageService {
        @Resource
        private StorageMapper storageMapper;
        // 扣减库存
        @Override
        public int decrease(Long productId, Integer count) {
            log.info("--->>>> storage-service中扣减库存开始 <<<< ");
            int decrease = storageMapper.decrease(productId, count);
            log.info("---<<<< storage-service中扣减库存结束 >>>> ");
            return decrease;
        }
    }
    
  1. Controller
  • StorageController

    @RestController
    @Slf4j
    public class StorageController {
    
        @Autowired
        private StorageService storageService;
    
        //扣减库存
        @RequestMapping("/storage/decrease")
        public CommonResult decrease(Long productId, Integer count) {
            int decrease = storageService.decrease(productId, count);
            log.info("------ storage-decrease-影响行数 :"+ decrease);
            return new CommonResult(200,"扣减库存成功!", decrease);
        }
    }
    
  1. 数据源配置,改用 Seata 的

    和 2001 模块一样

  2. 主启动类

  • SeataStorageServiceApplication2002

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataStorageServiceApplication2002 {
        public static void main(String[] args) {
            SpringApplication.run(SeataStorageServiceApplication2002.class, args);
        }
    }
    
  1. 启动项目测试

    访问 :http://localhost:2002/storage/decrease?productId=1&count=1

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

5.3 账户模块

  1. 新建模块 :seata-order-service-2003

  2. 修改 POM

    2002 模块一样

  3. 编写 YML

    2002 模块一样,除了端口号和服务名

  4. 配置 file.conf 、registry.conf

    2002 模块一样

  5. 实体类

  • CommonResult,和 2002 模块一样

  • Account

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Account {
        private Long id;
    
        /** 用户id */
        private Long userId;
    
        /** 总额度 */
        private BigDecimal total;
    
        /** 已用额度 */
        private BigDecimal used;
    
        /**  剩余额度 */
        private BigDecimal residue;
    }
    
  1. Mapper : AccountMapper

    @Mapper
    public interface AccountMapper {
        /** 扣减账户余额 */
        int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
    }
    
  • 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.demo.springcloud.mapper.AccountMapper">
        
        <resultMap id="BaseResultMap" type="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
              residue = residue - #{money},used = used + #{money}
            WHERE
              user_id = #{userId};
        </update>
    
    </mapper>
    
  1. Service 接口及实现类
  • AccountService

    @Service
    public interface AccountService {
        /** 扣减账户余额  */
        int decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
    }
    
  • AccountServiceImpl

    @Service
    @Slf4j
    public class AccountServiceImpl implements AccountService {
        @Resource
        private AccountMapper accountMapper;
    
        /** 扣减账户余额 */
        @Override
        public int decrease(Long userId, BigDecimal money) {
            log.info("--->>>> account-service中扣减账户余额开始 <<<< ");
            int decrease = accountMapper.decrease(userId, money);
            log.info("---<<<< account-service中扣减账户余额结束 >>>> ");
            return decrease;
        }
    }
    
  1. Controller :AccountController

    @RestController
    @Slf4j
    public class AccountController {
        @Resource
        AccountService accountService;
    
        /** 扣减账户余额 */
        @RequestMapping("/account/decrease")
        public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
            int decrease = accountService.decrease(userId, money);
            log.info("------ account-decrease-影响行数 :"+ decrease);
            return new CommonResult(200,"扣减账户余额成功!", decrease);
        }
    }
    
  2. 数据源配置,改用 Seata 的

    和 2002 模块一样

  3. 主启动类 :SeataAccountMainApp2003

    @SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
    @EnableDiscoveryClient
    @EnableFeignClients
    public class SeataAccountMainApp2003 {
        public static void main(String[] args) {
            SpringApplication.run(SeataAccountMainApp2003.class, args);
        }
    }
    
  4. 启动测试

    访问 :http://localhost:2003/account/decrease?userId=1&money=11
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

6. 整合 Seata 测试

  • 上面已经把环境都搭建完成了,下面就是整合 Seata 进行测试使用了
  • 下面的测试斗志针对 2001 模块的
  • 测试之前,最好先把数据恢复,这样等等的效果比较直观
  1. 成功执行的测试

    访问 :http://localhost:2001/order?id=1&userId=1&productId=1&count=22&money=66
    在这里插入图片描述
    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

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

  2. 模拟异常

  1. 当库存和账户余额扣减后,订单状态并没有设置为已经完成,没有从零改为1
  2. 而且由于 feign 的重试机制,账户余额还有可能被多次扣减,(Account 就出现了重复扣减的情况)
  1. 在 2001 中需要进行事务的方法上添加 @GlobalTransactional

    @GlobalTransactional(name = "fps-create-order", rollbackFor = Exception.class)
    在这里插入图片描述

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yuan_404

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值