【SpringCloud】Seata分布式事务 【十三】

1. 概述

Seata主要由三个重要组件组成:

TC:Transaction Coordinator 事务协调器,管理全局和分支事务的状态,用于全局性事务的提交和回滚。

TM:Transaction Manager 事务管理器,用于开启、提交或者回滚全局事务。

RM:Resource Manager 资源管理器,用于分支事务上的资源管理,向TC注册分支事务,上报分支事务的状态,接受TC的命令来提交或者回滚分支事务。

在这里插入图片描述

Seata的执行流程如下:

  1. A服务的TM向TC申请开启一个全局事务,TC就会创建一个全局事务并返回一个唯一的XID

  2. A服务的RM向TC注册分支事务,并将其纳入XID对应全局事务的管辖

  3. A服务执行分支事务,向数据库做操作

  4. A服务开始远程调用B服务,此时XID会在微服务的调用链上传播

  5. B服务的RM向TC注册分支事务,并将其纳入XID对应的全局事务的管辖

  6. B服务执行分支事务,向数据库做操作

  7. 全局事务调用链处理完毕,TM根据有无异常向TC发起全局事务的提交或者回滚

  8. TC协调其管辖之下的所有分支事务, 决定是否回滚

2. 环境准备

2.1 数据库操作

2.1.1 seata-server数据库表

GitHub建表sql:点击查看

CREATE DATABASE seata_server;
use seata_server;

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;

2.1.2 新建业务数据库


/* ============ seata_account ============= */
CREATE DATABASE seata_account;
use seata_account;

DROP TABLE IF EXISTS `account`;
CREATE TABLE `account`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `balance` decimal(15, 2) COMMENT '余额' ,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

INSERT INTO `account` VALUES (1, 100.00);
/* ============ seata_account ============= */


/* ============ seata_order ============= */
CREATE DATABASE seata_order;
use seata_order;

