0
点赞
收藏
分享

微信扫一扫

Seata分布式事务组件案例演示

夕阳孤草 2023-03-31 阅读 38

一、案例演示:业务系统集成 Seata 

以电商系统为例,来演示下业务系统是如何集成 Seata 的。在电商系统中,用户下单购买一件商品,需要以下 3 个服务提供支持:

  1. Order(订单服务):创建和修改订单。
  2. Storage(库存服务):对指定的商品扣除仓库库存。
  3. Account(账户服务) :从用户帐户中扣除商品金额。

这三个微服务分别使用三个不同的数据库,架构图:

Seata分布式事务组件案例演示_spring

当用户从这个电商网站购买了一件商品后,其服务调用步骤如下:

  1. 调用 Order 服务,创建一条订单数据,订单状态为“未完成”;
  2. 调用 Storage 服务,扣减商品库存;
  3. 调用 Account 服务,从用户账户中扣除商品金额;
  4. 调用 Order 服务,将订单状态修改为“已完成”。

二、库存服务

1. 在 MySQL 数据库中,新建一个名为 seata-storage 的数据库实例

通过SQL 语句创建 2 张表:t_storage(库存表)和 undo_log(回滚日志表),代码:

-- ----------------------------
-- Table structure for t_storage
-- ----------------------------
DROP TABLE IF EXISTS `t_storage`;
CREATE TABLE `t_storage` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `total` int DEFAULT NULL COMMENT '总库存',
  `used` int DEFAULT NULL COMMENT '已用库存',
  `residue` int DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_storage
-- ----------------------------
INSERT INTO `t_storage` VALUES ('1', '1', '100', '0', '100');

-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 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 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';

2. 创建一个名为 cloud-alibaba-seata-storage-8006 的 Spring Boot 模块

在其 pom.xml 中添加以下依赖,内容如下:

<dependencies>
        <!--nacos 服务注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--引入 OpenFeign 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <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.6.1</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.augus.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--添加 Spring Boot 的监控模块-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--配置中心 nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>

3. 在 cloud-alibaba-seata-storage-8006 的类路径(/resources 目录)下

创建一个配置文件 application.yml,配置内容如下:

#端口
server:
  port: 8006

spring:
  application:
    name: cloud-alibabab-seata-storage-8006
  cloud:
    # 指定 nacos
    nacos:
      discovery:
        server-addr: localhost:8848 # nacos 服务器地址
        username: nacos
        password: nacos
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080  #Sentinel 控制台地址
        port: 8719
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata-storage?rewriteBatchedStatements=true&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

#开启 OpenFeign 功能
feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

seata:
  tx-service-group: fsp_tx_group  #这里每个服务都是对应不同的映射名,在配置中心可以看到
  # 注册方式为nacos
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  service:
    vgroup-mapping:
      #这里也要注意 key为映射名,事务分组
      fsp_tx_group: default

