微服务分布式事务组件 Seata(二)

一、Seata 快速开始

1.1、Seata Server(TC)环境搭建

官网部署指南地址

这里为了组件版本统一,使用 1.3.0 版本

下载地址

Server端存储模式(store.mode) 支持三种:

  • file(默认)单机模式,全局事务会话信息内存中读写并持久化本地文件 root.data,目录为bin/sessionStore/root.data,性能较高
  • db:数据库(5.7+),高可用模式,全局事务会话信息通过db共享,相应性能差些
  • redis: Seata-Server 1.3及以上版本支持,性能较高,存在事务信息丢失风险请提前配置适合当前场景的redis持久化配置

1.2、部署高可用集群模式

db + Nacos 的方式部署

微服务分布式事务组件 Seata

vim /conf/file.conf
修改 mode
mode = "db"
并且修改 db 对应的相关数据库连接信息

1.3、创建数据库

数据库表需要参考 seata 官网的资源目录说明

资源目录介绍

点击查看(或根据版本分支选择对应的资源目录)

  • client

存放client端sql脚本 (包含 undo_log表) ,参数配置

  • config-center

各个配置中心参数导入脚本,config.txt(包含server和client,原名nacos-config.txt)为通用参数文件

  • server (服务器)

server (服务器) 端数据库脚本 (包含 lock_table、branch_table 与 global_table) 及各个容器配置

执行 seata-1.3.0/script/server/db/mysql.sql 文件

1.4、修改注册中心

为了微服务与 seata 服务进行通信

vim /config/registry.conf

修改 type = "nacos"
并且配置 nacos 服务地址以及用户名密码等信息
并且可以配置 loadBanance 策略

1.5、修改配置中心

为 seata 服务配置进行统一管理,默认存储在 config.txt 文件中。

vim /config/registry.conf

修改 config 中的 type = "nacos"
并且配置 nacos 的服务地址等信息

1.6、将配置注册到注册中心

注意:如果配置了seata server 使用nacos作为配置中心,则配置信息会从 naco s读取, file.conf 可以不用配置。客户端配置 registry.conf 使用 nacos 时也要注意 group 要和 seata server中的 group 一致,默认group是”DEFAULT_GROUP”

微服务分布式事务组件 Seata

配置事务分组,要与客户端配置的事务分组一致

my_test_tx_group 要与客户端保持一致 default 需要跟客户端和 regisry.conf 中 registry 中的 cluster 保持一致

事务分组:异地机房停电容错机制
参考官方文档(事务分组与高可用)
my

微服务分布式事务组件 Seata
my_test_tx_group 可以自定义 比如:(guagnzhou、shanghai),对应的客户端也要去设置

seata.service.vgroup-mapping.projectA=guagnzhou

default 必须要等于 registry.conf 注册中心的配置 cluster=”default”

运行脚本文件,将配置存入注册中心,脚本文件位置/script/config-center/nacos/nacos-config.sh,如果要指定 nacos 服务地址 sh nacos-config.sh -h localhost -p 8848 -g SEATA_GROUP -t xxxx
参数说明:
-h:host,默认值为 localhost
-p:port,默认值为 8848
-g:配置分组,默认值为 “SEATA_GROUP”
-t:租户信息,对应nacos的命名空间 ID 字段,默认为空
-u:用户名
-w:密码

微服务分布式事务组件 Seata

可以看到配置都注册进来了

微服务分布式事务组件 Seata

1.7、启动 seata 服务

启动命令
bin/seata-server.sh -h 127.0.0.1 -p 8091 -m db -n 1 -e test

参数 全写 作用 备注
-h –host 指定在注册中心注册的IP 不指定时获取当前的IP,外部访问部署在云环境和容器中的server建议指定
-p –port 指定 server 启动的端口 默认为8901
-m –storeMode 事务日志存储方式 支持file,db,redis,默认为file,注:redis 需要 seata-server 1.3 版本及以上
-n –serverNode 用于指定 seata-server节点ID 1,2,3…,默认为1
-e –seataEnv 指定 seata-server 运行环境 dev,test等,服务启动时或使用 registry-dev.conf这样的配置

在注册中心可以看到 seata-server 注册成功

微服务分布式事务组件 Seata(二)

如果是部署集群的话,seata-server.sh -p 8091 -n 1seata-server.sh -p 8092 -n 2seata-server.sh -p 8093 -n 3。。。,不需要通过其它进行负载均衡。

二、Seata Client 快速开始

声明式事务实现(@GlobalTransactional)
接入微服务应用
业务场景:
用户下单,这个那个业务逻辑由三个微服务构成:

  • 订单服务:根据采购需求创建订单
  • 库存服务:对给定的商品扣除库存数量

创建数据库,语句如下

