• Seata


    本地事务

    本地事务,也就是传统的单机事务。在传统数据库事务中,必须要满足四个原则:

    在这里插入图片描述

    //  Exception.class 是所有异常的父类-->指定即可
    @Transactional(rollbackFor=Exception.class)
    
    • 1
    • 2

    分布式事务

    分布式事务,就是指不是在单个服务或单个数据库架构下,产生的事务,例如:

    • 跨数据源的分布式事务
    • 跨服务的分布式事务
    • 综合情况

    在数据库水平拆分、服务垂直拆分之后,一个业务操作通常要跨多个数据库、服务才能完成。例如电商行业中比较常见的下单付款案例,包括下面几个行为:

    • 创建新订单
    • 扣减商品库存
    • 从用户账户余额扣除金额

    完成上面的操作需要访问三个不同的微服务和三个不同的数据库
    在这里插入图片描述
    订单的创建、库存的扣减、账户扣款在每一个服务和数据库内是一个本地事务,可以保证ACID原则。

    但是当我们把三件事情看做一个"业务",要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式系统下的事务了。

    此时ACID难以满足,这是分布式事务要解决的问题

    分布式事务的思路

    分布式 最大的问题是各个子事务的一致性问题,因此可以借鉴 CAP定理和BASE理论,有两种解决思路:

    • AP模式:各 子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。

    • CP模式:各 个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。

    但 V不管是哪一种模式,都需要在子系统事务之间互相通讯,协调事务状态,也就是需要一个事务协调者(TC)
    在这里插入图片描述
    这里的子系统事务,称为分支事务;有关联的各个分支事务在一起称为全局事务

    初识Seata

    Seata的架构

    Seata事务管理中有三个重要的角色:

    • TC (Transaction Coordinator) - **事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。

    • TM (Transaction Manager) - **事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。

    • RM (Resource Manager) - **资源管理器:**管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

    整体的架构如图:
    在这里插入图片描述

    安装 TC事务协调者

    1. 解压文件
      http://seata.io/zh-cn/blog/download.html
    2. 新建 数据库 seata ,导入 sql

    sql

    create database seata;
    use seata;
    -- -------------------------------- 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_and_branch_id` (`xid` , `branch_id`)
    ) 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);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75

    地址:/seata/script/server/db/mysql.sql

    1. 改配置

    seata/conf/application.yml

    server:
      port: 7091  # 控制台 端口
    
    spring:
      application:
        name: seata-server
    
    logging:
      config: classpath:logback-spring.xml
      file:
        path: ${user.home}/logs/seata
      extend:
        logstash-appender:
          destination: 127.0.0.1:4560
        kafka-appender:
          bootstrap-servers: 127.0.0.1:9092
          topic: logback_to_logstash
    
    console:
      user:
        username: seata  # 控制台账号
        password: seata  # 控制台密码
    
    seata:
      config:
        # support: nacos, consul, apollo, zk, etcd3,采用 nacos作为注册中心
        type: nacos
        nacos:
          server-addr: 192.168.111.101:8848  # nacos地址
          # 默认为 public
          namespace: "" 
          #注意分组
          group: DEFAULT_GROUP
          username: nacos
          password: nacos
          data-id: seata-server.properties #seata-server配置文件
      registry:
        # support: nacos, eureka, redis, zk, consul, etcd3, sofa
        type: nacos
        nacos:
          application: seata-tc-server  # 注册到 nacos中的应用名
          server-addr: 192.168.111.101:8848
          namespace: ""
          #注意分组
          group: DEFAULT_GROUP
          cluster: HF #集群
          username: nacos
          password: nacos
    
      store:
        # support: file 、 db 、 redis,存储地址配置
        mode: db
        db:
          datasource: druid
          db-type: mysql
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.111.101:3306/seata?useUnicode=true&rewriteBatchedStatements=true&serverTimezone=Asia/Shanghai #填线上Mysql的IP:PORT,seata数据库名,要和前面插入seata表同一个名
          user: root #数据库账号
          password: 123456 #数据库密码
          min-conn: 5
          max-conn: 100
          global-table: global_table
          branch-table: branch_table
          lock-table: lock_table
          distributed-lock-table: distributed_lock
          query-limit: 100
          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,/**/*.ico,/console-fe/public/**,/api/v1/auth/login
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    1. 赋予权限
    chmod +x seata-server.sh
    
    • 1
    1. 启动 seata 的 tc服务

    先启动mysql和nacos!!!!

    ./seata-server.sh -p 8091 -h 192.168.111.101
    # 必须指定地址,-h 192.168.111.101,不然无法注册事务管理器TM!!
    
    • 1
    • 2
    1. 查看日志
    tail  -f /app/seata/logs/start.out
    
    • 1

    在这里插入图片描述

    1. 查看seata 控制台,账号密码都是seata
      在这里插入图片描述
    2. 查看Nacos

    在这里插入图片描述

    TC服务的高可用和异地容灾

    将事务组映射配置到 nacos

    接下来,我们需要将tx-service-group与cluster的映射关系都配置到nacos配置中心。

    新建一个配置:
    在这里插入图片描述

    配置的内容如下:

    # 事务组映射关系
    service.vgroupMapping.seata-demo=SH
    
    service.enableDegrade=false
    service.disableGlobalTransaction=false
    # 与TC服务的通信配置
    transport.type=TCP
    transport.server=NIO
    transport.heartbeat=true
    transport.enableClientBatchSendRequest=false
    transport.threadFactory.bossThreadPrefix=NettyBoss
    transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
    transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
    transport.threadFactory.shareBossWorker=false
    transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
    transport.threadFactory.clientSelectorThreadSize=1
    transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
    transport.threadFactory.bossThreadSize=1
    transport.threadFactory.workerThreadSize=default
    transport.shutdown.wait=3
    # RM配置
    client.rm.asyncCommitBufferLimit=10000
    client.rm.lock.retryInterval=10
    client.rm.lock.retryTimes=30
    client.rm.lock.retryPolicyBranchRollbackOnConflict=true
    client.rm.reportRetryCount=5
    client.rm.tableMetaCheckEnable=false
    client.rm.tableMetaCheckerInterval=60000
    client.rm.sqlParserType=druid
    client.rm.reportSuccessEnable=false
    client.rm.sagaBranchRegisterEnable=false
    # TM配置
    client.tm.commitRetryCount=5
    client.tm.rollbackRetryCount=5
    client.tm.defaultGlobalTransactionTimeout=60000
    client.tm.degradeCheck=false
    client.tm.degradeCheckAllowTimes=10
    client.tm.degradeCheckPeriod=2000
    
    # undo日志配置
    client.undo.dataValidation=true
    client.undo.logSerialization=jackson
    client.undo.onlyCareUpdateColumns=true
    client.undo.logTable=undo_log
    client.undo.compress.enable=true
    client.undo.compress.type=zip
    client.undo.compress.threshold=64k
    client.log.exceptionRate=100
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    微服务读取nacos配置

    接下来,需要修改每一个微服务的application.yml文件,让微服务读取nacos中的client.properties文件:

    seata:
      config:
        type: nacos
        nacos:
          server-addr: 127.0.0.1:8848
          username: nacos
          password: nacos
          group: SEATA_GROUP
          data-id: client.properties
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    重启微服务,现在 微服务到底是连接tc的SH集群,还是tc的HZ集群,都统一由nacos的client.properties来决定了。

    微服务集成Seata, 往TC注册服务

    每个 参与全局事务的微服务都要 完成以下步骤!!!

    1. pom依赖
    
    <dependency>
        <groupId>io.seatagroupId>
        <artifactId>seata-spring-boot-starterartifactId>
        <version>1.5.2version>
    dependency>
    
    
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-seataartifactId>
        <exclusions>
            
            <exclusion>
                <artifactId>seata-spring-boot-starterartifactId>
                <groupId>io.seatagroupId>
            exclusion>
        exclusions>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    1. yaml
    seata:
      registry: # 去 注册中心nacos中 获取tc服务地址
        type: nacos # 注册中心类型 nacos
        nacos:
          server-addr: 192.168.111.101:8848 # nacos地址
          namespace: "" # namespace,默认为空
          group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
          application: seata-tc-server # seata服务名称
          username: nacos
          password: nacos
      tx-service-group: seata-demo # 事务组名称
      service:
        vgroup-mapping: # 事务组与cluster的映射关系
          seata-demo: HF
    spring:
      application:
        name: order # 必须要指定!!
      cloud:
        nacos:
          server-addr: 192.168.111.101:8848  #将服务注册到nacos
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    注册成功!

    微服务如何根据这些配置寻找TC的地址呢?

    我们知道 注册到Nacos中的微服务,确定一个具体实例需要四个信息:

    • namespace:命名空间
    • group:分组
    • application:服务名
    • cluster:集群名
      在这里插入图片描述
      namespace为空,就是默认的public

    结合起来,TC服务的信息就是:public@DEFAULT_GROUP@seata-tc-server@SH,这样就能确定TC服务集群了。然后就可以去Nacos拉取对应的实例信息了。

    Seata的使用

    AT模式

    配置

    seata:
      data-source-proxy-mode: XA # 强一致
      # 默认是AT
    
    • 1
    • 2
    • 3

    加注解@GlobalTransactional

    import io.seata.spring.annotation.GlobalTransactional;
    @GlobalTransactional
    
    • 1
    • 2

    XA 强一致性,AT 最终一致性

    AT:
    在这里插入图片描述

    TCC模式

    TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:

    • Try:资源的检测和预留;

    • Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。

    • Cancel:预留资源释放,可以理解为try的反向操作。

    在这里插入图片描述

    TCC的优点

    • 一阶段完成直接提交事务,释放数据库资源,性能好
    • 相比AT模型,无需生成快照,无需使用全局锁,性能最强
    • 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库

    TCC的缺点

    • 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
    • 软状态,事务是最终一致
    • 需要考虑Confirm和Cancel的失败情况,做好幂等处理

    事务悬挂和空回滚

    空回滚

    当某分支事务的try阶段阻塞时,可能导致全局事务超时而触发二阶段的cancel操作。在未执行try操作时先执行了cancel操作,这时cancel不能做回滚,就是空回滚

    如图:
    在这里插入图片描述
    执行cancel操作时,应当判断try是否已经执行,如果尚未执行,则应该空回滚。

    业务悬挂

    对于已经空回滚的业务,之前被阻塞的try操作恢复,继续执行try,就永远不可能confirm或cancel ,事务一直处于中间状态,这就是业务悬挂

    执行try操作时,应当判断cancel是否已经执行过了,如果已经执行,应当阻止空回滚后的try操作,避免悬挂

    实现TCC模式

    解决空回滚和业务悬挂问题,必须要记录当前事务状态,是在try、还是cancel?

    思路分析

    这里我们定义一张表:

    CREATE TABLE `account_freeze_tbl` (
      `xid` varchar(128) NOT NULL,
      `user_id` varchar(255) DEFAULT NULL COMMENT '用户id',
      `freeze_money` int(11) unsigned DEFAULT '0' COMMENT '冻结金额',
      `state` int(1) DEFAULT NULL COMMENT '事务状态,0:try,1:confirm,2:cancel',
      PRIMARY KEY (`xid`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    其中:

    • xid:是全局事务id
    • freeze_money:用来记录用户冻结金额
    • state:用来记录事务状态

    那此时,我们的业务开怎么做呢?

    • Try业务:
      • 记录冻结金额和事务状态到account_freeze表
      • 扣减account表可用金额
    • Confirm业务
      • 根据xid删除account_freeze表的冻结记录
    • Cancel业务
      • 修改account_freeze表,冻结金额为0,state为2
      • 修改account表,恢复可用金额
    • 如何判断是否空回滚?
      • cancel业务中,根据xid查询account_freeze,如果为null则说明try还没做,需要空回滚
    • 如何避免业务悬挂?
      • try业务中,根据xid查询account_freeze ,如果已经存在则证明Cancel已经执行,拒绝执行try业务

    接下来,我们改造account-service,利用TCC实现余额扣减功能。

    声明TCC接口

    TCC的Try、Confirm、Cancel方法都需要在接口中基于注解来声明,

    我们在account-service项目中的cn.itcast.account.service包中新建一个接口,声明TCC三个接口:

    package cn.itcast.account.service;
    
    import io.seata.rm.tcc.api.BusinessActionContext;
    import io.seata.rm.tcc.api.BusinessActionContextParameter;
    import io.seata.rm.tcc.api.LocalTCC;
    import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
    
    @LocalTCC
    public interface AccountTCCService {
    
        @TwoPhaseBusinessAction(name = "deduct", commitMethod = "confirm", rollbackMethod = "cancel")
        void deduct(@BusinessActionContextParameter(paramName = "userId") String userId,
                    @BusinessActionContextParameter(paramName = "money")int money);
    
        boolean confirm(BusinessActionContext ctx);
    
        boolean cancel(BusinessActionContext ctx);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    编写实现类

    在account-service服务中的cn.itcast.account.service.impl包下新建一个类,实现TCC业务:

    package cn.itcast.account.service.impl;
    
    import cn.itcast.account.entity.AccountFreeze;
    import cn.itcast.account.mapper.AccountFreezeMapper;
    import cn.itcast.account.mapper.AccountMapper;
    import cn.itcast.account.service.AccountTCCService;
    import io.seata.core.context.RootContext;
    import io.seata.rm.tcc.api.BusinessActionContext;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    
    @Service
    @Slf4j
    public class AccountTCCServiceImpl implements AccountTCCService {
    
        @Autowired
        private AccountMapper accountMapper;
        @Autowired
        private AccountFreezeMapper freezeMapper;
    
        @Override
        @Transactional
        public void deduct(String userId, int money) {
            // 0.获取事务id
            String xid = RootContext.getXID();
            // 1.扣减可用余额
            accountMapper.deduct(userId, money);
            // 2.记录冻结金额,事务状态
            AccountFreeze freeze = new AccountFreeze();
            freeze.setUserId(userId);
            freeze.setFreezeMoney(money);
            freeze.setState(AccountFreeze.State.TRY);
            freeze.setXid(xid);
            freezeMapper.insert(freeze);
        }
    
        @Override
        public boolean confirm(BusinessActionContext ctx) {
            // 1.获取事务id
            String xid = ctx.getXid();
            // 2.根据id删除冻结记录
            int count = freezeMapper.deleteById(xid);
            return count == 1;
        }
    
        @Override
        public boolean cancel(BusinessActionContext ctx) {
            // 0.查询冻结记录
            String xid = ctx.getXid();
            AccountFreeze freeze = freezeMapper.selectById(xid);
    
            // 1.恢复可用余额
            accountMapper.refund(freeze.getUserId(), freeze.getFreezeMoney());
            // 2.将冻结金额清零,状态改为CANCEL
            freeze.setFreezeMoney(0);
            freeze.setState(AccountFreeze.State.CANCEL);
            int count = freezeMapper.updateById(freeze);
            return count == 1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62

    高可用

    在这里插入图片描述
    微服务基于事务组(tx-service-group)与TC集群的映射关系,来查找当前应该使用哪个TC集群。当SH集群故障时,只需要将vgroup-mapping中的映射关系改成HZ。则 所有微服务就会切换到HZ的TC集群了。

  • 相关阅读:
    python setup.py bdist_wheel 报错的处理办法
    软件开发介绍
    百度飞桨公布最新成果:凝聚535万开发者,服务20万家企事业单位
    虚拟环境下把python代码打包成exe(小白教程)
    手写RPC Day3 服务注册
    网络ioctl实践3:设置网卡的mac、ip、子网掩码、广播地址
    如何选择合适的官文转录供应商
    网络工程师----第三十六天
    做好一个BI项目的关键是什么
    2022年最新海南机动车签字授权人模拟考试及答案
  • 原文地址:https://blog.csdn.net/qq_30659573/article/details/127563258