###################################### MyBatis 配置 ######################################
mybatis:
  # 指定 mapper.xml 的位置
  mapper-locations: classpath:mapper/*.xml
  #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
  type-aliases-package: com.augus.cloud.pojo
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true

4. 创建主启动类

主启动类:CloudAlibabaSeataStorage8006Application 代码如下:

package com.augus.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient //让注册中心能够发
@EnableFeignClients //启用openFeign客户端
public class CloudAlibabaSeataStorage8006Application {
    public static void main(String[] args) {
        SpringApplication.run(CloudAlibabaSeataStorage8006Application.class, args);
    }
}

5. 在 com.augus.cloud.pojo包下,创建一个名为 Storage 的实体类

对应之前创建的表storage

package com.augus.cloud.pojo;

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Storage {

    private Long id;

    private Long productId;

    private Integer total;

    private Integer used;

    private Integer residue;
}

6. 在 com.augus.cloud.dao包下,创建一个名为 StorageMapper 的接口

在其中创建 减扣库存的服务:

package com.augus.cloud.dao;

import com.augus.cloud.pojo.Storage;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface StorageMapper {

    Storage selectByProductId(Long productId);

    int decrease(Storage record);
}

7. 在 /resouces 目录下创建mapper 目录

在其中创建一个名为 StorageMapper.xml 的 MyBatis 映射文件,代码如下:

<?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.augus.cloud.dao.StorageMapper">
    <resultMap id="BaseResultMap" type="com.augus.cloud.pojo.Storage">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="product_id" jdbcType="BIGINT" property="productId"/>
        <result column="total" jdbcType="INTEGER" property="total"/>
        <result column="used" jdbcType="INTEGER" property="used"/>
        <result column="residue" jdbcType="INTEGER" property="residue"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , product_id, total, used, residue
    </sql>
    <update id="decrease" parameterType="com.augus.cloud.pojo.Storage">
        update t_storage
        <set>
            <if test="total != null">
                total = #{total,jdbcType=INTEGER},
            </if>
            <if test="used != null">
                used = #{used,jdbcType=INTEGER},
            </if>
            <if test="residue != null">
                residue = #{residue,jdbcType=INTEGER},
            </if>
        </set>
        where product_id = #{productId,jdbcType=BIGINT}
    </update>
    <select id="selectByProductId" parameterType="java.lang.Long" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from t_storage
        where product_id = #{productId,jdbcType=BIGINT}
    </select>
</mapper>

8. 在 com.augus.cloud.service 包下,创建一个名为 StorageService 的 Service 接口

实现扣减库存,代码如下:

package com.augus.cloud.service;

public interface StorageService {
    
    int decrease(Long productId, Integer count);
}

9. 在 com.augus.cloud.service.impl 包下创建 StorageService 的实现类 StorageServiceImpl,代码如下:

package com.augus.cloud.service.impl;

import com.augus.cloud.dao.StorageMapper;
import com.augus.cloud.pojo.Storage;
import com.augus.cloud.service.StorageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class StorageServiceImpl implements StorageService {

    @Resource
    StorageMapper storageMapper;

    @Override
    public int decrease(Long productId, Integer count) {
        log.info("------->storage-service中扣减库存开始");
        log.info("------->storage-service 开始查询商品是否存在");
        Storage storage = storageMapper.selectByProductId(productId);
        if (storage != null && storage.getResidue() >= count) {
            Storage storage2 = new Storage();
            storage2.setProductId(productId);
            storage.setUsed(storage.getUsed() + count);
            storage.setResidue(storage.getTotal() - storage.getUsed());
            int decrease = storageMapper.decrease(storage);
            log.info("------->storage-service 扣减库存成功");
            return decrease;
        } else {
            log.info("------->storage-service 库存不足,开始回滚!");
            throw new RuntimeException("库存不足,扣减库存失败!");
        }
    }
}

10. 在 com.augus.cloud.controller 包下,创建一个名为 StorageController 的 Controller 类,代码如下。

package com.augus.cloud.controller;

import com.augus.cloud.cloud.pojo.CommonResult;
import com.augus.cloud.service.StorageService;
import org.springframework.beans.factory.annotation.Value;
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
    private StorageService storageService;

    @Value("${server.port}")
    private String serverPort;

    @PostMapping(value = "/storage/decrease")
    public CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count) {
        int decrease = storageService.decrease(productId, count);
        CommonResult result;
        if (decrease > 0) {
            result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, decrease);
        } else {
            result = new CommonResult(505, "from mysql,serverPort:  " + serverPort, "库存扣减失败");
        }
        return result;
    }
}

三、账户服务

1. 在 MySQL 数据库中,新建 seata-account 的数据库,通过SQL 语句创建 2 张表:t_account(账户表)和 undo_log(回滚日志表)

DROP TABLE IF EXISTS `t_account`;
CREATE TABLE `t_account` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` bigint 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 '剩余可用额度',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of t_account
-- ----------------------------
INSERT INTO `t_account` VALUES ('1', '1', '1000', '0', '1000');

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 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 DEFAULT CHARSET=utf8;

2. 创建一个名为 cloud-alibaba-seata-account-8007 的 Spring Boot 模块,并在其 pom.xml 中添加以下依赖

<dependencies>
        <!--nacos 服务注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--引入 OpenFeign 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <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.6.1</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.augus.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--添加 Spring Boot 的监控模块-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--配置中心 nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>

3. 在 cloud-alibaba-seata-account-8007 的resources 目录下,创建一个配置文件 application.yml,配置内容如下

#端口
server:
  port: 8007

spring:
  application:
    name: cloud-alibaba-seata-account-8007 #服务名
  cloud:
    # 指定 nacos
    nacos:
      discovery:
        server-addr: localhost:8848 # nacos 服务器地址
        username: nacos
        password: nacos
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080  #Sentinel 控制台地址
        port: 8719
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata-account?rewriteBatchedStatements=true&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

#开启 OpenFeign 功能
feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

seata:
  tx-service-group: fsp_tx_group  #这里每个服务都是对应不同的映射名,在配置中心可以看到
  # 注册方式为nacos
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  service:
    vgroup-mapping:
      #这里也要注意 key为映射名,事务分组
      fsp_tx_group: default

###################################### MyBatis 配置 ######################################
mybatis:
  # 指定 mapper.xml 的位置
  mapper-locations: classpath:mapper/*.xml
  #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
  type-aliases-package: com.augus.cloud.pojo
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true

4. 创建主启动类

名为:CloudAlibabaSeataAccount8007Application

package com.augus.cloud;

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

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

5. 在 cloud.augus.cloud.pojo包下,创建一个名为 Account 的实体类,代码如下。

package com.augus.cloud.pojo;

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

import java.math.BigDecimal;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {

    private Long id;

    private Long userId;

    private BigDecimal total;

    private BigDecimal used;

    private BigDecimal residue;
}

6. 在 com.augus.cloud.dao包下,创建一个名为 AccountMapper 的接口,代码如下。

package com.augus.cloud.dao;

import com.augus.cloud.pojo.Account;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface AccountMapper {

    Account selectByUserId(Long userId);

    int decrease(@Param("userId") Long userId, @Param("money") BigDecimal money);
}

7. 在 resouces下创建mapper 目录,载里面创建一个名为 AccountMapper.xml 的 MyBatis 映射文件,代码如下:

<?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.augus.cloud.dao.AccountMapper">
    <resultMap id="BaseResultMap" type="com.augus.cloud.pojo.Account">
        <id column="id" jdbcType="BIGINT" property="id"/>
        <result column="user_id" jdbcType="BIGINT" property="userId"/>
        <result column="total" jdbcType="DECIMAL" property="total"/>
        <result column="used" jdbcType="DECIMAL" property="used"/>
        <result column="residue" jdbcType="DECIMAL" property="residue"/>
    </resultMap>
    <sql id="Base_Column_List">
        id
        , user_id, total, used, residue
    </sql>
    <select id="selectByUserId" resultType="com.augus.cloud.pojo.Account">
        select
        <include refid="Base_Column_List"/>
        from t_account
        where user_id = #{userId,jdbcType=BIGINT}
    </select>
    <update id="decrease">
        UPDATE t_account
        SET residue = residue - #{money},
            used    = used + #{money}
        WHERE user_id = #{userId};
    </update>
</mapper>

8. 在 com.augus.cloud.service 包下,创建一个名为 AccountService 的接口,代码如下:

package com.augus.cloud.service;

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

import java.math.BigDecimal;

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

9. 在 com.augus.cloud.service.impl 包下,创建 AccountService 的实现类 AccountServiceImpl,代码如下:

package com.augus.cloud.service.impl;

import com.augus.cloud.dao.AccountMapper;
import com.augus.cloud.pojo.Account;
import com.augus.cloud.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 implements AccountService {
    @Resource
    AccountMapper accountMapper;

    @Override
    public int decrease(Long userId, BigDecimal money) {
        log.info("------->account-service 开始查询账户余额");
        Account account = accountMapper.selectByUserId(userId);
        log.info("------->account-service 账户余额查询完成," + account);
        if (account != null && account.getResidue().intValue() >= money.intValue()) {
            log.info("------->account-service 开始从账户余额中扣钱!");
            int decrease = accountMapper.decrease(userId, money);
            log.info("------->account-service 从账户余额中扣钱完成");
            return decrease;
        } else {
            log.info("账户余额不足,开始回滚!");
            throw new RuntimeException("账户余额不足!");
        }
    }
}

10. 在 com.augus.cloud.controller 包下,创建一个名为AccountController的 Controller 类,代码如下。

package com.augus.cloud.controller;

import com.augus.cloud.cloud.pojo.CommonResult;
import com.augus.cloud.service.AccountService;
import org.springframework.beans.factory.annotation.Value;
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
    private AccountService accountService;

    @Value("${server.port}")
    private String serverPort;

    @PostMapping(value = "/account/decrease")
    public CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money) throws InterruptedException {
        //保持返回值
        CommonResult result;//完成扣款操作
        int decrease = accountService.decrease(userId, money);

        if (decrease > 0) {
            result = new CommonResult(200, "from mysql,serverPort:  " + serverPort, decrease);
        } else {
            result = new CommonResult(505, "from mysql,serverPort:  " + serverPort, "金额扣减失败");
        }
        return result;
    };
}

四、创建订单(Order)服务

1. 在 MySQL 数据库中,新建数据库  seata-order ,在其中创建 2 张表:t_order(订单表)和 undo_log(回滚日志表)

直接创建数据库后,执行下面SQL即可

-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` bigint DEFAULT NULL COMMENT '用户id',
  `product_id` bigint DEFAULT NULL COMMENT '产品id',
  `count` int DEFAULT NULL COMMENT '数量',
  `money` decimal(11,0) DEFAULT NULL COMMENT '金额',
  `status` int DEFAULT NULL COMMENT '订单状态:0:未完成;1:已完结',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for undo_log
-- ----------------------------
DROP TABLE IF EXISTS `undo_log`;
CREATE TABLE `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 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 DEFAULT CHARSET=utf8 ;

2. 创建一个名为 cloud-alibaba-seata-order-8005 的 Spring Boot 模块

并在其 pom.xml 中添加以下依赖,内容如下:

<dependencies>
        <!--nacos 服务注册中心-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--引入 OpenFeign 的依赖-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!--seata-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <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.6.1</version>
        </dependency>


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.augus.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${project.version}</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--添加 Spring Boot 的监控模块-->
        <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--SpringCloud ailibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--配置中心 nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
    </dependencies>

3. 在 spring-cloud-alibaba-seata-order-8005 的类路径(/resources 目录)下,创建一个配置文件 application.yml,

这里一定注意事务群组的名字需要和nacos配置中心创建的seata一致,如下:

#端口
server:
  port: 8005

spring:
  application:
    name: cloud-alibabab-seata-order-8005
  cloud:
    # 指定 nacos
    nacos:
      discovery:
        server-addr: localhost:8848 # nacos 服务器地址
    sentinel:
      transport:
        dashboard: 127.0.0.1:8080  #Sentinel 控制台地址
        port: 8719
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/seata-order?rewriteBatchedStatements=true&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

#开启 OpenFeign 功能
feign:
  hystrix:
    enabled: false

logging:
  level:
    io:
      seata: info

seata:
  registry:
    type: nacos
    nacos:
      server-addr: localhost:8848
      group: SEATA_GROUP
  #tx-service-group: default_tx_group  #这里每个服务都是对应不同的映射名,在配置中心可以看到
  service:
    vgroup-mapping:
      default_tx_group: default #TC 集群(必须和seata-server保持一致)
  # 事务群组(可以每个应用独立名字,也可以单独的名字,注意跟配置文件中的保持一致)
  tx-service-group: default_tx_group


###################################### MyBatis 配置 ######################################
mybatis:
  # 指定 mapper.xml 的位置
  mapper-locations: classpath:mapper/*.xml
  #扫描实体类的位置,在此处指明扫描实体类的包,在 mapper.xml 中就可以不写实体类的全路径名
  type-aliases-package: com.augus.cloud.pojo
  configuration:
    #默认开启驼峰命名法,可以不用设置该属性
    map-underscore-to-camel-case: true

4. 在 com.augus.cloud包下,创建一个名为主启动类

创建名为:CloudAlibabaSeataOrder8005Application 的主启动类

package com.augus.cloud;

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

@SpringBootApplication
@EnableDiscoveryClient //让注册中心能够发
@EnableFeignClients //启用openFeign客户端
public class CloudAlibabaSeataOrder8005Application {
    public static void main(String[] args) {
        SpringApplication.run(CloudAlibabaSeataOrder8005Application.class, args);
    }
}

5. 在 com.augus.cloud.pojo包下,创建一个名为 Order 的实体类,

这个实体类对应之前创建的order表,代码如下:

package com.augus.cloud.pojo;

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

6. 在 com.augus.cloud.dao包下,创建一个名为 OrderMapper 的 Mapper 接口,代码如下。

这里定义两个接口:创建订单、修改订单状态:

package com.augus.cloud.dao;

import com.augus.cloud.pojo.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface OrderMapper {

    /**
     * 1.创建订单
     * @param order
     * @return
     */
    int create(Order order);

    //2 修改订单状态,从零改为1
    void update(@Param("userId") Long userId, @Param("status") Integer status);

}