seata_order.sql

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for order_tbl
-- ----------------------------
DROP TABLE IF EXISTS `order_tbl`;
CREATE TABLE `order_tbl` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `product_id` int NOT NULL COMMENT '商品id',
  `total_amount` int NOT NULL COMMENT '总金额',
  `status` int NOT NULL COMMENT '0:待付款,1:待发货',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- ----------------------------
-- Records of order_tbl
-- ----------------------------
BEGIN;
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

seata_stock.sql

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for stock_tbl
-- ----------------------------
DROP TABLE IF EXISTS `stock_tbl`;
CREATE TABLE `stock_tbl` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `product_id` int DEFAULT NULL,
  `count` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

-- ----------------------------
-- Records of stock_tbl
-- ----------------------------
BEGIN;
INSERT INTO `stock_tbl` (`id`, `product_id`, `count`) VALUES (1, 9, 100);
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;

父工程引入依赖

<dependencies>
    <!--jdbc-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <!--web-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--mybatis-plus-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.2</version>
    </dependency>
    <!--mysql 驱动-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!--druid数据源-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.11</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
    </dependency>
</dependencies>

项目流程大致如下

微服务分布式事务组件 Seata(二)

项目结构如下

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

2.1、测试本地事务是否可行

订单服务

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("order_tbl")
public class Order  {
    @TableId
    private Integer id;

    //商品id
    private Integer productId;
    //总金额
    private Integer totalAmount;
    //0:待付款,1:待发货
    private Integer status;
}

dao 层

@Repository
public interface OrderDao extends BaseMapper<Order> {
}

业务层

@Service
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements OrderService {

    @Autowired
    private RestTemplate restTemplate;

    @Transactional
    @Override
    public void create(Order order) {
        // 插入是否成功
        save(order);

        // 扣减库存是否成功
        LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("productId",order.getProductId());

        String response = restTemplate.postForObject("http://localhost:8071/stock/reduce", paramMap, String.class);

        // 异常
        int a = 1/0;
    }
}

控制层

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    // 插入订单信息
    @RequestMapping("/add")
    public String add() {
        Order order = new Order();
        order.setProductId(9);
        order.setStatus(0);
        order.setTotalAmount(100);

        orderService.create(order);
        return "下单成功";
    }
}

启动类

@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.hudu.dao")
public class SeataOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderApplication.class, args);
    }
}

application.yml 配置文件

server:
  port: 8070
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/seata_order?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

    # 初始化时运行 sql 脚本
    # schema: classpath:sql/seata_order.sql
    # initialization-mode: always
mybatis-plus:
  mapper-locations: classpath*:/mapper/*.xml
  global-config:
    db-config:
      id-type: auto

库存服务

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("stock_tbl")
public class Stock  {
    @TableId
    private Integer id;
    private Integer productId;
    private Integer count;
}

dao 层

@Repository
public interface StockDao extends BaseMapper<Stock> {
    void reduce(Integer productId);
}

映射层

<?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.hudu.dao.StockDao">
    <update id="reduce">
        update stock_tbl set `count` = `count` - 1
        where product_id = #{product}
    </update>
</mapper>

业务层

@Service("StockService")
@Slf4j
public class StockServiceImpl extends ServiceImpl<StockDao, Stock> implements StockService {

    @Autowired
    private StockDao stockDao;

    @Override
    public void reduce(Integer productId) {
        log.info("更新商品:{}",productId);
        stockDao.reduce(productId);
    }
}

控制层

@RestController
@RequestMapping("/stock")
public class StockController {
    @Autowired
    private StockService stockService;

    @RequestMapping("/reduce")
    public String reduce(@RequestParam(value = "productId") Integer productId) {
        stockService.reduce(productId);
        return "扣减库存";
    }
}

启动类

@SpringBootApplication
@MapperScan("com.hudu.dao")
public class SeataStockApplication {
    public static void main(String[] args) {
        SpringApplication.run(SeataStockApplication.class,args);
    }
}

application.yml 配置文件

server:
  port: 8071
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/seata_stock?characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123456
    type: com.alibaba.druid.pool.DruidDataSource

mybatis-plus:
  mapper-locations: classpath*:/mapper/*.xml
  global-config:
    db-config:
      id-type: auto

请求新增订单接口http://localhost:8070/order/add,在除零异常之前请求了扣减库存服务。

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

订单并没有插入,由于事务的回滚,但是库存已经减少了。所以使用本地事务的@Transactional
注解是无法解决分布式事务的场景的。

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

2.2、集成 openfeign 以及 nacos

订单服务引入 openfeign 和 Nacos 依赖

<dependencies>
    <!-- Nacos 服务注册与发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    <!--添加 openfeign 依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

库存服务添加依赖

<dependencies>
    <!-- Nacos 服务注册与发现 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
</dependencies>

配置注册中心地址

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.33.62:8847
        username: hudu
        password: 123456
        network-interface: en2

将之前的 restTemplate 调用改为 OpenFeign 远程调用

启动类添加@EnabldOpenFeignClients注解

@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.hudu.dao")
@EnableFeignClients
public class AlibabaSeataOrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(AlibabaSeataOrderApplication.class, args);
    }
}

创建一个接口,添加@FeignClient注解,并且指定服务名称,以及路径地址,添加需要请求的接口的地址

@FeignClient(value = "alibaba-stock-seata",path = "/stock")
public interface StockService {

    @RequestMapping("/reduce")
    String reduce(@RequestParam(value = "productId") Integer productId);
}

将原先业务层的 restTemplate 调用改为 openFeign 调用

@Service
public class OrderServiceImpl extends ServiceImpl<OrderDao, Order> implements OrderService {

    // @Autowired
    // private RestTemplate restTemplate;

    @Autowired
    private StockService stockService;

    @Transactional
    @Override
    public void create(Order order) {
        // 插入是否成功
        save(order);

        // 扣减库存是否成功
        // LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        // paramMap.add("productId",order.getProductId());

        // String response = restTemplate.postForObject("http://localhost:8071/stock/reduce", paramMap, String.class);
        stockService.reduce(order.getProductId());

        // 异常
        int a = 1/0;
    }
}

启动服务,可以看到两个服务都注册到了注册中心。

微服务分布式事务组件 Seata(二)

请求下单接口http://localhost:8070/order/add,由于依然使用的是本地事务,所以无法解决分布式事务问题。

三、搭建 seata client

3.1、引入 seata 依赖

<!--seata 依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

3.2、各微服务对应数据库中添加 undo_log 表

文件地址为seata/script/client/at/db/mysql.sql

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

3.3、配置事务分组

配置事务分组,与之前添加的配置保持一致

微服务分布式事务组件 Seata(二)

spring:
  cloud:
    alibaba:
      seata:
        tx-service-group: guangzhou

3.4、配置与 seata 的通信方式

seata:
  registry:
    # 配置 seata 注册中心,告诉 seata client 如何访问 seata server
    type: nacos
    nacos:
      # seata server 所在的 nacos 服务地址
      server-addr: 192.168.33.62:8847
      # seata server 的服务名称,默认为 seata-server
      application: seata-server
      username: nacos
      password: nacos
      # seata server 的分组,默认为 SEATA_GROUP
      group: SEATA_GROUP
  config:
    # 配置 seata 的配置中心,可以读取关于 seata client 的配置
    type: nacos
    nacos:
      server-addr: 192.168.33.62:8847
      username: nacos
      password: nacos
      namespace: public

3.5、使用@GlobalTransactional注解

    @GlobalTransactional
    @Override
    public void create(Order order) {
        // 插入是否成功
        save(order);
        stockService.reduce(order.getProductId());

        // 异常
        int a = 1/0;
    }

启动服务之后,服务都注册到了注册中心,并且可以看到 seata server 日志

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

再次请求生成订单接口http://localhost:8070/order/add,可以看到此时,订单没有生产,库存也没有减少

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

可以看到异常之后,事务回滚日志

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

四、Seata 运行原理总结

微服务分布式事务组件 Seata

1.TM 请求 TC 开启一个全局事务。TC 会生成一个 XID 作为该全局事务的编号。XID 会在微服务的调用链路中传播,保证将多个微服务的子事务关联在一起。

请求生产订单接口http://localhost:8070/order/add,当一进入到事务方法 crate() 中就会产生 XID,会注册全局事务信息,通过全局事务的 XID 进行关联。

微服务分布式事务组件 Seata(二)

global_table 存储的是全局事务信息

微服务分布式事务组件 Seata(二)

2.RM 请求 TC 将本地事务注册为全局事务的分支事务,通过全局事务的 XID 进行关联。
当执行到 sava() 和 调用远程的扣减库存时,即运行数据库操作方式时,会为事务参与者创建事务分支信息,存储在 branch_table 中

微服务分布式事务组件 Seata(二)

lock_table 是锁表的信息,可以看到在执行到 sava() 方法时,锁的是 order_tbl 表,锁的主键为 7

微服务分布式事务组件 Seata(二)

微服务分布式事务组件 Seata(二)

并且brefore image 和 after image 信息都保存在了 undo_log 表中的 rollback_info 字段中。

微服务分布式事务组件 Seata(二)

可以通过SELECT CONVERT(rollback_info USING utf8) FROM undo_logsql 语句将内容转换为 utf8 进行查看。可以看到 brefore image 以及 after image。before image 之所以没数据是因为,before image 执行的是插入数据操作,如果需要回滚,只需要将插入之后的 after image 的数据进行删除即可。可以看到 after image 中的信心,id主键,值为 7,product_id,商品 id 为 9 等信息。

微服务分布式事务组件 Seata(二)

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
247
粉丝
19
喜欢
219
收藏
63
排名:722
访问:9993
私信
所有博文
社区赞助商