Flink作业执行之 3.StreamGraph

Flink任务如何跑起来之 3.StreamGraph

1. StreamGraphGenerator

在前文了解Transformation和StreamOperator后。接下来Transformation将转换成StreamGraph,即作业的逻辑拓扑结构。

env.execute()方法中调用getStreamGraph方法生成StreamGraph实例。StreamGraphStreamGraphGenerator负责生成。

StreamGraphGenerator实例中封装了前面生成的Transformation集合。

private StreamGraph getStreamGraph(List<Transformation<?>> transformations) {synchronizeClusterDatasetStatus();// 根据Transformation生成StreamGraphGenerator,然后再生成StreamGraphreturn getStreamGraphGenerator(transformations).generate();
}
// 创建StreamGraphGenerator实例
private StreamGraphGenerator getStreamGraphGenerator(List<Transformation<?>> transformations) {// ...return new StreamGraphGenerator(// 传入transformations集合new ArrayList<>(transformations), config, checkpointCfg, configuration).setStateBackend(defaultStateBackend).setChangelogStateBackendEnabled(changelogStateBackendEnabled).setSavepointDir(defaultSavepointDirectory).setChaining(isChainingEnabled).setUserArtifacts(cacheFile).setTimeCharacteristic(timeCharacteristic).setDefaultBufferTimeout(bufferTimeout).setSlotSharingGroupResource(slotSharingGroupResources);
}

generate方法核心逻辑如下,首先创建一个空的StreamGraph实例。然后通过遍历transformations集合,依次调用transform方法完成StreamGraph中节点和边实例的创建,并将节点和边加入到StreamGraph中。

public StreamGraph generate() {// 先实例化一个空的StreamGraphstreamGraph = new StreamGraph(executionConfig, checkpointConfig, savepointRestoreSettings);// ...for (Transformation<?> transformation : transformations) {// 依次处理transformationtransform(transformation);}final StreamGraph builtStreamGraph = streamGraph;// ...return builtStreamGraph;
}

一个作业中生成的StreamGraph和Transformation实例数量而言,一个任务会生成多个Transformation实例,单个Transformation实例中仅包含直接上游实例。但一个任务只会生成一个StreamGraph实例,StreamGraph是一个完整的图的表示,其中包含了图中全部的节点和边。

2. TransformationTranslator

TransformationTranslator 负责根据执行模式将给定的 Transformation 转换为其运行时实现,即StreamGraph。其接口中定义了批和流处理模式下的方法。

public interface TransformationTranslator<OUT, T extends Transformation<OUT>> {// 批模式Collection<Integer> translateForBatch(final T transformation, final Context context);// 流模式Collection<Integer> translateForStreaming(final T transformation, final Context context);
}

在StreamGraphGenerator实例的创建过程中会通过静态代码块生成如下TransformationTransformationTranslator的映射关系。包含了Transformation子类中除FeedbackTransformationCoFeedbackTransformation之外的其他剩余子类,共计16个值。
FeedbackTransformationCoFeedbackTransformation未提供TransformationTranslator的实现,需要单独处理。

static {Map<Class<? extends Transformation>, TransformationTranslator<?, ? extends Transformation>>tmp = new HashMap<>();tmp.put(OneInputTransformation.class, new OneInputTransformationTranslator<>());tmp.put(TwoInputTransformation.class, new TwoInputTransformationTranslator<>());tmp.put(MultipleInputTransformation.class, new MultiInputTransformationTranslator<>());tmp.put(KeyedMultipleInputTransformation.class, new MultiInputTransformationTranslator<>());tmp.put(SourceTransformation.class, new SourceTransformationTranslator<>());tmp.put(SinkTransformation.class, new SinkTransformationTranslator<>());tmp.put(LegacySinkTransformation.class, new LegacySinkTransformationTranslator<>());tmp.put(LegacySourceTransformation.class, new LegacySourceTransformationTranslator<>());tmp.put(UnionTransformation.class, new UnionTransformationTranslator<>());tmp.put(PartitionTransformation.class, new PartitionTransformationTranslator<>());tmp.put(SideOutputTransformation.class, new SideOutputTransformationTranslator<>());tmp.put(ReduceTransformation.class, new ReduceTransformationTranslator<>());tmp.put(TimestampsAndWatermarksTransformation.class, new TimestampsAndWatermarksTransformationTranslator<>());tmp.put(BroadcastStateTransformation.class, new BroadcastStateTransformationTranslator<>());tmp.put(KeyedBroadcastStateTransformation.class, new KeyedBroadcastStateTransformationTranslator<>());tmp.put(CacheTransformation.class, new CacheTransformationTranslator<>());// 将映射关系保存在成员属性中translatorMap = Collections.unmodifiableMap(tmp);
}