7. 在 /resouces/目录下创建/mapper 目录

创建一个名为 OrderMapper.xml 的 MyBatis 映射文件,代码如下:

<?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.augus.cloud.dao.OrderMapper">
    <insert id="create" parameterType="com.augus.cloud.pojo.Order">
        insert into t_order (user_id, product_id,
                             count, money, status)
        values (#{userId,jdbcType=BIGINT}, #{productId,jdbcType=BIGINT},
                #{count,jdbcType=INTEGER}, #{money,jdbcType=DECIMAL}, #{status,jdbcType=INTEGER})
    </insert>

    <update id="update">
        update t_order
        set status = 1
        where user_id = #{userId}
          and status = #{status};
    </update>
</mapper>

8. 在 com.augus.cloud.service 包下,创建一个名为 OrderService 的 Service 接口,代码如下。

这个接口是用来完成创建订单的,减库存、减账户余额,这个操作的

package com.augus.cloud.service;

import com.augus.cloud.cloud.pojo.CommonResult;
import com.augus.cloud.pojo.Order;


public interface OrderService {
    /**
     * 创建订单数据
     * @param order
     */
    CommonResult create(Order order);
}

9. 在 com.augus.cloud.service 包下,创建一个名为 StorageService 的接口。

这里使用的是openFegin去调用8006库存服务,进行库存的减扣

package com.augus.cloud.service;

import com.augus.cloud.cloud.pojo.CommonResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "cloud-alibabab-seata-storage-8006") //指定调用的服务
public interface StorageService {

