Apache Seata应用侧启动过程剖析——注册中心与配置中心模块

news2024/10/5 14:08:57

本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。
本文来自 Apache Seata官方文档,欢迎访问官网,查看更多深度文章。

Apache Seata应用侧启动过程剖析——注册中心与配置中心模块

前言

在Seata的应用侧(RM、TM)启动过程中,首先要做的就是与协调器侧(TC)建立通信,这是Seata能够完成分布式事务协调的前提,那么Seata在完成应用侧初始化以及与TC建立连接的过程中,是如何找到TC事务协调器的集群和地址的?又是如何从配置模块中获取各种配置信息的呢?这正是本文要探究的重点。

给个限定

Seata作为一款中间件级的底层组件,是很谨慎引入第三方框架具体实现的,感兴趣的同学可以深入了解下Seata的SPI机制,看看Seata是如何通过大量扩展点(Extension),来将依赖组件的具体实现倒置出去,转而依赖抽象接口的,同时,Seata为了更好地融入微服务、云原生等流行架构所衍生出来的生态中,也基于SPI机制对多款主流的微服务框架、注册中心、配置中心以及Java开发框架界“扛把子”——SpringBoot等做了主动集成,在保证微内核架构、松耦合、可扩展的同时,又可以很好地与各类组件“打成一片”,使得采用了各种技术栈的环境都可以比较方便地引入Seata。

本文为了贴近大家刚引入Seata试用时的场景,在以下介绍中,选择应用侧的限定条件如下:使用File(文件)作为配置中心与注册中心,并基于SpringBoot启动。

有了这个限定条件,接下来就让我们深入Seata源码,一探究竟吧。

多模块交替协作的RM/TM初始化过程

在 Seata客户端启动过程剖析(一)中,我们分析了Seata应用侧TM与RM的初始化、以及应用侧如何创建Netty Channel并向TC Server发送注册请求的过程。除此之外,在RM初始化过程中,Seata的其他多个模块(注册中心、配置中心、负载均衡)也都纷纷登场,相互协作,共同完成了连接TC Server的过程。

当执行Client重连TC Server的方法:NettyClientChannelManager.Channreconnect()时,首先需要根据当前的事务分组获取可用的TC Server地址列表:

    /**
     * NettyClientChannelManager.reconnect()
     * Reconnect to remote server of current transaction service group.
     *
     * @param transactionServiceGroup transaction service group
     */
    void reconnect(String transactionServiceGroup) {
        List<String> availList = null;
        try {
            //从注册中心中获取可用的TC Server地址
            availList = getAvailServerList(transactionServiceGroup);
        } catch (Exception e) {
            LOGGER.error("Failed to get available servers: {}", e.getMessage(), e);
            return;
        }
        //以下代码略
    }

关于事务分组的详细概念介绍,大家可以参考官方文档事务分组介绍。这里简单介绍一下:

  • 每个Seata应用侧的RM、TM,都具有一个事务分组
  • 每个Seata协调器侧的TC,都具有一个集群名地址
    应用侧连接协调器侧时,经历如下两步:
  • 通过事务分组的名称,从配置中获取到该应用侧对应的TC集群名
  • 通过集群名称,可以从注册中心中获取TC集群的地址列表
    以上概念、关系与过程,如下图所示:
    Seata事务分组与建立连接的关系

注册中心获取TC Server集群地址

了解RM/TC连接TC时涉及的主要概念与步骤后,我们继续探究getAvailServerList方法:

    private List<String> getAvailServerList(String transactionServiceGroup) throws Exception {
        //① 使用注册中心工厂,获取注册中心实例
        //② 调用注册中心的查找方法lookUp(),根据事务分组名称获取TC集群中可用Server的地址列表
        List<InetSocketAddress> availInetSocketAddressList = RegistryFactory.getInstance().lookup(transactionServiceGroup);
        if (CollectionUtils.isEmpty(availInetSocketAddressList)) {
            return Collections.emptyList();
        }

        return availInetSocketAddressList.stream()
                                         .map(NetUtil::toStringAddress)
                                         .collect(Collectors.toList());
    }
用哪个注册中心?Seata元配置文件给出答案

上面已提到,Seata支持多种注册中心的实现,那么,Seata首先需要从一个地方先获取到“注册中心的类型”这个信息。

