← 返回首页
SpringCloud基础教程(十一)
发表时间:2022-05-15 12:53:54
seata实现分布式事务案例

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

1.seata

seata实现分布式事务的处理过程为:一ID+三组件模型,一个ID即Transaction ID XID,全局唯一的事务ID。

三组件:

处理过程如下图:

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

2.seata-server安装

以windows版本seata为例。

1)修改registry.conf配置文档。

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

  nacos {
    application = "seata-server"
    serverAddr = "192.168.2.31:8848"
    group = "SEATA_GROUP"
    namespace = ""
    cluster = "default"
    username = "nacos"
    password = "nacos"
  }
  eureka {
    serviceUrl = "http://localhost:8761/eureka"
    application = "default"
    weight = "1"
  }
  redis {
    serverAddr = "localhost:6379"
    db = 0
    password = ""
    cluster = "default"
    timeout = 0
  }
  zk {
    cluster = "default"
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  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 = "nacos"

  nacos {
    serverAddr = "192.168.2.31:8848"
    namespace = ""
    group = "SEATA_GROUP"
    username = "nacos"
    password = "nacos"
  }
  consul {
    serverAddr = "127.0.0.1:8500"
  }
  apollo {
    appId = "seata-server"
    apolloMeta = "http://192.168.1.204:8801"
    namespace = "application"
  }
  zk {
    serverAddr = "127.0.0.1:2181"
    sessionTimeout = 6000
    connectTimeout = 2000
    username = ""
    password = ""
  }
  etcd3 {
    serverAddr = "http://localhost:2379"
  }
  file {
    name = "file.conf"
  }
}

2)启动seata-server,只需运行%SEATA_HOME%\bin\seata-server.bat 即可。

3.实例

总共有四个微服务模块,将四个微服务模块都注册到nacos server 上面。

seata-buy: 用户购买商品,所以要和seata-order,seata-stock和seata-user三个微服务交互。 seata-order: 用户购买商品的时候,将订单保存到orderform数据库 seata-stock:用户购买商品的时候,stock数据库里指定ID的商品数量减去用户购买的数量 seata-user: 用户购买商品的时候,user数据库里指定用户ID的用户减去购买所花的钱(默认所有的商品单价都是1元)。

项目结构图如下:

组件版本关系:nacos2.1.0+seata1.3.0+mysql8.0.26

建表

创建名字为:seatademo的数据库。导入以下数据。


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for orderform
-- ----------------------------
DROP TABLE IF EXISTS `orderform`;
CREATE TABLE `orderform`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `user_id` int NULL DEFAULT NULL,
  `product_id` int NULL DEFAULT NULL,
  `number` int NULL DEFAULT 0,
  `money` int NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of orderform
-- ----------------------------

-- ----------------------------
-- Table structure for stock
-- ----------------------------
DROP TABLE IF EXISTS `stock`;
CREATE TABLE `stock`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `number` int NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of stock
-- ----------------------------
INSERT INTO `stock` VALUES (1, 5);
INSERT INTO `stock` VALUES (2, 10);