    @PostMapping(value = "/storage/decrease")
    CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

10. 在 com.augus.cloud.service 包下,创建一个名为 AccountService 的接口

这里使用的是openFegin去调用8007账户服务,进行余额的减扣:

package com.augus.cloud.service;

import com.augus.cloud.cloud.pojo.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 = "cloud-alibaba-seata-account-8007")
public interface AccountService {
    @PostMapping(value = "/account/decrease")
    CommonResult decrease(@RequestParam("userId") Long userId, @RequestParam("money") BigDecimal money);
}

11. 在 com.augus.cloud.service.impl 包下,创建 OrderService 接口的实现类 OrderServiceImpl

在这个实现类中就要完成创建订单、减库存、减金额、修改状态的操作

package com.augus.cloud.service.impl;

import com.augus.cloud.cloud.pojo.CommonResult;
import com.augus.cloud.dao.OrderMapper;
import com.augus.cloud.pojo.Order;
import com.augus.cloud.service.AccountService;
import com.augus.cloud.service.OrderService;
import com.augus.cloud.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 OrderMapper orderMapper;

    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改订单状态
     */
    @Override
    public CommonResult create(Order order) {
        log.info("----->开始新建订单");
        //1 新建订单
        order.setUserId(new Long(1));
        order.setStatus(0);
        orderMapper.create(order);
        //2 扣减库存
        log.info("----->订单服务开始调用库存服务,开始扣减库存");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->订单微服务开始调用库存,扣减库存结束");
        //3 扣减账户
        log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->订单微服务开始调用账户,账户扣减商品金额结束");
        //4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderMapper.update(order.getUserId(), 0);
        log.info("----->修改订单状态结束");
        log.info("----->下订单结束了------->");
        return new CommonResult(200, "订单创建成功");
    }
}