从哪里获取呢?Seata设计了一个“配置文件”用于存放其框架内所用组件的一些基本信息,我更愿意称这个配置文件为 『元配置文件』,这是因为它包含的信息,其实是“配置的配置”,也即“元”的概念,大家可以对比数据库表中的信息,和数据库表本身结构的信息(表数据和表元数据)来理解。

我们可以把注册中心、配置中心中的信息,都看做是配置信息本身,而这些配置信息的配置是什么?这些信息,就包含在Seata的元配置文件中。实际上,『元配置文件』中只包含两类信息

  • 一是注册中心的类型:registry.type,以及该类型注册中心的一些基本信息,比如当注册中心类型为文件时,元配置文件中存放了文件的名字信息;当注册中心类型是Nacos时,元配置文件中则存放着Nacos的地址、命名空间、集群名等信息
  • 二是配置中心的类型:config.type,以及该类型配置中心的一些基本信息,比如当配置中心为文件时,元配置文件中存放了文件的名字信息;当注册中心类型为Consul时,元配置文件中存放了Consul的地址信息

Seata的元配置文件支持Yaml、Properties等多种格式,而且可以集成到SpringBoot的application.yaml文件中(使用seata-spring-boot-starter即可),方便与SpringBoot集成。

Seata中自带的默认元配置文件是registry.conf,当我们采用文件作为注册与配置中心时,registry.conf中的内容设置如下:

registry {
  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
  type = "file"
  file {
    name = "file.conf"
  }
}

config {
  # file、nacos 、apollo、zk、consul、etcd3
  type = "file"
  file {
    name = "file.conf"
  }
}

在如下源码中,我们可以发现,Seata使用的注册中心的类型,是从ConfigurationFactory.CURRENT_FILE_INSTANCE中获取的,而这个CURRENT_FILE_INSTANCE,就是我们所说的,Seata元配置文件的实例

    //在getInstance()中,调用buildRegistryService,构建具体的注册中心实例
    public static RegistryService getInstance() {
        if (instance == null) {
            synchronized (RegistryFactory.class) {
                if (instance == null) {
                    instance = buildRegistryService();
                }
            }
        }
        return instance;
    }

    private static RegistryService buildRegistryService() {
        RegistryType registryType;
        //获取注册中心类型
        String registryTypeName = ConfigurationFactory.CURRENT_FILE_INSTANCE.getConfig(
            ConfigurationKeys.FILE_ROOT_REGISTRY + ConfigurationKeys.FILE_CONFIG_SPLIT_CHAR
                + ConfigurationKeys.FILE_ROOT_TYPE);
        try {
            registryType = RegistryType.getType(registryTypeName);
        } catch (Exception exx) {
            throw new NotSupportYetException("not support registry type: " + registryTypeName);
        }
        if (RegistryType.File == registryType) {
            return FileRegistryServiceImpl.getInstance();
        } else {
            //根据注册中心类型,使用SPI的方式加载注册中心的实例
            return EnhancedServiceLoader.load(RegistryProvider.class, Objects.requireNonNull(registryType).name()).provide();
        }
    }