3. StreamGraph

StreamGraph表示Flink执行图,描述了作业的逻辑拓扑结构,并以DAG的形式描述作业中算子之间的上下游连接关系。

StreamGraph实现了Pipeline接口,接口中没有任何内容,仅为了表示DataStream中的StreamGraphDataSet中的Plan都属于Pipeline类型。
在这里插入图片描述
StreamGraph表示DAG,DAG中节点和边分别使用StreamNodeStreamEdge类表示。

三者的UML关系如下
在这里插入图片描述
StreamGraph中将全部的StreamNode节点保存在其集合属性中,同时单独指定了Source节点和sink节点,相关属性如下

// 全部节点数据,key=节点id,即transformation的id
private Map<Integer, StreamNode> streamNodes;
// 表示Source的节点id
private Set<Integer> sources;
// 表示sink的节点id
private Set<Integer> sinks;
private Set<Integer> expandedSinks;
// 旁路输出的节点信息
private Map<Integer, Tuple2<Integer, OutputTag>> virtualSideOutputNodes;
// 虚拟节点信息,key = 新生成的虚拟节点id,tuple3为虚拟节点信息.f0=此虚拟节点的上游节点id
private Map<Integer, Tuple3<Integer, StreamPartitioner<?>, StreamExchangeMode>> virtualPartitionNodes;

一个节点最基础的信息有:节点id/名称、入/出边信息、工作内容。
在这里插入图片描述
上述基础信息维护在以下属性中。其中operatorFactory和jobVertexClass属性表示节点工作内容。

// 节点id
private final int id;
// 并行度
private int parallelism;
private int maxParallelism;
// 节点名称
private final String operatorName;
// 工作内容:算子信息
private StreamOperatorFactory<?> operatorFactory;
// 节点入边
private List<StreamEdge> inEdges = new ArrayList<StreamEdge>();
// 节点出边
private List<StreamEdge> outEdges = new ArrayList<StreamEdge>();
// 工作内容:StreamTask实例,表示该节点所属的StreamTask子类型。
private final Class<? extends TaskInvokable> jobVertexClass;

StreamEdge中表示边基本信息的属性字段如下。

// 边id
private final String edgeId;
// 边连接的上游节点id,即StreamNode.id
private final int sourceId;
// 边连接的下游节点id
private final int targetId;
// 上游节点名称
private final String sourceOperatorName;
// 下游节点名称
private final String targetOperatorName;

4. 生成StreamGraph

对实现了TransformationTranslator接口的16种Transformation而言(上述静态代码内容),Transformation转换过程大致如下。

首先从Transformation中获取id、name、输入类型(即上游Transformation中的输出类型,Source没有)、输出类型、StreamOperatorFactory实例等内容作为节点和边实例中基础信息。
Class<? extends TaskInvokable> vertexClass信息在具体的TransformationTranslator子类中进行指定。

然后通过StreamGraph中addNode方法,生成StreamNode实例并将该实例加入到Map<Integer, StreamNode> streamNodes,如果是Source则将节点id加入到Set<Integer> sources,如果是sink则将节点id加入到Set<Integer> sinks

4.1. 生成节点

addNode方法如下

protected StreamNode addNode(Integer vertexID, // transformation id@Nullable String slotSharingGroup,@Nullable String coLocationGroup,Class<? extends TaskInvokable> vertexClass, // StreanTask实例StreamOperatorFactory<?> operatorFactory,  // transformation中的工厂实例String operatorName) { // transformation name,如果是Source或sink,则分别拼接"Source: "或"Sink: "前缀// ...// 生成节点实例StreamNode vertex =new StreamNode(vertexID,slotSharingGroup,coLocationGroup,operatorFactory,operatorName,vertexClass);// 将节点添加到mapstreamNodes.put(vertexID, vertex);return vertex;
}

节点id和名称直接取自Transformation的id和名称。如果是Source或sink,则分别拼接"Source: "或"Sink: "前缀。
节点工作内容来自Transformation中的StreamOperatorFactory实例。

生成节点实例后,根据Transformation中的并行度,设置节点的并行度。如果Transformation中未设置并行度时,获取配置中默认的并行度。

