依照《尚硅谷》分布式事务使用Seata1.3.0版本进行构建测试

1、Seata分布式事务

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

本篇是一篇小白教程


1.1、Seata 简介


Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了 ATTCCSAGAXA事务模式,为用户打造一站式的分布式解决方案。

1.2、Seata事务处理模型

关于Seata事务处理模型,详见http://seata.io/zh-cn/blog/seata-at-mode-design.html


Seata 内部定义了 3个模块来处理全局事务分支事务的关系和处理过程,这三个组件分别是:

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

img

简要说说整个全局事务的执行步骤:

  1. TMTC申请开启一个全局事务,TC创建全局事务后返回全局唯一的XIDXID 会在全局事务的上下文中传播;
  2. RMTC注册分支事务,该分支事务归属于拥有相同 XID的全局事务;
  3. TMTC发起全局提交或回滚;
  4. TC 调度XID 下的分支事务完成提交或者回滚。

1.3、安装配置环境启动


一、下载

下载地址:https://seata.io/zh-cn/blog/download.html

我当前下载的版本为:1.3.0 (2020-07-14)binary

后续如果存在问题,再对其解释更换

二、安装配置

下载完毕后,解压到目录

主要修改:事务日志存储模式为db+数据库连接信息+配置中心连接信息

1.3.1、file.conf日志存储模式为db


修改seata-server-1.3.0\seata\conf目录下的file.conf文件,建议先先备份原始file.conf文件,修改conf目录下的file.conf配置文件

原:

## transaction log store, only used in seata-server
store {
  ## store mode: file、db、redis
  mode = "file"

  ## file store property
  file {
    ## store location dir
    ...
  }  
}  

修改为:

mode = "db"

1.3.2、file.conf配置数据库连接信息


数据库连接信息:

原:

## database store property
db {
    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
    datasource = "druid"
    ## mysql/oracle/postgresql/h2/oceanbase etc.
    dbType = "mysql"
    driverClassName = "com.mysql.jdbc.Driver"
    url = "jdbc:mysql://127.0.0.1:3306/seata"
    user = "mysql"
    password = "mysql"
    minConn = 5
    maxConn = 30
}

修改为:

url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "123456"

1.3.3、file.conf新增service配置


代码:单独配置一块,由于我使用1.3.0版本,没有该配置,所以对其配置

service {
  #transaction service group mapping
  vgroupMapping.my_test_tx_group = "default"
  #only support when registry.type=file, please don't set multiple addresses
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

如果没有该配置,等下连接seata会抛出endpoint format should like ip:port异常信息

  • 该篇地址会有介绍,跟vgroupMapping.my_test_tx_group = "default"有关,等下进行业务模块配置yaml就明白了

1.3.4、搭建数据库环境


根据上方连接信息搭建对应数据库:seata

CREATE DATABASE seata;

创建数据库表:前往https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql复制对应代码

-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
    `xid`                       VARCHAR(128) NOT NULL,
    `transaction_id`            BIGINT,
    `status`                    TINYINT      NOT NULL,
    `application_id`            VARCHAR(32),
    `transaction_service_group` VARCHAR(32),
    `transaction_name`          VARCHAR(128),
    `timeout`                   INT,
    `begin_time`                BIGINT,
    `application_data`          VARCHAR(2000),
    `gmt_create`                DATETIME,
    `gmt_modified`              DATETIME,
    PRIMARY KEY (`xid`),
    KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
    KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
    `branch_id`         BIGINT       NOT NULL,
    `xid`               VARCHAR(128) NOT NULL,
    `transaction_id`    BIGINT,
    `resource_group_id` VARCHAR(32),
    `resource_id`       VARCHAR(256),
    `branch_type`       VARCHAR(8),
    `status`            TINYINT,
    `client_id`         VARCHAR(64),
    `application_data`  VARCHAR(2000),
    `gmt_create`        DATETIME(6),
    `gmt_modified`      DATETIME(6),
    PRIMARY KEY (`branch_id`),
    KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
    `row_key`        VARCHAR(128) NOT NULL,
    `xid`            VARCHAR(128),
    `transaction_id` BIGINT,
    `branch_id`      BIGINT       NOT NULL,
    `resource_id`    VARCHAR(256),
    `table_name`     VARCHAR(32),
    `pk`             VARCHAR(36),
    `gmt_create`     DATETIME,
    `gmt_modified`   DATETIME,
    PRIMARY KEY (`row_key`),
    KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
    `lock_key`       CHAR(20) NOT NULL,
    `lock_value`     VARCHAR(20) NOT NULL,
    `expire`         BIGINT,
    primary key (`lock_key`)
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

1.3.5、registry.conf配置注册中心连接信息


修改seata-server-1.3.0\seata\conf目录下的registry.conf文件

原:

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

    nacos {
        application = "seata-server"
        serverAddr = "127.0.0.1:8848"
        group = "SEATA_GROUP"
        namespace = ""
        cluster = "default"
        username = ""
        password = ""
    }
    ....
}