12. 在 com.augus.cloud.controller 包下,创建一个名为 OrderController 的 Controller

代码如下:

package com.augus.cloud.controller;

import com.augus.cloud.cloud.pojo.CommonResult;
import com.augus.cloud.pojo.Order;
import com.augus.cloud.service.OrderService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

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

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

    @GetMapping(value = "/order/create/{productId}/{count}/{money}")
    public CommonResult create(@PathVariable("productId") Integer productId, @PathVariable("count") Integer count
            , @PathVariable("money") BigDecimal money) {
        Order order = new Order();
        order.setProductId(Integer.valueOf(productId).longValue());
        order.setCount(count);
        order.setMoney(money);
        return orderService.create(order);
    }
}

五、测试

5.1.环境说明

订单表:刚开始为空

Seata分布式事务组件案例演示_spring_02

库存表:目前库存100件,剩余100件商品,卖出0件

Seata分布式事务组件案例演示_Storage_03

 账户: 1号用户,账户总计1000元,余额为1000,消费0

Seata分布式事务组件案例演示_Storage_04

然后依次启动 Nacos Server、sentinel和Seata Server,依次启动 cloud-alibaba-seata-storage-8006、cloud-alibaba-seata-account-8007、cloud-alibaba-seata-order-8005、

5.2.测试

模拟 实现1号用户,买了2个产品、花了20块

5.2.1.在浏览器访问:http://localhost:8005/order/create/1/2/20

Seata分布式事务组件案例演示_Storage_05

5.2.2.查看订单表中变化

会发现创建了一条订单:

Seata分布式事务组件案例演示_spring_06

5.2.3.查看库存表中变化