注意,此时的节点并不包含边属性。

4.2. 设置节点的边

节点可能存在入边和出边,根据节点是否存在上游决定是否需要设置入边信息,完成当前节点的入边设置同时,将该边设置为相应上游节点的出边。每个节点的出边由下游节点触发设置
Source作为头节点,不存在上游,因此source节点不存在设置边的操作。

当节点存在上游节点时,通过StreamGraph中addEdge方法完成节点边的设置。如果存在多个上游,则循环调用addEdge方法。

public void addEdge(Integer upStreamVertexID, // 上游节点idInteger downStreamVertexID, // 当前节点idint typeNumber, // 只有co-task任务才会涉及到,多条入边的序号IntermediateDataSetID intermediateDataSetId) {// 注意在这里调用时, partitioner、outputTag、exchangeMode传null值addEdgeInternal(upStreamVertexID,downStreamVertexID,typeNumber,null, // 注意new ArrayList<String>(),null, // 注意null, // 注意intermediateDataSetId);
}private void addEdgeInternal(Integer upStreamVertexID,Integer downStreamVertexID,int typeNumber,StreamPartitioner<?> partitioner,List<String> outputNames,OutputTag outputTag,StreamExchangeMode exchangeMode,IntermediateDataSetID intermediateDataSetId) {if (virtualSideOutputNodes.containsKey(upStreamVertexID)) {// 上游节点是旁路输出节点时int virtualId = upStreamVertexID;upStreamVertexID = virtualSideOutputNodes.get(virtualId).f0;if (outputTag == null) {outputTag = virtualSideOutputNodes.get(virtualId).f1;}// 递归调用addEdgeInternal(upStreamVertexID,downStreamVertexID,typeNumber,partitioner,null,outputTag,exchangeMode,intermediateDataSetId);} else if (virtualPartitionNodes.containsKey(upStreamVertexID)) {// 上游节点是虚拟节点时int virtualId = upStreamVertexID;// 上游(虚拟)节点的父节点idupStreamVertexID = virtualPartitionNodes.get(virtualId).f0;if (partitioner == null) {// 获取了虚拟节点的partitionerpartitioner = virtualPartitionNodes.get(virtualId).f1;}// 获取了虚拟节点的数据exchangeModeexchangeMode = virtualPartitionNodes.get(virtualId).f2;// 递归调用addEdgeInternal(upStreamVertexID,downStreamVertexID,typeNumber,partitioner,outputNames,outputTag,exchangeMode,intermediateDataSetId);} else {// 创建边实例createActualEdge(upStreamVertexID,downStreamVertexID,typeNumber,partitioner,outputTag,exchangeMode,intermediateDataSetId);}
}

createActualEdge方法完成边的创建并将边添加到上下游节点中。

private void createActualEdge(Integer upStreamVertexID,Integer downStreamVertexID,int typeNumber,StreamPartitioner<?> partitioner,OutputTag outputTag,StreamExchangeMode exchangeMode,IntermediateDataSetID intermediateDataSetId) {StreamNode upstreamNode = getStreamNode(upStreamVertexID);StreamNode downstreamNode = getStreamNode(downStreamVertexID);// 设置数据分区partitioner = ...// 算子之间的数据交换模式if (exchangeMode == null) {exchangeMode = StreamExchangeMode.UNDEFINED;}int uniqueId = getStreamEdges(upstreamNode.getId(), downstreamNode.getId()).size();// 生成边实例StreamEdge edge =new StreamEdge(upstreamNode,downstreamNode,typeNumber,partitioner,outputTag,exchangeMode,uniqueId,intermediateDataSetId);// 最后将生成的边分别添加到上游节点的List<StreamEdge> outEdges和当前节点的List<StreamEdge>getStreamNode(edge.getSourceId()).addOutEdge(edge);getStreamNode(edge.getTargetId()).addInEdge(edge);
}

5. WordCount实例的StreamGraph

WordCount示例中,按照DataStream的转换流程将得到如下关系的Transformation信息。因此StreamGraph将由如下Transformation得到。
在这里插入图片描述
前文提到Transformation分为物理和虚拟两大类,物理类别将会生成节点,而虚拟类别将生成边。上述生成的5个Transformation中PartitionTransformation属于虚拟类别,而其余4个均数据物理类别。既然虚拟类别将生成边,那么其处理方式定然与其他4个节点有所不同。

5.1. 虚拟节点

在PartitionTransformationTranslator中translateInternal方法中,将调用StreamGraph中的addVirtualPartitionNode方法,将PartitionTransformation加入到表示虚拟节点集合中。并没有生成节点的操作。

private Collection<Integer> translateInternal(final PartitionTransformation<OUT> transformation,final Context context,boolean supportsBatchExchange) {checkNotNull(transformation);checkNotNull(context);final StreamGraph streamGraph = context.getStreamGraph();// 上游Transformation,在本示例中为OneInputTransformation,tId=2final Transformation<?> input = ...List<Integer> resultIds = new ArrayList<>();StreamExchangeMode exchangeMode = ...;for (Integer inputId : context.getStreamNodeIds(input)) {// 当前作业中已生成5个Transformation实例,因此下一个自增id为6final int virtualId = Transformation.getNewNodeId();// 加入虚拟节点集合中streamGraph.addVirtualPartitionNode(// inputId即上游id=2,virtualId=6inputId, virtualId, transformation.getPartitioner(), exchangeMode);resultIds.add(virtualId);}// 最后将新生成的ids返回return resultIds;
}// StreamGraph中的addVirtualPartitionNode方法
public void addVirtualPartitionNode(Integer originalId,Integer virtualId,StreamPartitioner<?> partitioner,StreamExchangeMode exchangeMode) {virtualPartitionNodes.put(virtualId, new Tuple3<>(originalId, partitioner, exchangeMode));
}

处理完成PartitionTransformation之后,StreamGraph实例中的虚拟节点集合中Map<Integer, Tuple3<Integer, StreamPartitioner<?>, StreamExchangeMode>> virtualPartitionNodes中便存在了元素。
接下来处理ReduceTransformation,其上游节点是虚拟节点,因此在生成边时,在addEdgeInternal方法中将会执行上游节点是虚拟节点时得逻辑分支。

还记得前面提到的addEdgeInternal方法中存在3个逻辑判断吗?

private void addEdgeInternal(Integer upStreamVertexID,Integer downStreamVertexID,int typeNumber,StreamPartitioner<?> partitioner,List<String> outputNames,OutputTag outputTag,StreamExchangeMode exchangeMode,IntermediateDataSetID intermediateDataSetId) {if (virtualSideOutputNodes.containsKey(upStreamVertexID)) {// 上游节点是旁路输出节点时// ...} else if (virtualPartitionNodes.containsKey(upStreamVertexID)) {// 上游节点是虚拟节点时// 本实例中ReduceTransformation的上游节点为虚拟节点,因此将会执行这段逻辑。int virtualId = upStreamVertexID; // 6,为什么是6在介绍PartitionTransformationTranslator处理逻辑时有解释// 上游(虚拟)节点的父节点upStreamVertexID = virtualPartitionNodes.get(virtualId).f0; // 2,即OneInputTransformation的idif (partitioner == null) {// 获取了虚拟节点的partitionerpartitioner = virtualPartitionNodes.get(virtualId).f1;}// 获取了虚拟节点的exchangeModeexchangeMode = virtualPartitionNodes.get(virtualId).f2;// 递归调用时,PartitionTransformation从上下游中消失了,仅仅从PartitionTransformation中获取了partitioner和exchangeMode信息。addEdgeInternal(upStreamVertexID, // 2downStreamVertexID, // 4typeNumber,partitioner,outputNames,outputTag,exchangeMode,intermediateDataSetId);} else {// 生成边信息// ...}
}

原始的Transformation关系中,ReduceTransformation的上游是PartitionTransformationT(tId=3),从前面PartitionTransformationTranslator处理逻辑中已知,PartitionTransformation并未真正生成节点,而是加入到了表示虚拟节点集合中,因此获PartitionTransformation的上游节点即OneInputTransformation(tId=2),作为ReduceTransformation在StreamGraph的父节点。

最终得到的StreamGraph示意图如下图所示(省略并行度信息)。
在这里插入图片描述
当作业中存在旁路输出时,处理方式与虚拟节点类似,不在赘述。

6. 一点理解

试着理解下为什么要将Transformation转成StreamGraph?
最初设计者的设计和初衷不得而知,以下纯粹个人理解。

Transformation到StreamGraph转换可以看作是链表结构到图结构的转换。

Transformation是类似于单向链表的结构,并且还是指向上游的逆向链表,从其中任何一个Transformation开始只能获取其上游数据。必须遍历全部的Transformation实例后,才能得到完成的作业信息。
Transformation结构中和上游是嵌套关系,这样多个实例中都最终指向同一个上游,处理关系时存在冗余。
但是Transformation的好处是生成方便。每次DataStream转换时,十分清楚的知道上游是谁,直接将上游实例传递到当前实例中即可。

StreamGraph是图的结构。可以使用图的处理方式快速处理节点关系。同时也更接近最终的作业执行拓扑结构。

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

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

相关文章

如何在 ASP.NET Core Web Api 项目中应用 NLog 写日志?

前言 昨天分享了在 .NET Core Console 项目中应用 NLog 写日志的详细例子&#xff0c;有几位小伙伴私信说 ASP.NET Core Web Api 项目中无法使用&#xff0c;其实在 ASP.NET Core Web Api 项目中应用 NLog 写日志&#xff0c;跟 .NET Core Console 项目是有些不一样的&#xf…

如何确保数据跨域交换安全、合规、可追溯性?

数据跨域交换是指在不同的组织、系统或网络之间进行数据的传输和共享。随着数字经济的发展&#xff0c;数据跨域交换在促进数据流通和创新融合方面发挥着重要作用。然而&#xff0c;这一过程也面临着诸多挑战和风险&#xff0c;例如数据安全、合规性、完整性以及责任不清晰等问…

顶顶通呼叫中心中间件(mod_cti基于FreeSWITCH)-通话时长限制

文章目录 前言联系我们场景运用机器人场景普通通话场景 前言 顶顶通呼叫中心中间件限制通话时长有两种写法&#xff0c;分别作用于机器人场景与普通通话场景。 普通场景可分为分机互打、分机外呼手机等。 联系我们 有意向了解呼叫中心中间件的用户&#xff0c;可以点击该链接…

SAP Build 2-PDF数据提取与决策

0. 安装desktop agent 在后续过程中发现要预先安装desktop agent&#xff0c;否则没法运行自动化流程… 0.1 agent下载 参考官方文档说明 https://help.sap.com/docs/build-process-automation/sap-build-process-automation/create-user-in-rbsc-download-repository?loca…

RabbitMQ安装配置,封装工具类,发送消息及监听

1. Get-Started docker安装rabbitmq 拉取镜像 [rootheima ~]# docker pull rabbitmq:3.8-management 3.8-management: Pulling from library/rabbitmq 7b1a6ab2e44d: Pull complete 37f453d83d8f: Pull complete e64e769bc4fd: Pull complete c288a913222f: Pull complet…

C# Winform Chart图表使用和详解

Chart控件是微软自带的一种图形可视化组件&#xff0c;能展示种类丰富的图表形式。如曲线图&#xff0c;折线图&#xff0c;饼状图&#xff0c;环形图&#xff0c;柱状图&#xff0c;曲线面积图。 实例代码链接&#xff1a;https://download.csdn.net/download/lvxingzhe3/8943…

【wiki知识库】06.文档管理接口的实现--SpringBoot后端部分

目录 一、&#x1f525;今日目标 二、&#x1f388;SpringBoot部分类的添加 1.调用MybatisGenerator 2.添加DocSaveParam 3.添加DocQueryVo 三、&#x1f686;后端新增接口 3.1添加DocController 3.1.1 /all/{ebokId} 3.1.2 /doc/save 3.1.3 /doc/delete/{idStr} …

[Qt] Qt Creator 以及 Qt 在线安装教程

一、Qt Creator 下载及安装 1、从以下镜像源下载安装包常规安装即可 Qt Creator 也可以在第二步Qt 在线安装时一次性勾选安装&#xff0c;见后文 Qt Creator 中科大源下载地址 二、Qt 在线安装 1、根据所在平台选择对应的安装器下载 Qt 在线安装器下载 2、可能的安装报错…

云电脑有多好用?适合哪些人使用?

云电脑作为一种新型的计算模式&#xff0c;其应用场景广泛且多样&#xff0c;适合各类人群使用。云电脑适合什么人群使用&#xff1f;云电脑有哪些应用场景&#xff1f;有什么好的云电脑推荐&#xff1f;以下本文将详细探讨云电脑的主要应用场景及其适用人群的相关内容&#xf…

禁用PS/Photoshop等一系列Adobe旗下软件联网外传用户数据操作

方案一&#xff1a; 下载火绒杀毒&#xff0c;在联网请求上禁用Adobe软件的联网请求&#xff0c;甚至还可以额外发现哪些是它要想要偷偷摸摸干的。 方案二&#xff1a; 最后注意&#xff1a; 用盗版软件只是获得了使用权&#xff01;

Docker系列.Docker Desktop中如何启用Kubernetes

Docker技术概论 Docker Desktop中如何启用Kubernetes - 文章信息 - Author: 李俊才 (jcLee95) Visit me at CSDN: https://jclee95.blog.csdn.netMy WebSite&#xff1a;http://thispage.tech/Email: 291148484163.com. Shenzhen ChinaAddress of this article:https://blog.…

Linux编辑器 vim使用 (解决普通用户无法进行sudo提权问题)

文章目录 一.vim是什么命令模式底行模式 二.关于vim暂停问题三.注释批量化注释批量化去注释 四.解决普通用户无法进行sudo提权问题五.vim的配置 一.vim是什么 用过VS的都知道&#xff0c;拥有着编辑器编译器调试.编写C&#xff0c;C&#xff0c;python等的功能。就是集成 Linu…

LeetCode | 434.字符串中的单词数

这道题直接使用语言内置的 split 函数可直接分离出字符串中的每个单词&#xff0c;但是要注意区分两种情况&#xff1a;1、空串&#xff1b;2、多个空格连续&#xff0c;分割后会出现空字符的情况&#xff0c;应该舍弃 class Solution(object):def countSegments(self, s):&qu…

Dubbo3 服务原生支持 http 访问,兼具高性能与易用性

作者&#xff1a;刘军 作为一款 rpc 框架&#xff0c;Dubbo 的优势是后端服务的高性能的通信、面向接口的易用性&#xff0c;而它带来的弊端则是 rpc 接口的测试与前端流量接入成本较高&#xff0c;我们需要专门的工具或协议转换才能实现后端服务调用。这个现状在 Dubbo3 中得…

SVN 报错Error: Unable to connect to a repository at URL解决方法

1. 报错背景&#xff1a; 使用ssh 用svn拉取仓库代码时&#xff0c;出现如下报错&#xff1a; Can’t create session: Unable to connect to a repository at URL svn://127.0.0.1 …. Can’t connect to host ‘127.0.0.1’: Connection refused at C:/Program Files/Git/mi…

蓝牙耳机怎么连接电脑?轻松实现无线连接

蓝牙耳机已经成为许多人生活中不可或缺的一部分&#xff0c;不仅可以方便地连接手机&#xff0c;还能轻松连接电脑&#xff0c;让我们在工作和娱乐时享受无线的自由。然而&#xff0c;对于一些用户来说&#xff0c;将蓝牙耳机与电脑连接可能会遇到一些问题。本文将介绍蓝牙耳机…

从大型语言模型到大脑语言理解:探索话语理解的神经机制

随着科技的飞速发展&#xff0c;人工智能领域取得了令人瞩目的成就。在这其中&#xff0c;大型语言模型&#xff08;LLMs&#xff09;以其卓越的性能和广泛的应用前景&#xff0c;成为了当前研究的热点。然而&#xff0c;尽管LLMs在文本生成、语言翻译等领域展现出了惊人的能力…

镭速如何做到数据同步文件及文件夹的ACL属性?

数据文件同步时&#xff0c;除了要同步文件的内容&#xff0c;还要对文件的属性做同步。权限属性作为一个重要的文件属性&#xff0c;是属性同步的重中之重&#xff0c;控制着不同用户与用户组对文件和文件夹的访问权限。不同的操作系统有着自己不同的权限控制机制&#xff0c;…

第2章 Rust初体验6/8:Option枚举及其变体:能避免空指针异常问题:猜骰子冷热游戏

讲动人的故事,写懂人的代码 2.6 故事4: 一直让玩家不断猜 我们全班要一起用三种语言来写第4个故事啦。这可能是我们所有故事中最复杂的一个了。不过别担心,贾克强已经把这个故事的需求都用投影仪展示出来了。 程序会提示玩家猜两个骰子的点数之和。如果玩家第一次输入点数之…

byzer 笔记总结

1.总览&#xff08;简单了解&#xff09; 1.1 数据挖掘的定义 基于大数据技术&#xff0c;针对有价值是业务场景&#xff0c;对数据中台沉淀的大量数据进行探索&#xff0c;分析。寻找数据与数据之间潜藏的关系&#xff0c;转化为自动化的算法模型&#xff0c;从而获取有价值的…