DROP TABLE IF EXISTS `order`;
CREATE TABLE `order`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_name` varchar(255) COMMENT '商品名称',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

INSERT INTO `order` VALUES (1, '手机');
/* ============ seata_order ============= */



/* ============ seata_storage ============= */
CREATE DATABASE seata_storage;
use seata_storage;

DROP TABLE IF EXISTS `storage`;
CREATE TABLE `storage`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `stocks` int(255) NULL DEFAULT NULL COMMENT '剩余库存',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;

INSERT INTO `storage` VALUES (1, 10);
/* ============ seata_storage ============= */

2.1.3 在每个业务数据库添加回滚日志表undo_log

建表sql:http://seata.io/zh-cn/docs/dev/mode/at-mode.html最下面

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

2.1.4 数据库最终结构

在这里插入图片描述

2.2 seata-server集群(docker方式)

2.2.1 下载运行镜像
# 下载seata-server镜像
docker pull seataio/seata-server:1.3.0

# 运行镜像
# -p 这里宿主端口和容器端口都要改,否则会按8091去访问,集群可能会出问题
# -h 宿主IP地址,不用用127.0.0.1
# -e SERVER_NODE 在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突
# -e SEATA_IP 跟 -h保持一致即可
# -e SEATA_PORT跟 -p保持一致即可
docker run \
	--name c_seata1 \
	-d \
	-p 18091:18091 \
	-h 192.168.23.131 \
	-e SERVER_NODE=1 \
	-e SEATA_IP=192.168.23.131 \
	-e SEATA_PORT=18091 \
	seataio/seata-server:1.3.0



# 进入容器,因为1.3.0版本没有bash,所以这里用sh代替
docker exec -it c_seata1 /bin/sh

#sh有自带的vi命令,而bash需要自己下载vim命令
#apt-get update
#apt-get install -y vim
2.2.2 修改resources/file.conf
vi resources/file.conf
#vim resources/file.conf

在这里插入图片描述

在这里插入图片描述

2.2.3 修改resources/registry.conf
vi resources/registry.conf
#vim resources/registry.conf

在这里插入图片描述

# 退出镜像
exit
# 重启镜像
docker restart c_seata1
2.2.4 设置第二个seata-server
# 运行镜像
docker run \
	--name c_seata2 \
	-d \
	-p 28091:28091 \
	-h 192.168.23.131 \
	-e SERVER_NODE=2 \
	-e SEATA_IP=192.168.23.131 \
	-e SEATA_PORT=28091 \
	seataio/seata-server:1.3.0


# 进入容器,因为1.3.0版本没有bash,所以这里用sh代替
docker exec -it c_seata2 /bin/sh

剩余操作跟上面的第一个seata-server一样

2.2.5 查看Nacos控制台服务列表

在这里插入图片描述

2.3 seata-server集群(Linux方式)

file.conf和registry.conf文件的修改跟2.2一样的

下载地址:https://github.com/seata/seata/releases/tag/v1.3.0

# 创建一个soft文件夹存放tar包
# 把下载的tar包上传到Linux的soft文件夹下
[root@localhost /]# mkdir /soft

[root@localhost /]# cd soft
# 解压
[root@localhost soft]# tar -zxvf seata-server-1.3.0.tar.gz 

# 复制seata到/usr/local/seata1
[root@localhost soft]# cp -rf seata /usr/local/seata1

# 进入seata1的conf目录
[root@localhost seata1]# cd /usr/local/seata1/conf

# 修改file.conf配置文件
[root@localhost conf]# vim file.conf

# 修改registry.conf配置文件
[root@localhost conf]# vim registry.conf

# 把seata1复制一份
[root@localhost conf]# cp -rf /usr/local/seata1 /usr/local/seata2

# 启动seata1
# -h 这里需要设置虚拟机的IP地址,不能设置为127.0.0.1
# -p 避免端口冲突
# -n Server node,在多个 TC Server 时,需区分各自节点,用于生成不同区间的 transactionId 事务编号,以免冲突
/usr/local/seata1/bin/seata-server.sh -h 192.168.23.131 -p 18091 -n 1

# 启动seata2
/usr/local/seata2/bin/seata-server.sh -h 192.168.23.131 -p 28091 -n 2

2.4 Seata与SpringCloudAlibaba版本对应关系

在这里插入图片描述

父pom

<dependencyManagement>
   <dependencies>
       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-alibaba-dependencies</artifactId>
           <version>2.1.4.RELEASE</version>
           <type>pom</type>
           <scope>import</scope>
       </dependency>
    </dependencies>
</dependencyManagement>

子pom

<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>
<!-- 指定1.3.0版本 -->
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>

3. Seata客户端应用

3.1 cloudalibaba-seata-storage9021

3.1.1 pom文件

    <dependencies>
        <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>

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

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

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

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>


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

    </dependencies>
3.1.2 yml文件
server:
  port: 9021

spring:
  application:
    name: seata-storage-service

  datasource:
    url: jdbc:mysql://192.168.23.131:3306/seata_storage?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    username: root
    password: root
    driverClassName: com.mysql.jdbc.Driver

  cloud:
    nacos:
      discovery:
        server-addr: 192.168.23.131:8848  #Nacos服务注册中心地址


seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: seata_group
  service:
    vgroup-mapping:
      seata_group: default
      
      # seata单机版
#    grouplist:
#      default: 192.168.23.131:18091


# seata集群版
  registry:
    type: nacos
    nacos:
      application: seata-server  # 通过微服务集群方式访问seata
      server-addr: 192.168.23.131:8848
      cluster: default
      group: SEATA_GROUP

在这里插入图片描述

在这里插入图片描述

3.1.3 启动类
package com.springcloud;

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  // 启用Nacos客户端
@MapperScan("com.springcloud.dao")  // Mybatis的Dao接口扫描
public class SeataStorageMain9021 {
    public static void main(String[] args) {
        SpringApplication.run(SeataStorageMain9021.class,args);
    }
}
3.1.4 Dao
package com.springcloud.dao;

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

public interface StorageDao {
    // 减库存
    @Update("update storage set stocks=stocks-#{count} where id=#{id}")
    int decrease(@Param("id") int id,@Param("count") int count);
}

3.1.5 Service
package com.springcloud.service;

public interface StorageService {
    int decrease( int id,int count);
}
package com.springcloud.service.impl;

import com.springcloud.dao.StorageDao;
import com.springcloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class StorageServiceImpl implements StorageService {
    @Autowired
    private StorageDao storageDao;


    @Override
    public int decrease(int id, int count) {
        return storageDao.decrease(id,count);
    }
}
3.1.6 Controller
package com.springcloud.controller;

import com.springcloud.service.StorageService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("storage")
public class StorageController {
    @Autowired
    private StorageService storageService;

    @GetMapping("/decrease")
    public String decrease(@RequestParam("id") int id,@RequestParam("count") int count){
        int result=storageService.decrease(id,count);
//        int i=10/0;
        return result==0?"库存操作失败":"减库存成功";
    }
}

3.2 cloudalibaba-seata-account9022

几乎跟storage9021一样,此处省略

3.3 cloudalibaba-seata-order9023

3.3.1 pom文件
<dependencies>
    <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>

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

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>

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

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

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

    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.0</version>
    </dependency>


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

</dependencies>
3.3.2 yml文件
server:
  port: 9023

spring:
  application:
    name: seata-order-service

  datasource:
    url: jdbc:mysql://192.168.23.131:3306/seata_order?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC&useSSL=false
    username: root
    password: root
    driverClassName: com.mysql.jdbc.Driver

  cloud:
    nacos:
      discovery:
        server-addr: 192.168.23.131:8848  #Nacos服务注册中心地址


seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: seata_group
  service:
    vgroup-mapping:
      seata_group: default
#    grouplist:
#      default: 192.168.23.131:18091


  registry:
    type: nacos
    nacos:
      application: seata-server  # 通过微服务集群方式访问seata
      server-addr: 192.168.23.131:8848
      userName: ""
      password: ""
      cluster: default
3.3.3 启动类
package com.springcloud;

import org.mybatis.spring.annotation.MapperScan;
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  // Nacos
@EnableFeignClients     // OpenFeign
@MapperScan("com.springcloud.dao")  // Mybatis
public class SeataOrderMain9023 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMain9023.class,args);
    }
}

3.3.4 Dao
package com.springcloud.dao;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface OrderDao {
    // 下订单
    @Insert("insert into `order` values (null,#{product_name})")
    int create(String product_name);
}

3.3.5 Feign
package com.springcloud.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Component
@FeignClient(value = "seata-storage-service")
public interface StorageFeign {
    @GetMapping("/storage/decrease")
    // OpenFeign访问多个参数的请求时需要加上@RequestParam
    public String decrease(@RequestParam("id") int id,@RequestParam("count") int count);
}

package com.springcloud.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Component
@FeignClient(value = "seata-account-service")
public interface AccountFeign {
    @GetMapping("/account/decrease")
    public String decrease(@RequestParam("id") int id,@RequestParam("amount") int amount);
}

3.3.6 Service
package com.springcloud.service;
public interface OrderService {
    int create(String product_name);
}

package com.springcloud.service.impl;

import com.springcloud.dao.OrderDao;
import com.springcloud.feign.AccountFeign;
import com.springcloud.feign.StorageFeign;
import com.springcloud.service.OrderService;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderDao orderDao;

    @Autowired
    private AccountFeign accountFeign;
    @Autowired
    private StorageFeign storageFeign;

    @Override
    @GlobalTransactional // 分布式事务
    public int create(String product_name){
        // 为了方便测试,数据随便写
        orderDao.create(product_name);
        storageFeign.decrease(1,1);
        accountFeign.decrease(1,10);

        return 1;
    }
}

3.3.6 Controller
package com.springcloud.controller;

import com.springcloud.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("order")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping("/create")
    public String create(String product_name){
        int result= orderService.create(product_name);
        return result==0?"订单操作失败":"下单成功";
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值