会发现库存减少了两个:

Seata分布式事务组件案例演示_spring_07

5.2.4.查看金额表中变化

会发现金额减少了20:

Seata分布式事务组件案例演示_spring_08

5.3.模拟异常情况

5.3.1.在账户服务AccountController 中添加线程等待模拟超时

如下图所示:

Seata分布式事务组件案例演示_Storage_09

 代码如下:

//模拟超时 2000S
TimeUnit.SECONDS.sleep(2000);

5.3.2.重启8006、8007和8005服务

然后 浏览器访问 http://localhost:8005/order/create/1/3/30,模拟 1号用户,买了3个产品、花了30块钱,如下图8007出现了超时异常

Seata分布式事务组件案例演示_spring_10

5.3.3.查看订单表中变化

会发现创建了一条订单,但是订单状态依然为0,并没有改变:

Seata分布式事务组件案例演示_Storage_11

5.3.4.查看库存表中变化

会发现库存减了3

 

Seata分布式事务组件案例演示_Storage_12

5.3.5.查看账户表中变化

会发现账户并没有减少

Seata分布式事务组件案例演示_Storage_13

通过上面就发现,订单生成了但是状态依然是未支付,账户的金额没有变化,但是库存缺减少了,这种情况下就要求订单服务、账户服务、库存服务绑定到一起,要不全部成功,要么都不成功,如果其中一个出现了异常,就几个服务都进行回滚操作。

5.4.@GlobalTransactional 注解

在分布式微服务架构中,我们可以使用 Seata 提供的 @GlobalTransactional 注解实现分布式事务的开启、管理和控制。当调用 @GlobalTransaction 注解的方法时,TM 会先向 TC 注册全局事务,TC 生成一个全局唯一的 XID,返回给 TM。
@GlobalTransactional 注解既可以在类上使用,也可以在类方法上使用,该注解的使用位置决定了全局事务的范围,具体关系如下:

  • 在类中某个方法使用时,全局事务的范围就是该方法以及它所涉及的所有服务。
  • 在类上使用时,全局事务的范围就是这个类中的所有方法以及这些方法涉及的服务。

5.4.1.只需要在 cloud-alibaba-seata-order-8005 的 com.augus.cloud.service.impi 包下的 OrderServiceImpl中,创建订单的方法添加@GlobalTransactional 注解即可,代码如下。

需要注意的@GlobalTransactional 注解name属性的值随便写,只要唯一即可

package com.augus.cloud.service.impl;

import com.augus.cloud.cloud.pojo.CommonResult;
import com.augus.cloud.dao.OrderMapper;
import com.augus.cloud.pojo.Order;
import com.augus.cloud.service.AccountService;
import com.augus.cloud.service.OrderService;
import com.augus.cloud.service.StorageService;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderMapper orderMapper;

    @Resource
    private StorageService storageService;
    @Resource
    private AccountService accountService;

    /**
     * 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
     * 简单说:下订单->扣库存->减余额->改订单状态
     */
    @Override
    @GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class)
    public CommonResult create(Order order) {
        log.info("----->开始新建订单");
        //1 新建订单
        order.setUserId(new Long(1));
        order.setStatus(0);
        orderMapper.create(order);
        //2 扣减库存
        log.info("----->订单服务开始调用库存服务,开始扣减库存");
        storageService.decrease(order.getProductId(), order.getCount());
        log.info("----->订单微服务开始调用库存,扣减库存结束");
        //3 扣减账户
        log.info("----->订单服务开始调用账户服务,开始从账户扣减商品金额");
        accountService.decrease(order.getUserId(), order.getMoney());
        log.info("----->订单微服务开始调用账户,账户扣减商品金额结束");
        //4 修改订单状态,从零到1,1代表已经完成
        log.info("----->修改订单状态开始");
        orderMapper.update(order.getUserId(), 0);
        log.info("----->修改订单状态结束");
        log.info("----->下订单结束了------->");
        return new CommonResult(200, "订单创建成功");
    }
}

5.4.2.依次重启服务8006、8007和8005

待重启完成后,浏览器再次访问http://localhost:8005/order/create/1/3/30

  • 查看订单表,会发现没有再次生成订单

Seata分布式事务组件案例演示_Storage_14

  • 查看库存表,会发现库存并没有减少

Seata分布式事务组件案例演示_bc_15

  • 查看账户表,会发现账户消费金额并没有变化

Seata分布式事务组件案例演示_Storage_16



举报

相关推荐

0 条评论