我们来看一下元配置文件的初始化过程,当首次获取静态字段CURRENT_FILE_INSTANCE时,触发ConfigurationFactory类的初始化:

    //ConfigurationFactory类的静态块
    static {
        load();
    }

     /**
     * load()方法中,加载Seata的元配置文件
     */   
    private static void load() {
        //元配置文件的名称,支持通过系统变量、环境变量扩展
        String seataConfigName = System.getProperty(SYSTEM_PROPERTY_SEATA_CONFIG_NAME);
        if (seataConfigName == null) {
            seataConfigName = System.getenv(ENV_SEATA_CONFIG_NAME);
        }
        if (seataConfigName == null) {
            seataConfigName = REGISTRY_CONF_DEFAULT;
        }
        String envValue = System.getProperty(ENV_PROPERTY_KEY);
        if (envValue == null) {
            envValue = System.getenv(ENV_SYSTEM_KEY);
        }
        //根据元配置文件名称,创建一个实现了Configuration接口的文件配置实例
        Configuration configuration = (envValue == null) ? new FileConfiguration(seataConfigName,
                false) : new FileConfiguration(seataConfigName + "-" + envValue, false);
        Configuration extConfiguration = null;
        //通过SPI加载,来判断是否存在扩展配置提供者
        //当应用侧使用seata-spring-boot-starer时,将通过SpringBootConfigurationProvider作为扩展配置提供者,这时当获取元配置项时,将不再从file.conf(默认)中获取,而是从application.properties/application.yaml中获取
        try {
            //通过ExtConfigurationProvider的provide方法,将原有的Configuration实例替换为扩展配置的实例
            extConfiguration = EnhancedServiceLoader.load(ExtConfigurationProvider.class).provide(configuration);
            if (LOGGER.isInfoEnabled()) {
                LOGGER.info("load Configuration:{}", extConfiguration == null ? configuration.getClass().getSimpleName()
                        : extConfiguration.getClass().getSimpleName());
            }
        } catch (EnhancedServiceNotFoundException ignore) {

        } catch (Exception e) {
            LOGGER.error("failed to load extConfiguration:{}", e.getMessage(), e);
        }
        //存在扩展配置,则返回扩展配置实例,否则返回文件配置实例
        CURRENT_FILE_INSTANCE = extConfiguration == null ? configuration : extConfiguration;
    }

load()方法的调用序列图如下:
Seata元配置文件的加载过程

上面的序列图中,大家可以关注以下几点:

  • Seata元配置文件名称支持扩展
  • Seata元配置文件后缀支持3种后缀,分别为yaml/properties/conf,在创建元配置文件实例时,会依次尝试匹配
  • Seata中配置能力相关的顶级接口为Configuration,各种配置中心均需实现此接口,Seata的元配置文件就是使用FileConfiguration(文件类型的配置中心)实现了此接口
/**
 * Seata配置能力接口
 * package:io.seata.config
 */

public interface Configuration {
    /**
     * Gets short.
     *
     * @param dataId       the data id
     * @param defaultValue the default value
     * @param timeoutMills the timeout mills
     * @return the short
     */
    short getShort(String dataId, int defaultValue, long timeoutMills);

    //以下内容略,主要能力为配置的增删改查
}
  • Seata提供了一个类型为ExtConfigurationProvider的扩展点,开放了对配置具体实现的扩展能力,它具有一个provide()方法,接收原有的Configuration,返回一个全新的Configuration,此接口方法的形式决定了,一般可以采用静态代理、动态代理、装饰器等设计模式来实现此方法,实现对原有Configuration的增强
/**
 * Seata扩展配置提供者接口
 * package:io.seata.config
 */
public interface ExtConfigurationProvider {
    /**
     * provide a AbstractConfiguration implementation instance
     * @param originalConfiguration
     * @return configuration
     */
    Configuration provide(Configuration originalConfiguration);
}
  • 当应用侧基于seata-seata-spring-boot-starter启动时,将采用『SpringBootConfigurationProvider』作为扩展配置提供者,在其provide方法中,使用动态字节码生成(CGLIB)的方式为『FileConfiguration』实例创建了一个动态代理类,拦截了所有以"get"开头的方法,来从application.properties/application.yaml中获取元配置项。

关于SpringBootConfigurationProvider类,本文只说明下实现思路,不再展开分析源码,这也仅是ExtConfigurationProvider接口的一种实现方式,从Configuration可扩展、可替换的角度来看,Seata正是通过ExtConfigurationProvider这样一个扩展点,为多种配置的实现提供了一个广阔的舞台,允许配置的多种实现与接入方案。

经历过上述加载流程后,如果我们没有扩展配置提供者,我们将从Seata元配置文件中获取到注册中心的类型为file,同时创建了一个文件注册中心实例:FileRegistryServiceImpl

从注册中心获取TC Server地址

获取注册中心的实例后,需要执行lookup()方法(RegistryFactory.getInstance().lookup(transactionServiceGroup)),FileRegistryServiceImpl.lookup()的实现如下:

    /**
     * 根据事务分组名称,获取TC Server可用地址列表
     * package:io.seata.discovery.registry
     * class:FileRegistryServiceImpl
     */
    @Override
    public List<InetSocketAddress> lookup(String key) throws Exception {
        //获取TC Server集群名称
        String clusterName = getServiceGroup(key);
        if (clusterName == null) {
            return null;
        }
        //从配置中心中获取TC集群中所有可用的Server地址
        String endpointStr = CONFIG.getConfig(
            PREFIX_SERVICE_ROOT + CONFIG_SPLIT_CHAR + clusterName + POSTFIX_GROUPLIST);
        if (StringUtils.isNullOrEmpty(endpointStr)) {
            throw new IllegalArgumentException(clusterName + POSTFIX_GROUPLIST + " is required");
        }
        //将地址封装为InetSocketAddress并返回
        String[] endpoints = endpointStr.split(ENDPOINT_SPLIT_CHAR);
        List<InetSocketAddress> inetSocketAddresses = new ArrayList<>();
        for (String endpoint : endpoints) {
            String[] ipAndPort = endpoint.split(IP_PORT_SPLIT_CHAR);
            if (ipAndPort.length != 2) {
                throw new IllegalArgumentException("endpoint format should like ip:port");
            }
            inetSocketAddresses.add(new InetSocketAddress(ipAndPort[0], Integer.parseInt(ipAndPort[1])));
        }
        return inetSocketAddresses;
    }

    /**
     * 注册中心接口中的default方法
     * package:io.seata.discovery.registry
     * class:RegistryService
     */
    default String  getServiceGroup(String key) {
        key = PREFIX_SERVICE_ROOT + CONFIG_SPLIT_CHAR + PREFIX_SERVICE_MAPPING + key;
        //在配置缓存中,添加事务分组名称变化监听事件
        if (!SERVICE_GROUP_NAME.contains(key)) {
            ConfigurationCache.addConfigListener(key);
            SERVICE_GROUP_NAME.add(key);
        }
        //从配置中心中获取事务分组对应的TC集群名称
        return ConfigurationFactory.getInstance().getConfig(key);
    }

可以看到,代码逻辑与第一节中图Seata事务分组与建立连接的关系中的流程相符合,
这时,注册中心将需要配置中心的协助,来获取事务分组对应的集群名称、并查找集群中可用的服务地址。

配置中心获取TC集群名称

配置中心的初始化

配置中心的初始化(在ConfigurationFactory.buildConfiguration()),与注册中心的初始化流程类似,都是先从元配置文件中获取配置中心的类型等信息,然后初始化一个具体的配置中心实例,有了之前的分析基础,这里不再赘述。

获取配置项的值

上方代码段的两个方法:*FileRegistryServiceImpl.lookup()以及RegistryService.getServiceGroup()*中,都从配置中心中获取的配置项的值:

  • lookup()需要由具体的注册中心实现,使用文件作为注册中心,其实是一种直连TC Server的情况,其特殊点在于TC Server的地址是写死在配置中的的(正常应存于注册中心中),因此FileRegistryServiceImpl.lookup()方法,是通过配置中心获取的TC集群中Server的地址信息
  • getServiceGroup()是RegistryServer接口中的default方法,即所有注册中心的公共实现,Seata中任何一种注册中心,都需要通过配置中心来根据事务分组名称来获取TC集群名称

负载均衡

经过上述环节配置中心、注册中心的协作,现在我们已经获取到了当前应用侧所有可用的TC Server地址,那么在发送真正的请求之前,还需要通过特定的负载均衡策略,选择一个TC Server地址,这部分源码比较简单,就不带着大家分析了。

关于负载均衡的源码,大家可以阅读AbstractNettyRemotingClient.doSelect(),因本文分析的代码是RMClient/TMClient的重连方法,此方法中,所有获取到的Server地址,都会通过遍历依次连接(重连),因此这里不需要再做负载均衡。

以上就是Seata应用侧在启动过程中,注册中心与配置中心这两个关键模块之间的协作关系与工作流程,欢迎共同探讨、学习!

后记:本文及其上篇 Seata客户端启动过程剖析(一),是本人撰写的首批技术博客,将上手Seata时,个人认为Seata中较为复杂、需要研究和弄通的部分源码进行了分析和记录。
在此欢迎各位读者提出各种改进建议,谢谢!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1901305.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Docker逃逸CVE-2019-5736、procfs云安全漏洞复现,全文5k字,超详细解析!

Docker容器挂载procfs 逃逸 procfs是展示系统进程状态的虚拟文件系统&#xff0c;包含敏感信息。直接将其挂载到不受控的容器内&#xff0c;特别是容器默认拥有root权限且未启用用户隔离时&#xff0c;将极大地增加安全风险。因此&#xff0c;需谨慎处理&#xff0c;确保容器环…

最适合mysql5.6安装的linux版本-实战

文章目录 一, 适合安装mysql5.6的linu版本1. CentOS 72. Ubuntu 14.04 LTS (Trusty Tahr)3. Debian 8 (Jessie)4. Red Hat Enterprise Linux (RHEL) 7 二, 具体以Ubuntu 14.04 LTS (Trusty Tahr)为例安装虚拟机安装Ubuntu 14.04 LTS (Trusty Tahr) 自己弄安装ssh(便于远程访问,…

入职字节外包2个月后,我离职了...

有一种打工人的羡慕&#xff0c;叫做“大厂”。 真是年少不知大厂香&#xff0c;错把青春插稻秧。 但是&#xff0c;在深圳有一群比大厂员工更庞大的群体&#xff0c;他们顶着大厂的“名”&#xff0c;做着大厂的工作&#xff0c;还可以享受大厂的伙食&#xff0c;却没有大厂…

短视频商城系统源码揭秘:架构设计与实现

在短视频平台和电商平台蓬勃发展的背景下&#xff0c;短视频商城系统应运而生&#xff0c;融合了短视频内容和电商功能&#xff0c;给用户带来了全新的购物体验。本文将揭示短视频商城系统的源码架构设计与实现&#xff0c;帮助开发者了解该系统的内部工作原理及其关键技术。 …

C++11中新特性介绍-之(二)

11.自动类型推导 (1) auto类型自动推导 auto自动推导变量的类型 auto并不代表某个实际的类型&#xff0c;只是一个类型声明的占位符 auto并不是万能的在任意场景下都能推导&#xff0c;使用auto声明的变量必须进行初始化&#xff0c;以让编译器推导出它的实际类型&#xff0c;…

自然之美无需雕琢

《自然之美&#xff0c;无需雕琢 ”》在这个颜值至上的时代&#xff0c;但在温馨氛围中&#xff0c;单依纯以一种意想不到的方式&#xff0c;为我们诠释了自然之美的真谛。而医生的回答&#xff0c;如同一股清流耳目一新。“我说医生你看我这张脸&#xff0c;有没有哪里要动的。…

团队编程:提升代码质量与知识共享的利器

目录 前言1. 什么是团队编程&#xff1f;1.1 团队编程的起源1.2 团队编程的工作流程 2. 团队编程的优势2.1 提高代码质量2.2 促进知识共享2.3 增强团队协作2.4 提高开发效率 3. 团队编程的挑战3.1 开发成本较高3.2 需要良好的团队协作3.3 个人风格和习惯的差异3.4 长时间的集中…

2024亚太杯数学建模竞赛(B题)的全面解析

你是否在寻找数学建模比赛的突破点&#xff1f;数学建模进阶思路&#xff01; 作为经验丰富的数学建模团队&#xff0c;我们将为你带来2024亚太杯数学建模竞赛&#xff08;B题&#xff09;的全面解析。这个解决方案包不仅包括完整的代码实现&#xff0c;还有详尽的建模过程和解…

Docker:一、安装与卸载、配置阿里云加速器(Ubuntu)

目录 &#x1f341;安装docker&#x1f332;1、环境准备&#x1f332;2、安装docker Engine&#x1f9ca;1、卸载旧版、任何冲突的包&#x1f9ca;2、使用存储库安装&#x1f9ca;3、安装 Docker 包。&#x1f9ca;4、查询是否安装成功&#x1f9ca;5、运行hello-world镜像&…

图像处理调试软件推荐

对于图像处理的调试&#xff0c;使用具有图形用户界面&#xff08;GUI&#xff09;且支持实时调整和预览的图像处理软件&#xff0c;可以大大提高工作效率。以下是几款常用且功能强大的图像处理调试软件推荐&#xff1a; ImageJ/FijiMATLABOpenCV with GUI LibrariesNI Vision …

Stable Diffusion:最全详细图解

Stable Diffusion&#xff0c;作为一种革命性的图像生成模型&#xff0c;自发布以来便因其卓越的生成质量和高效的计算性能而受到广泛关注。不同于以往的生成模型&#xff0c;Stable Diffusion在生成图像的过程中&#xff0c;采用了独特的扩散过程&#xff0c;结合深度学习技术…

WAIC:生成式 AI 时代的到来,高通创新未来!

目录 01 在终端侧算力上&#xff0c;动作最快的就是高通 02 模型优化&#xff0c;完成最后一块拼图 在WAIC上&#xff0c;高通展示的生成式AI创新让我们看到了未来的曙光。 生成式 AI 的爆发带来了意想不到的产业格局变化&#xff0c;其速度之快令人惊叹。 仅在一个月前&…

考研必备~总结严蔚敏教授《数据结构》课程的重要知识点及考点

作者主页&#xff1a;知孤云出岫 目录 1. 基本概念1.1 数据结构的定义1.2 抽象数据类型 (ADT) 2. 线性表2.1 顺序表2.2 链表 3. 栈和队列3.1 栈3.2 队列 4. 树和二叉树4.1 树的基本概念4.2 二叉树 5. 图5.1 图的基本概念5.2 图的遍历 6. 查找和排序6.1 查找6.2 排序 7. 重点考…

[图解]SysML和EA建模住宅安全系统-11-接口块

1 00:00:00,660 --> 00:00:04,480 接下来的步骤是定义系统上下文 2 00:00:04,960 --> 00:00:07,750 首先是图17.17 3 00:00:09,000 --> 00:00:10,510 系统上下文展示了 4 00:00:10,520 --> 00:00:12,510 ESS和外部系统、用户 5 00:00:12,520 --> 00:00:14,1…

简介时间复杂度

好了&#xff0c;今天我们来了解一下&#xff0c;我们在做练习题中常出现的一个名词。时间复杂度。我相信大家如果有在练习过题目的话。对这个名词应该都不陌生吧。但是可能很少的去思考它是干什么的代表的什么意思。反正我以前练习的时候就是这样。我只知道有这么一个名词在题…

DevOps实战:使用GitLab+Jenkins+Kubernetes(k8s)建立CI_CD解决方案

一.系统环境 本文主要基于Kubernetes1.21.9和Linux操作系统CentOS7.4。 服务器版本docker软件版本Kubernetes(k8s)集群版本CPU架构CentOS Linux release 7.4.1708 (Core)Docker version 20.10.12v1.21.9x86_64CI/CD解决方案架构图:CI/CD解决方案架构图描述:程序员写好代码之…

测试几个 ocr 对日语的识别情况

测试几个 ocr 对日语的识别情况 1. EasyOCR2. PaddleOCR3. Deepdoc&#xff08;识别pdf中图片&#xff09;4. Deepdoc&#xff08;识别pdf中文字&#xff09;5. Nvidia neva-22b6. Claude 3.5 sonnet 识别图片中的文字7. Claude 3.5 sonnet 识别 pdf 中表格8. OpenAI gpt-4o 识…

CMS Made Simple v2.2.15 远程命令执行漏洞(CVE-2022-23906)

前言 CVE-2022-23906 是一个远程命令执行&#xff08;RCE&#xff09;漏洞&#xff0c;存在于 CMS Made Simple v2.2.15 中。该漏洞通过上传头像功能进行利用&#xff0c;攻击者可以上传一个经过特殊构造的图片文件来触发漏洞。 漏洞详情 CMS Made Simple v2.2.15 中的头像上…

verilog读写文件注意事项

想要的16进制数是文本格式提供的文件&#xff0c;想将16进制数提取到变量内&#xff0c; 可以使用 f s c a n f ( f d 1 , " 也可以使用 fscanf(fd1,"%h",rd_byte);实现 也可以使用 fscanf(fd1,"也可以使用readmemh(“./FILE/1.txt”,mem);//fe放在mem[0…

alphazero学习

AlphaGoZero是AlphaGo算法的升级版本。不需要像训练AlphaGo那样&#xff0c;不需要用人类棋局这些先验知识训练&#xff0c;用MCTS自我博弈产生实时动态产生训练样本。用MCTS来创建训练集&#xff0c;然后训练nnet建模的策略网络和价值网络。就是用MCTSPlayer产生的数据来训练和…