-- ----------------------------
-- 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(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
  `context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci 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 INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of undo_log
-- ----------------------------

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `money` int NULL DEFAULT 0,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 10);
INSERT INTO `user` VALUES (2, 6);

SET FOREIGN_KEY_CHECKS = 1;

parent模块

父项目pom.xml文件规定依赖版本 我使用的数据库是mysql 8,其他组件版本下面都有,下面是一个父module,用于管理各组件版本,便于管理下面的子module(各微服务模块的组件版本统一比较好)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.simoniu</groupId>
    <artifactId>parent</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>seata-buy</module>
        <module>seata-order</module>
        <module>seata-stock</module>
        <module>seata-user</module>
    </modules>
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.7.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <spring-cloud-alibaba-version>2.2.7.RELEASE</spring-cloud-alibaba-version>
        <spring-cloud-version>Hoxton.SR12</spring-cloud-version>
        <mysql-connector-java-version>8.0.26</mysql-connector-java-version>
        <lombok-version>1.18.22</lombok-version>
        <mybatis-plus-boot-starter-version>3.4.2</mybatis-plus-boot-starter-version>
    </properties>

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

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${spring-cloud-alibaba-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus-boot-starter-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector-java-version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

seata-buy子项目

交易请求的接口,与下面三个微服务项目交互。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>parent</artifactId>
        <groupId>com.simoniu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata-buy</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

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

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <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.3.0</version>
        </dependency>
     </dependencies>
</project>

application.yml

server:
  port: 9000
spring:
  application:
    name: seata-buy
  cloud:
    nacos:
      # 将这个服务注册到nacos 上面去
      discovery:
        server-addr: your nacos ip:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group  # 配置事务分组
        service:
          vgroup-mapping:
            my_test_tx_group: default

seata:
  registry:
    #配置seata 的注册中心,告诉seata client 怎么去访问seata server(TC,里面运行着这个事务协调器)
    type: nacos
    nacos:
      server-addr: your nacos ip:8848  # seata-server 所在的nacos服务地址
      application: seata-server     # 服务名
      username: nacos
      password: nacos
      group: SEATA_GROUP   # seata-server 所在的分组
  config:
    type: nacos
    nacos:
      server-addr: your nacos ip:8848

BuyController

package com.simoniu.controller;

import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

@RestController
public class BuyController {

    @Autowired
    private final RestTemplate restTemplate;

    public BuyController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @PostMapping("/buy")

    public String buy(@RequestParam("userId") Integer userId,
                      @RequestParam("productId") Integer productId,
                      @RequestParam("number") Integer count) {

        // 请求参数
        MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
        queryParams.add("userId", userId.toString());
        queryParams.add("productId", productId.toString());
        queryParams.add("number", count.toString());
        queryParams.add("money", count.toString());

        // 构造请求
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("http://seata-order/orderform/create").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);

        // 构造请求
        builder = UriComponentsBuilder.fromHttpUrl("http://seata-stock/stock/deduct").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);

        // 构造请求 restTemplate 加上了@LoadBalanced注解以后,必须使用应用名来访问指定服务,而不是IP地址
        builder = UriComponentsBuilder.fromHttpUrl("http://seata-user/user/debit").queryParams(queryParams);
        restTemplate.postForObject(builder.toUriString(), null, Void.class);
        return "success";
    }
}

SeataBuyApplication


package com.simoniu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableDiscoveryClient
public class SeataBuyApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(SeataBuyApplication.class);
    }
}

seata-order子项目

交易进行时,生成用户购买指定商品的订单,然后入库,结构图:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>parent</artifactId>
        <groupId>com.simoniu</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>seata-order</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

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

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

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

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>

        <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.3.0</version>
        </dependency>
    </dependencies>
</project>

application.yml

server:
  port: 9001

spring:
  application:
    name: seata-order

  cloud:
    nacos:
      # 将这个服务注册到nacos 上面去
      discovery:
        server-addr: your nacos ip:8848
    alibaba:
      seata:
        tx-service-group: my_test_tx_group  # 配置事务分组
        service:
          vgroup-mapping:
            my_test_tx_group: default

  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://127.0.0.1:3306/seatademo?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    hikari:
      max-lifetime: 30000
#  jpa:
#    show-sql: true


# 配置日志输出 使用默认控制台打印
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

seata:
  registry:
    #配置seata 的注册中心,告诉seata client 怎么去访问seata server(TC,里面运行着这个事务协调器)
    type: nacos
    nacos:
      server-addr: your nacos ip:8848  # seata-server 所在的nacos服务地址
      application: seata-server     # 服务名
      username: nacos
      password: nacos
      group: SEATA_GROUP   # seata-server 所在的分组
  config:
    type: nacos
    nacos:
      server-addr: your nacos ip:8848

OrderForm

package com.simoniu.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@TableName("orderform")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderForm implements Serializable {
    // 订单id
    @TableId(type = IdType.AUTO)
    private Integer id;
    // 用户id
    private Integer userId;
    // 商品id
    private Integer productId;
    // 商品购买数量
    private Integer number;
    // 订单金额
    private Integer money;
}

OrderFormMapper

package com.simoniu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.simoniu.entity.OrderForm;
import org.springframework.stereotype.Repository;

@Repository
public interface OrderFormMapper extends BaseMapper<OrderForm> {
}

OrderFormService和OrderFormServiceImpl

package com.simoniu.service;

public interface OrderService {
    void create(int userId, int productId, int number, int money);
}
package com.simoniu.service.impl;

import com.simoniu.entity.OrderForm;
import com.simoniu.mapper.OrderFormMapper;
import com.simoniu.service.OrderService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

@Service
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderFormMapper orderFormMapper;
    @Override
    public void create(int userId, int productId, int number, int money) {
        // 生成订单
        OrderForm order = new OrderForm();
        order.setUserId(userId);
        order.setProductId(productId);
        order.setNumber(number);
        order.setMoney(money);
        System.out.println(order.toString());
        orderFormMapper.insert(order);
    }
}

OrderFormController

package com.simoniu.controller;

import com.simoniu.service.OrderService;
import org.springframework.web.bind.annotation.PostMapping;
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;

@RestController
@RequestMapping("/orderform")
public class OrderFormController {

    @Resource
    private OrderService orderService;

    @PostMapping("/create")

    public void create(@RequestParam("userId") Integer userId,
                       @RequestParam("productId") Integer productId,
                       @RequestParam("number") Integer count,
                       @RequestParam("money") Integer money) {
        orderService.create(userId, productId, count, money);
    }
}

SeataOrderFormApplication

package com.simoniu;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.simoniu.mapper")
public class SeataOrderFormApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeataOrderFormApplication.class);
    }
}

seata-stock子项目 交易进行时,扣除指定商品的库存数量,项目结构图:

pom.xml 和上一个子项目的依赖都一样,我就不贴了

application.yml 也和上一个差不多,改一下服务端口号和应用名称就行。

server:
  port: 9002

spring:
  application:
    name: seata-stoc

Stock

package com.simoniu.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@TableName("stock")
@Data
public class Stock implements Serializable {
    // 商品id
    private Integer id;
    // 库存
    private Integer number;
}

StockMapper

package com.simoniu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.simoniu.entity.Stock;
import org.springframework.stereotype.Repository;

@Repository
public interface StockMapper extends BaseMapper<Stock> {

}

StockService和StockServiceImpl

package com.simoniu.service;

public interface StockService {
    void deduct(int productId, int number);
}
package com.simoniu.service.impl;

import com.simoniu.entity.Stock;
import com.simoniu.mapper.StockMapper;
import com.simoniu.service.StockService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;

@Service
public class StockServiceImpl implements StockService {
    @Resource
    private StockMapper stockMapper;

    @Override
    public void deduct(int productId, int number) {
        Optional<Stock> byId = Optional.ofNullable(stockMapper.selectById(productId));

        if(byId.isPresent()) {
            Stock storage = byId.get();
            if(storage.getNumber() >= number) {
                // 减库存
                storage.setNumber(storage.getNumber() - number);
                //stockMapper.insert(storage);
                stockMapper.updateById(storage);
            }
            else {
                throw new RuntimeException("该商品库存不足!");
            }
        }
        else {
            throw new RuntimeException("该商品不存在!");
        }
    }
}

StockController

package com.simoniu.controller;

import com.simoniu.service.StockService;
import org.springframework.web.bind.annotation.PostMapping;
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;

@RestController
@RequestMapping("/stock")
public class StockController {

    @Resource
    private StockService storageService;

    @PostMapping("/deduct")
    public void deduct(@RequestParam("productId") Integer productId,
                       @RequestParam("number") Integer count) {
        storageService.deduct(productId, count);
    }
}

SeataStockApplication

package com.simoniu;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.simoniu.mapper")
public class SeataStockApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeataStockApplication.class);
    }
}

seata-user子项目 交易进行时,扣除指定用户账户里面的钱。项目结构图:

pom.xml文件和上一子项目依赖都一样

application.yml 和上一子项目也差不多,就改一下服务端口号和应用名。

server:
  port: 9003

spring:
  application:
    name: seata-user

User

package com.simoniu.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
@TableName("user")
@Data
public class User implements Serializable {
    // 用户id
    private Integer id;
    // 用户余额
    private Integer money;
}

UserMapper

package com.simoniu.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.simoniu.entity.User;
import org.springframework.stereotype.Repository;

@Repository
public interface UserMapper extends BaseMapper<User> {

}

UserService和UserServiceImpl

package com.simoniu.service;

public interface UserService {
    void debit(int userId, int money);
}
package com.simoniu.service.impl;

import com.simoniu.entity.User;
import com.simoniu.mapper.UserMapper;
import com.simoniu.service.UserService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Optional;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public void debit(int userId, int money) {
        Optional<User> byId = Optional.ofNullable(userMapper.selectById(userId));
        if(byId.isPresent()) {
            User user = byId.get();
            if(user.getMoney() >= money) {
                // 减余额
                user.setMoney(user.getMoney() - money);
                //userMapper.insert(user);
                userMapper.updateById(user);
            }
            else {
                throw new RuntimeException("该用户余额不足!");
            }
        }
        else {
            throw new RuntimeException("没有该用户!");
        }
    }
}

UserController

package com.simoniu.controller;

import com.simoniu.service.UserService;
import org.springframework.web.bind.annotation.PostMapping;
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;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @PostMapping("/debit")
    public void debit(@RequestParam("userId") Integer userId,
                      @RequestParam("money") Integer money) {
        userService.debit(userId, money);
    }

}

SeataUserApplication

package com.simoniu;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.simoniu.mapper")
public class SeataUserApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeataUserApplication.class);
    }
}

在nacos服务器添加配置:

选择:配置管理->配置列表->新建。

测试(不使用分布式事务)

依次启动seata-buy->seata-order->seata-stock->seata-user四个项目。在nacos server观察四个微服务已经注册成功。如下图:

使用postman测试2号用户购买6件1号商品。测试接口如下:

我们发现seata-stock微服务抛出异常,而但是seata-order微服务却正常运行。因为用户购买的数量超过了库存数量,因此seata-stock微服务肯定会抛出异常,但是由于没有分布式事务,操作数据不一致。出现以下情况。

测试(使用分布式事务)

在BuyController的buy方法上添加@GlobalTransactional。以@GlobalTransactional为入口,GlobalTransactionalInterceptor为切入点,TM会向TC发起一个请求(服务端使用的netty)开启一个全局事务,生成全局事务的XID,通过服务调用链路传播,测试分布式事务。

再次测试,使用postman测试2号用户购买6件1号商品。测试接口如下:

我们发现seata-stock微服务因库存不足抛出异常,而eata-order微服务本来执行了sql 语句插入订单数据,后来还是回滚了,因为seata-stock微服务未执行成功,抛出了异常。

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d09ebb9] was not registered for synchronization because synchronization is not active
JDBC Connection [io.seata.rm.datasource.ConnectionProxy@55286307] will not be managed by Spring
==>  Preparing: INSERT INTO orderform ( user_id, product_id, number, money ) VALUES ( ?, ?, ?, ? )
==> Parameters: 2(Integer), 1(Integer), 6(Integer), 6(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d09ebb9]
2022-05-16 20:52:31.584  INFO 9028 --- [h_RMROLE_1_1_16] i.s.c.r.p.c.RmBranchRollbackProcessor    : rm handle branch rollback process:xid=203.6.234.202:8091:269569373063090176,branchId=269569376791826433,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/seatademo,applicationData=null

由于使用了分布式事务,不会破坏数据的一致性。回滚后数据如下: