【SpringCloud】10.Spring Cloud Alibaba Sentinel——分布式服务

官网:https://seata.apache.org/zh-cn/

下载地址:https://seata.apache.org/zh-cn/download/seata-server

一般来说,在多个微服务的环境下,不可能只有一个数据源吧?——如果只有,那么就不需要用到seta-Server了。

目前,关系型数据库提供的能力是基于单机事务的,一旦遇到分布式事务场景,就需要通过更多的技术手段来实现。seta-Server就是解决这些问题的。

概述

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

发展历程

阿里巴巴作为国内最早一批进行应用分布式(微服务化)改造的企业,很早就遇到微服务架构下的分布式事务问题。

2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案:

2014 年,阿里中间件团队发布 TXC(Taobao Transaction Constructor),为集团内应用提供分布式事务服务。

2016 年,TXC 在经过产品化改造后,以 GTS(Global Transaction Service) 的身份登陆阿里云,成为当时业界唯一一款云上分布式事务产品。在阿云里的公有云、专有云解决方案中,开始服务于众多外部客户。

2019 年起,基于 TXC 和 GTS 的技术积累,阿里中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback, FESCAR),和社区一起建设这个分布式事务解决方案。

2019 年 fescar(全称fast easy commit and rollback****) 被重命名为了seata(simple extensiable autonomous transaction architecture)。TXC、GTS、Fescar 以及 seata 一脉相承,为解决微服务架构下的分布式事务问题交出了一份与众不同的答卷。

Seata已经被Apache基金会收购‌。2023年10月,为了更好地通过社区驱动技术的演进,阿里巴巴和蚂蚁集团正式将Seata捐赠给Apache基金会,并通过了Apache基金会的投票决议,以全票通过的优秀表现加入Apache孵化器项目‌ 。

工作流简介

纵观整个分布式事务的管理,就是全局事务ID的传递和变更,要让开发者无感知。

Seata三个概念:

TC(Transaction Coordinator)-事务协调者,就是Seata,负责维护全局事务和分支事务的状态,驱动全局事务提交或回滚。

TM(Transaction Manager)-事务管理者,标注全局@GlobalTransactional,启动入口动作的微服务模块(比如订单模块),它是事务的发起者,负责定义全局事务的范围,并根据TC维护全局事务和分支事务状态,作出开始事务、提交事物、回滚事物的决议。

RM(Resource Manager)-资源管理者,。注册分支事务,汇报分支事务状态,驱动分支事务的提交或回滚。

分布式事务执行流程总结:

1737160137792

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

各种事务模式https://seata.apache.org/zh-cn/docs/user/mode/at

Seata AT模式;Seata TCC模式;Seata Saga模式;Seata XA模式。

Seta-Server 2.0.0 安装

  1. 下载seta,载地址:https://seata.apache.org/zh-cn/release-history/seata-server

  2. 新建seata库,用于存储seata的全局信息。

    打开mysql数据库,新建库seata库的全局信息。CREATE DATABASE seata;USE seata;

    seata运行mysql库:{home}\script\server\db\mysql.sql。

    1737017113937

    -- -------------------------------- The script used when storeMode is 'db' --------------------------------
    -- the table to store GlobalSession data
    CREATE TABLE IF NOT EXISTS `global_table`
    (
        `xid`                       VARCHAR(128) NOT NULL,
        `transaction_id`            BIGINT,
        `status`                    TINYINT      NOT NULL,
        `application_id`            VARCHAR(32),
        `transaction_service_group` VARCHAR(32),
        `transaction_name`          VARCHAR(128),
        `timeout`                   INT,
        `begin_time`                BIGINT,
        `application_data`          VARCHAR(2000),
        `gmt_create`                DATETIME,
        `gmt_modified`              DATETIME,
        PRIMARY KEY (`xid`),
        KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    -- 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 = utf8mb4;
    
    -- 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),
        `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_status` (`status`),
        KEY `idx_branch_id` (`branch_id`),
        KEY `idx_xid` (`xid`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `distributed_lock`
    (
        `lock_key`       CHAR(20) NOT NULL,
        `lock_value`     VARCHAR(20) NOT NULL,
        `expire`         BIGINT,
        primary key (`lock_key`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
    
  3. 更改模板配置:application.yml。(修改前记得先备份哦)位置:D:\seata\seata-2.0.0\conf\application.yaml。将seata的配置库,配置到数据库文件中。

    console:
      user:
        username: seata
        password: seata
    # 修改内容开始
    seata:
      config:
        # support: nacos, consul, apollo, zk, etcd3
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          namespace:
          group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
          username: nacos
          password: nacos
      registry:
        # support: nacos, eureka, redis, zk, consul, etcd3, sofa
        type: nacos
        nacos:
          application: seata-server
          server-addr: 127.0.0.1:8848
          group: SEATA_GROUP #后续自己在nacos里面新建,不想新建SEATA_GROUP,就写DEFAULT_GROUP
          namespace:
          cluster: default
          username: nacos
          password: nacos    
      store:
        # support: file 、 db 、 redis 、 raft
        mode: db
        db:
          datasource: druid
          db-type: mysql
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://localhost:13306/seata?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
          user: root
          password: root
          min-conn: 10
          max-conn: 100
          global-table: global_table
          branch-table: branch_table
          lock-table: lock_table
          distributed-lock-table: distributed_lock
          query-limit: 1000
          max-wait: 5000
      #  server:
      #    service-port: 8091 #If not configured, the default is '${server.port} + 1000'
     # 修改内容结束
     security:
        secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
        tokenValidityInMilliseconds: 1800000
        ignore:
          urls: /,/**/*.css,/**/*.js,/**/*.html,/**/*.map,/**/*.svg,/**/*.png,/**/*.jpeg,/**/*.ico,/api/v1/auth/login,/metadata/v1/**
    

    注:用户名和密码、mysql地址写自己的。

  4. 启动nacos 8848(startup.cmd -m standalone)。

  5. 打开{home}\bin路径。双击seata-server.bat命令执行后,查看http://localhost7091 。输入用户名和密码。查看配置知道,用户名和密码均为seata

  6. 再次查看nacos,服务列表已经添加seata-server服务。

    1737158425032

Seata案例实战——数据库和表准备

案例:我们准备3个业务数据:订单+ 库存+ 账户的MySql数据库准备。

当用户下单时,会在订单服务中创建一个订单,然后通过远程调用库存服务来扣减下单商品的库存,

再通过远程调用账户服务来扣减用户账户里面的余额,

最后在订单服务中修改订单状态为已完成。该操作跨越三个数据库,有两次远程调用,很明显会有分布式事务问题。

下订单→减库存→扣余额→改(订单状态)

步骤如下:

  1. 建立数据库
  2. 建立undo_log回滚日志表(AT模式专用)
  3. 3个库分表建立业务表

建立数据库,需要建立的库说明:

库名称 说明
seata_order 存储订单的数据库
seata_storage 存储库存的数据库
seata_account 存储账户信息的数据库
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;

建立undo_log回滚日志表(AT模式专用)

Seata是一种非入侵式的解决方案,需要为不同的库插入undo_log 日志 ,检查全局锁等。eata TCC模式;Seata Saga模式;Seata XA模式不需要。

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

3个库分别创建业务表

存储订单库(seata_order)数据表

CREATE TABLE t_order(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11)DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
SELECT * FROM t_order;

存储库存数据库(seata_storage)数据表

CREATE TABLE t_storage(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO t_storage(`id`,`product_id`,`total`,`used`,`residue`)VALUES('1','1','100','0','100');
SELECT * FROM t_storage;

存储账户信息数据库(seata_order)数据表

CREATE TABLE t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用账户余额',
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
)ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO t_account(`id`,`user_id`,`total`,`used`,`residue`)VALUES('1','1','1000','0','1000');
SELECT * FROM t_account;

微服务编码落地

业务数据库已经准备完毕,接着是编码落地。需求简单来说就是:

下订单→减库存→扣余额→改(订单状态)

步骤:

  • 一键生成代码
  • 修改公共方法
  • 新建Order微服务
  • 新建Storage微服务
  • 新建Account微服务

一键生成代码

接下来,我们会为以上数据库分表一键生成上述三个库的代码。这里使用的是Mybatis的Mapper4生成。(https://www.cnblogs.com/luyj00436/p/18683355)。

修改公共方法,cloud-api-commons 新增库存(StorageFeignApi)和账户(AccountFeign)的两个Feign。

库存Feign接口

/**
 * 库存接口
 * @Author:lyj
 * @Date:2025/1/22 09:29
 */
@FeignClient(value = "seata-storage-service")
public interface StorageFeignApi {
    /**
     * 扣减库存
     * @param productId
     * @param count
     * @return
     */
    @PostMapping(value = "/storage/decrease")
    ResultData decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}

账户Feign接口

/**
 * 账户余额接口
 * @Author:lyj
 * @Date:2025/1/22 09:51
 */
@FeignClient(value="seata-account-service")
public interface AccountFeignApi {
    /**
     * 扣减账户月
     * @param userId
     * @param money
     * @return
     */
    @PostMapping("/account/decrease")
    ResultData decrease(@RequestParam("userId") Long userId, @RequestParam("money") Long money);
}

新建Order 微服务

接下来,我们需要分别新建3个微服务。新建Order微服务,名称为:seata-order-service-2001

改POM,写业务类

<dependencies>
	<!-- nacos -->
	<dependency>
		<groupId>com.alibaba.cloud</groupId>
		<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
	</dependency>
	<!--alibaba-seata-->
	<dependency>
		<groupId>com.alibaba.cloud</groupId>
		<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	</dependency>
	<!--openfeign-->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-openfeign</artifactId>
	</dependency>
	<!--loadbalancer-->
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-loadbalancer</artifactId>
	</dependency>
	<!--cloud-api-commons-->
	<dependency>
		<groupId>com.atguigu.cloud</groupId>
		<artifactId>cloud-api-commons</artifactId>
		<version>1.0-SNAPSHOT</version>
	</dependency>
	<!--web + actuator-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-actuator</artifactId>
	</dependency>
	<!--SpringBoot集成druid连接池-->
	<dependency>
		<groupId>com.alibaba</groupId>
		<artifactId>druid-spring-boot-starter</artifactId>
	</dependency>
	<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
	<dependency>
		<groupId>org.springdoc</groupId>
		<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
	</dependency>
	<!--mybatis和springboot整合-->
	<dependency>
		<groupId>org.mybatis.spring.boot</groupId>
		<artifactId>mybatis-spring-boot-starter</artifactId>
	</dependency>
	<!--Mysql数据库驱动8 -->
	<dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
	</dependency>
	<!--persistence-->
	<dependency>
		<groupId>javax.persistence</groupId>
		<artifactId>persistence-api</artifactId>
	</dependency>
	<!--通用Mapper4-->
	<dependency>
		<groupId>tk.mybatis</groupId>
		<artifactId>mapper</artifactId>
	</dependency>
	<!--hutool-->
	<dependency>
		<groupId>cn.hutool</groupId>
		<artifactId>hutool-all</artifactId>
	</dependency>
	<!-- fastjson2 -->
	<dependency>
		<groupId>com.alibaba.fastjson2</groupId>
		<artifactId>fastjson2</artifactId>
	</dependency>
	<!--lombok-->
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.28</version>
		<scope>provided</scope>
	</dependency>
	<!--test-->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-test</artifactId>
		<scope>test</scope>
	</dependency>
</dependencies>

写YAML,写配置。

server:
  port: 2001

spring:
  application:
    name: seata-order-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服务注册中心地址
# ==========applicationName + druid-mysql8 driver===================
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:13306/seata_order?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
    username: root
    password: root
# ========================mybatis===================
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.atguigu.cloud.entities
  configuration:
    map-underscore-to-camel-case: true

# ========================seata===================
seata:
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  tx-service-group: default_tx_group # 事务组,由它获得TC服务的集群名称
  service:
    vgroup-mapping: # 点击源码分析
      default_tx_group: default # 事务组与TC服务集群的映射关系
  data-source-proxy-mode: AT

logging:
  level:
    io:
      seata: info

主启动

/**
 * @Author:lyj
 * @Date:2025/1/22 10:50
 */
@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper") //import tk.mybatis.spring.annotation.MapperScan;
@EnableDiscoveryClient  // 服务注册和发现
@EnableFeignClients
public class SeataOrderMain2001 {
    public static void main(String[] args) {
        SpringApplication.run(SeataOrderMain2001.class, args);
    }
}

业务类

复制之前生成的Order业务类。(Order.javaOrderMapper.javaOrderMapper.xml)。

新建Service及实现。

/**
 * @Author:lyj
 * @Date:2025/1/22 10:59
 */
public interface OrderService {
    /***
     * 创建订单
     * @param order
     */
    void create(Order order);
}
/**
 * @Author:lyj
 * @Date:2025/1/22 11:00
 * 下订单→减库存→扣余额→改订单
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Resource
    private OrderMapper orderMapper;        // 订单
    @Resource
    private StorageFeignApi storageFeignApi;    // 库存
    @Resource
    private AccountFeignApi accountFeignApi;      // 账户

    /**
     * 创建订单
     * @param order
     */
    @Override
    @GlobalTransactional(name = "sentinel-create-order", rollbackFor = Exception.class) //@GlobalTransactional ,事务的发起者(比如订单模块)
    public void create(Order order) {
        // xid 检查
        String xid = RootContext.getXID();
        // 1. 创建订单
        log.info("=================开始创建订单" + "\t" + "xid_order:" + xid);
        // 订单创建状态 status : (0:创建中; 1:已完结)
        order.setStatus(0);
        int result = orderMapper.insertSelective(order);
        // 插入订单成功后,获得mysql的实体对象
        Order orderFromDB = null;
        if (result > 1) {
            orderFromDB = orderMapper.selectOne(order);
            log.info("-------> 新建订单成功,orderFromDB info:" + orderFromDB);
            System.out.println();
            // 2. 扣减库存
            log.info("------> 订单微服务开始使用Storage库存,做扣减count");
            storageFeignApi.decrease(orderFromDB.getProductId(),orderFromDB.getCount());
            System.out.println();
            // 3. 扣减账号月
            log.info("------> 订单微服务开始使用Account庄户,做扣减money");
            accountFeignApi.decrease(orderFromDB.getProductId(),orderFromDB.getMoney());
            System.out.println();
            // 4. 修改订单状态(0:创建中,1:已完结)
            log.info("------->修改订单状态");
            orderFromDB.setStatus(1);
            Example whereCondition = new Example(Order.class);
            Example.Criteria criteria = whereCondition.createCriteria();
            criteria.andEqualTo("userId", orderFromDB.getUserId());
            criteria.andEqualTo("status", 0);
            int updateResult = orderMapper.updateByExampleSelective(orderFromDB, whereCondition);
            log.info("------->修改订单状态完成" + "\t" + updateResult);
        }
        System.out.println();
        log.info("结束新建订单\t" + "xid_order:" + xid);
    }
}

创建接口

/**
 * @Author:lyj
 * @Date:2025/1/22 11:35
 */
@RestController
public class OrderController {
    @Resource
    private OrderService orderService;

    @GetMapping("/order/create")
    public ResultData create(Order order) {
        orderService.create(order);
        return ResultData.success(order);
    }
}

创建库存、账户微服务

库存微服务,模块名称 : seata-storge-service2002。其他与常规创建模块方法一致,略。

账户微服务,模块名称:seata-account-service2003。其他与常规创建模块方法一致,略。

Seata案例测试

测试前准备

  • 启动Nacos
  • 启动Seata
  • 启动微服务2001
  • 启动微服务2002
  • 启动微服务2003

生成下单请求:GET http://localhost:2001/order/create? userId=1&productId=1&count=1&money=1

此时,发现数据库完成了一个订单,两个个扣减。

posted @ 2025-03-11 13:56  陆陆无为而治者  阅读(56)  评论(0)    收藏  举报