修改为:

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

  nacos {
    username = "nacos"
    password = "nacos"
  }
}

1.3.6、启动


进入seata-server-1.3.0\seata\bin目录,启动seata-server.bat

注意启动前:需要先启动nacos服务,否则会抛出连接失败异常

E:\program_jar\seata-server-1.3.0\seata\bin>seata-server.bat
Java HotSpot(TM) 64-Bit Server VM warning: Cannot open file E:\program_jar\seata-server-1.3.0\seata\bin\\../logs/seata_gc.log due to No such file or directory

1.4、业务环境搭建

分布式事务业务说明

这里我们会创建三个服务,一个订单服务,一个库存服务,一个账户服务

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

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


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

1.4.1、三个数据库相关配置


我们需要创建三个业务数据库

  • seata_ order:存储订单的数据库;
  • seata_ storage:存储库存的数据库;
  • seata_ account:存储账户信息的数据库。
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

seata_ order订单表:

USE seata_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 CHARSET=utf8;

SELECT * FROM t_order;

seata_ storage库存表:

USE seata_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=7 CHARSET=utf8;
INSERT INTO t_storage(id, product_id, total, used, residue) VALUES(1,1,100,0,100);
SELECT * FROM t_storage;

seata_ account账户信息表:

USE seata_account;
CREATE TABLE t_account(
    id BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    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=7 CHARSET=utf8;
INSERT INTO t_account(id, user_id, total, used, residue) VALUES(1,1,1000,0,1000);
SELECT * FROM t_account;

接着需要为每个数据库,建立一个回滚日志表,如何键,阅读seata-server-1.3.0\seata\conf\README-zh.md文件,找到undo_log 建表语句指向地址

所以前往https://github.com/seata/seata/blob/develop/script/client/at/db/mysql.sql

在每个业务数据库中执行:

-- for AT mode you must to init this sql for you business database. the seata server not need it.
CREATE TABLE IF NOT EXISTS `undo_log`
(
    `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
    `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
    `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
    `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
    `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
    `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
    `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
    UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
  AUTO_INCREMENT = 1
  DEFAULT CHARSET = utf8 COMMENT ='AT transaction mode undo table';

1.4.2、订单业务模块搭建


模块名:seata-order-service2001

1.4.2.1、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>
        <!-- 排除内置的Seata -->
        <exclusions>
            <exclusion>
                <artifactId>seata-all</artifactId>
                <groupId>io.seata</groupId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>io.seata</groupId>
        <artifactId>seata-all</artifactId>
        <!-- 根据您Seata服务的版本进行选择 -->
        <version>1.3.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>
        <version>1.1.10</version>
    </dependency>

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
    </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>
1.4.2.2、yaml

代码:

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    alibaba:
      seata:
        enabled: true
        application-id: ${spring.application.name}
        #自定义事务组名称需要与seata-server的file.conf的service组中的vgroupMapping后面那串
        #对应
        tx-service-group: my_test_tx_group
    nacos:
      discovery:
        server-addr: localhost:8848
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata_order
    username: root
    password: 123456

feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

mybatis:
  mapperLocations: classpath:mapper/*.xml
1.4.2.3、file.conf、registry.conf复制到模块中

我们需要把配置完毕的seata服务的file.confregistry.conf复制到resources目录下

在这里插入图片描述

1.4.2.4、domain实体类

数据格式返回类:

package com.migu.springcloud.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@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);
    }
}

对应数据库订单表实体类:

package com.migu.springcloud.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order
{
    private Long id;

    private Long userId;

    private Long productId;

    private Integer count;

    private BigDecimal money;

    private Integer status; //订单状态:0:创建中;1:已完结
}
1.4.2.5、dao接口配置

dao层:

package com.migu.springcloud.dao;

import com.migu.springcloud.domain.Order;
import org.apache.ibatis.annotations.Param;

public interface OrderDao {
    // 1、新建订单
    void create(Order order);

    // 2、修改订单状态( 0 --> 1)
    void update(@Param("userId") Long userId,@Param("status") Integer status);
}

mapper层:根据上方yaml配置到对应目录进行创建,也就是resources/mapper,这边取名为OrderMapper.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.migu.springcloud.dao.OrderDao">
    
    <resultMap id="BaseResultMap" type="com.migu.springcloud.domain.Order">
        <id 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>
    
    <insert id="create" parameterType="com.migu.springcloud.domain.Order">
        insert into t_order(user_id,product_id,`count`,money,status)
        values (#{userId},#{productId},#{count},#{money},0)
    </insert>
    <update id="update">
        update t_order set status=1
        where user_id = #{userId} and status=0
    </update>
</mapper>
1.4.2.6、service模块

我们除了创建当前订单模块的OrderService还需要创建AccountServiceStorageService

因为我们将通过openFeign进行远程接口调用

一、OrderService

package com.migu.springcloud.service;

import com.migu.springcloud.domain.Order;

public interface OrderService {
    void create(Order order);
}

二、OrderServiceImpl

package com.migu.springcloud.service.impl;

import com.migu.springcloud.dao.OrderDao;
import com.migu.springcloud.domain.Order;
import com.migu.springcloud.service.AccountService;
import com.migu.springcloud.service.OrderService;
import com.migu.springcloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderDao orderDao;
    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 测试一次订单交易流程,涉及多个数据库
     * 下订单 --> 减库存 --> 减余额 --> 改状态
     * @param order
     */
    @Override
    public void create(Order order) {
        log.info("开始新建订单....");
        orderDao.create(order);

        log.info("订单微服务 调用 库存微服务,做扣减......");
        storageService.decrease(order.getProductId(),order.getCount());
        log.info("执行扣减完毕......");

        log.info("订单微服务 调用 账户微服务,做付款......");
        accountService.decrease(order.getUserId(),order.getMoney());
        log.info("执行付款完毕......");

        log.info("修改订单状态.....");
        orderDao.update(order.getUserId(),0);
        log.info("订单流程结束.....");
    }
}

三、StorageService

package com.migu.springcloud.service;

import com.migu.springcloud.domain.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 {
    /**
     * 根据productID对指定产品进行减库存count操作
     */
    @PostMapping("storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId,
                          @RequestParam("count") Integer count);
}

四、AccountService

package com.migu.springcloud.service;

import com.migu.springcloud.domain.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 {
    /**
     * 根据userId对指定用户进行付款money操作
     */
    @PostMapping("account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId,
                          @RequestParam("money") BigDecimal money);
}
1.4.2.7、controller层

代码:

package com.migu.springcloud.controller;

import com.migu.springcloud.domain.CommonResult;
import com.migu.springcloud.domain.Order;
import com.migu.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,"订单OK");
    }
}
1.4.2.8、自定义数据源配置

由于我们需要采用seata做事务处理,所以数据源就需要采用seata实现:

package com.migu.springcloud.config;

import com.alibaba.druid.pool.DruidDataSource;
// 注意这边使用`seata`
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;

/**
 * 使用Seata对数据源进行代理
 */
@Configuration
public class DataSourceProxyConfig {

    @Value("${mybatis.mapperLocations}")
    private String mapperLocations;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource druidDataSource(){
        return new DruidDataSource();
    }

    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @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();
    }
}
1.4.2.9、主启动类

代码:

package com.migu.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
// 我们上方自定义了数据源,由于SpringBoot mybatis启动器有默认加载,所以对其排除
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class SeataOrderMainApp2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMainApp2001.class,args);
    }
}

这样就测试环境就搭建完毕了,其余两个模块步骤也类似

1.4.3、库存业务模块搭建


1.4.3.1、同一个套路

模块取名为:seata-storage-service2002

pom依赖:与上方一样

yaml配置:需要修改三处

  • 端口号改为2002
  • 服务名:与订单业务的service模块的StorageService的远程接口调用的服务名保持一致,所以为:seata-storage-service
  • 连接数据库表修改为seata_storage

依然需要复制file.confregistry.conf到项目中的resources目录下

自定义数据源一样,就那个config下的DataSourceProxyConfig

主启动类也是类似

其余就是业务相关的测试了

1.4.3.2、domain实体类

代码:

package com.migu.springcloud.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {

    private Long id;

    /**
     * 产品id
     */
    private Long productId;

    /**
     * 总库存
     */
    private Integer total;

    /**
     * 已用库存
     */
    private Integer used;

    /**
     * 剩余库存
     */
    private Integer residue;
}

CommonResult数据返回类,也同上方一样

1.4.3.3、dao接口配置

代码:

package com.migu.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface StorageDao {

    //扣减库存
    void decrease(@Param("productId") Long productId, @Param("count") Integer count);
}

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.migu.springcloud.dao.StorageDao">

    <resultMap id="BaseResultMap" type="com.migu.springcloud.domain.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.4.3.4、service模块

代码:

package com.migu.springcloud.service;

public interface StorageService {
    /**
     * 扣减库存
     */
    void decrease(Long productId, Integer count);
}

实现类:

  • 这边老师使用了不一样的日志进行输出信息
package com.migu.springcloud.service.impl;

import com.migu.springcloud.dao.StorageDao;
import com.migu.springcloud.service.StorageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;


@Service
public class StorageServiceImpl implements StorageService {

    private static final Logger LOGGER = LoggerFactory.getLogger(StorageServiceImpl.class);

    @Resource
    private StorageDao storageDao;

    /**
     * 扣减库存
     */
    @Override
    public void decrease(Long productId, Integer count) {
        LOGGER.info("------->storage-service中扣减库存开始");
        storageDao.decrease(productId,count);
        LOGGER.info("------->storage-service中扣减库存结束");
    }
}
1.4.3.5、controller层

代码:

  • 这些controller资源路径也就根据订单业务模块接口调用对应配置
package com.migu.springcloud.controller;

import com.migu.springcloud.domain.CommonResult;
import com.migu.springcloud.service.StorageService;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class StorageController {

    @Resource
    private StorageService storageService;

    /**
     * 扣减库存
     */
    @RequestMapping("/storage/decrease")
    public CommonResult decrease(Long productId, Integer count) {
        storageService.decrease(productId, count);
        return new CommonResult(200,"扣减库存成功!");
    }
}

1.4.4、账户业务模块搭建


不想多说啥了,这边就提供测试环境

1.4.4.1、domain实体类

代码:

package com.migu.springcloud.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private Long id;

    /**
     * 用户id
     */
    private Long userId;

    /**
     * 总额度
     */
    private BigDecimal total;

    /**
     * 已用额度
     */
    private BigDecimal used;

    /**
     * 剩余额度
     */
    private BigDecimal residue;
}

CommonResult别忘记了

1.4.4.2、dao接口配置

代码:

package com.migu.springcloud.dao;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;

import java.math.BigDecimal;

@Mapper
public interface AccountDao {

    /**
     * 扣减账户余额
     */
    void decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

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.migu.springcloud.dao.AccountDao">
    <resultMap id="BaseResultMap" type="com.migu.springcloud.domain.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.4.4.3、service模块

代码:

package com.migu.springcloud.service;

import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

public interface AccountService {

    /**
     * 扣减账户余额
     * @param userId 用户id
     * @param money 金额
     */
    void decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

实现类:

package com.migu.springcloud.service.impl;

import com.migu.springcloud.dao.AccountDao;
import com.migu.springcloud.service.AccountService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

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

/**
 */
@Service
public class AccountServiceImpl implements AccountService {

    private static final Logger LOGGER = LoggerFactory.getLogger(AccountServiceImpl.class);


    @Resource
    AccountDao accountDao;

    /**
     * 扣减账户余额
     */
    @Override
    public void decrease(Long userId, BigDecimal money) {
        LOGGER.info("------->account-service中扣减账户余额开始");
        accountDao.decrease(userId,money);
        LOGGER.info("------->account-service中扣减账户余额结束");
    }
}
1.4.4.4、controller层

代码:

package com.migu.springcloud.controller;

import com.migu.springcloud.domain.CommonResult;
import com.migu.springcloud.service.AccountService;
import org.springframework.web.bind.annotation.RequestMapping;
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;

   /**
    * 扣减账户余额
    */
   @RequestMapping("/account/decrease")
   public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money){
       accountService.decrease(userId,money);
       return new CommonResult(200,"扣减账户余额成功!");
   }
}

1.5、分布式事务测试


1.5.1、数据库当前基本情况


一、**seata_order数据库的t_order**表

SELECT * FROM t_order

在这里插入图片描述

二、**seata_storage数据库的t_storage**表

SELECT * FROM t_storage

在这里插入图片描述

三、**seata_account数据库的t_account**表

SELECT * FROM t_account;

在这里插入图片描述

感觉老师这边字段写错了好像

1.5.2、演示正常下单


正常下单 :用户ID1的账户,购买产品ID1的产品,购买十件,花了100

http://localhost:2001/order/create?userId=1&productId=1&count=10&money=100

打印:

{"code":200,"message":"订单OK","data":null}

查看数据库状态:

在这里插入图片描述

生成了一个这样的订单:userId1的用户,购买了product_id的产品,购买了10件,花费了100元,status状态为1,表示成功

库存表:

在这里插入图片描述

账户信息表:

在这里插入图片描述

查看IDEA控制台输出:

在这里插入图片描述

1.5.3、演示超时异常


在最后一步操作,AccountServiceImpl的付款操作进行,超时设置

/**
* 扣减账户余额
*/
@Override
public void decrease(Long userId, BigDecimal money) {
    LOGGER.info("------->account-service中扣减账户余额开始");
    // openFeign默认的超时时间为 1s ,如果超时了,测试是否会进行回滚
    try {
        TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    accountDao.decrease(userId,money);
    LOGGER.info("------->account-service中扣减账户余额结束");
}

再对其测试:

在这里插入图片描述

在数据进行查看时,发现库存减少了,钱也扣了

在这里插入图片描述

但是订单状态却为0

在这里插入图片描述

上面情况还是算很好,但我们需要考虑每种情况,那如何解决了,加一个@GlobalTransactional注解即可

1.5.4、异常回滚解决方案


只需在处理所有业务逻辑的总方法上,加上一个@GlobalTransactional注解

import io.seata.spring.annotation.GlobalTransactional;
/**
* 测试一次订单交易流程,涉及多个数据库
* 下订单 --> 减库存 --> 减余额 --> 改状态
* @param order
*/
@Override
// name: 唯一自定义  rollbackFor: 处理所有异常
@GlobalTransactional(name = "seata-create-order",rollbackFor = Exception.class)
public void create(Order order) {
    ...
}

此时运行测试时,我抛出了一个异常:

### Error updating database.  Cause: java.sql.SQLException: java.sql.SQLException: io.seata.common.exception.NotSupportYetException: undo_log needs to contain the primary key.
### The error may involve com.migu.springcloud.dao.OrderDao.create-Inline
### The error occurred while setting parameters
### SQL: insert into t_order(id,user_id,product_id,count,money,status)         values (null,?,?,?,?,0)
### Cause: java.sql.SQLException: java.sql.SQLException: io.seata.common.exception.NotSupportYetException: undo_log needs to contain the primary key.
; uncategorized SQLException; SQL state [null]; error code [0];

undo_log表没有主键,我确实没有主键,所以此时我为数据库的每个undo_log表都添加了id主键

同时又要为id设置为自增

此时需要重启服务,之后再测试运行,会发现,抛出了连接超时异常:

在这里插入图片描述

此时再次查看数据库,啥也没有,证明回滚成功,没有创建订单

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值