Flink作业执行之 3.StreamGraph

news2025/7/11 12:35:13

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,然后再生成StreamGraph
    return 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() {
    // 先实例化一个空的StreamGraph
    streamGraph = new StreamGraph(executionConfig, checkpointConfig, savepointRestoreSettings);
    // ...

    for (Transformation<?> transformation : transformations) {
        // 依次处理transformation
        transform(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);
    // 将节点添加到map
    streamNodes.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, // 上游节点id
        Integer downStreamVertexID, // 当前节点id
        int 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;
        // 上游(虚拟)节点的父节点id
        upStreamVertexID = virtualPartitionNodes.get(virtualId).f0;
        if (partitioner == null) {
            // 获取了虚拟节点的partitioner
            partitioner = virtualPartitionNodes.get(virtualId).f1;
        }
        // 获取了虚拟节点的数据exchangeMode
        exchangeMode = 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=2
    final Transformation<?> input = ...
    List<Integer> resultIds = new ArrayList<>();
    StreamExchangeMode exchangeMode = ...;

    for (Integer inputId : context.getStreamNodeIds(input)) {
        // 当前作业中已生成5个Transformation实例,因此下一个自增id为6
        final int virtualId = Transformation.getNewNodeId();
        // 加入虚拟节点集合中
        streamGraph.addVirtualPartitionNode(
                // inputId即上游id=2,virtualId=6
                inputId, 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的id
        if (partitioner == null) {
            // 获取了虚拟节点的partitioner
            partitioner = virtualPartitionNodes.get(virtualId).f1;
        }
        // 获取了虚拟节点的exchangeMode
        exchangeMode = virtualPartitionNodes.get(virtualId).f2;
        // 递归调用时,PartitionTransformation从上下游中消失了,仅仅从PartitionTransformation中获取了partitioner和exchangeMode信息。
        addEdgeInternal(
                upStreamVertexID, // 2
                downStreamVertexID, // 4
                typeNumber,
                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.coloradmin.cn/o/1822006.html

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

相关文章

Electron无感打印 静默打印(vue3 + ts + vite)

&#xff08;electron vue3 项目搭建部分 自行查找其他资源 本文只讲解Electronvue3 如何实现静默打印&#xff09; 第一步获取打印机资源 渲染端代码&#xff08;vue里面&#xff09; // 因使用了vite所以在浏览器中打开 require会报错 只能在electron中 const { ipcRender…

JS实现文字溢出隐藏效果

需求场景 由于项目原因&#xff0c;经常需要使用到canvas来将dom生成为图片供用户保存&#xff0c;但canvas的css属性&#xff08;例如本文实现的文字溢出隐藏效果&#xff09;支持并不全面&#xff0c;所有有些功能只能用JS来实现了 实现思路 用JS循环判断填充文本后的元素…

Ollama在windows上的设置

下载 Download Ollama on macOS 安装&#xff1a;是不可以选择安装路径&#xff0c;系统自动运行&#xff0c;不启动模型不占用GPU 参数设置&#xff1a;windows添加环境变量&#xff08;需要重启ollama&#xff09; 修改模型位置&#xff1a;添加 OLLAMA_MODELS D:\LLM\Oll…

【C++】模板及模板的特化

目录 一&#xff0c;模板 1&#xff0c;函数模板 什么是函数模板 函数模板原理 函数模板的实例化 推演(隐式)实例化 显示实例化 模板的参数的匹配原则 2&#xff0c;类模板 什么是类模板 类模板的实例化 二&#xff0c;模板的特化 1&#xff0c;类模板的特化 全特化…

基于uni-app与图鸟UI打造的各领域移动端模板大赏

随着移动互联网的迅猛发展&#xff0c;各类移动端应用层出不穷&#xff0c;为了帮助企业快速搭建高效、美观的移动平台&#xff0c;我们基于强大的uni-app与图鸟UI&#xff0c;精心打造了不下于40套覆盖多个领域的移动端模板。今天&#xff0c;就让我们一起领略这些模板的风采吧…

django-vue-admin 本地部署

一、项目地址 主分支&#xff1a;master&#xff08;稳定版本&#xff09; 开发分支&#xff1a;develop django-vue3-admin-masterhttps://gitee.com/huge-dream/django-vue3-admin 注意&#xff1a;下载master分支zip代码包&#xff0c;解压后删掉web\src\views\syst…

185.二叉树:二叉搜索树的最近公共祖先(力扣)

代码解决 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/class Solution { public:// 函数用于寻找二叉搜索树中节点 p 和 q 的最低…

小程序外卖开发中的关键技术与实现方法

小程序外卖服务凭借其便捷性和灵活性&#xff0c;正成为现代餐饮行业的重要组成部分。开发一个功能完善的小程序外卖系统&#xff0c;需要掌握一系列关键技术和实现方法。本文将介绍小程序外卖开发中的核心技术&#xff0c;并提供具体的代码示例&#xff0c;帮助开发者理解和实…

慎投!4区SCISSCI有停刊风险!网传的水刊之王解析大全,真的好投吗?

本周投稿推荐 SSCI • 中科院2区&#xff0c;6.0-7.0&#xff08;录用友好&#xff09; EI • 各领域沾边均可&#xff08;2天录用&#xff09; CNKI • 7天录用-检索&#xff08;急录友好&#xff09; SCI&EI • 4区生物医学类&#xff0c;0.5-1.0&#xff08;录用…

JDBC操作数据库的方法

目录 一、JDBC介绍 二、使用方法&#xff08;以MySQL为例&#xff09; &#xff08;1&#xff09;MySQL的jar包&#xff0c;导入到IDEA &#xff08;2&#xff09;使用代码&#xff0c;操作数据库 1&#xff09;设置数据源 1.创建MysqlDataSource对象&#xff0c;使用set…

【Unity每日一记】FairyGUI为什么能自动生成代码,它的好处是什么

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

GPT办公与科研应用、论文撰写、数据分析、机器学习、深度学习及AI绘图高级应用

原文链接&#xff1a;GPT办公与科研应用、论文撰写、数据分析、机器学习、深度学习及AI绘图高级应用https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247606667&idx3&sn2c5be84dfcd62d748f77b10a731d809d&chksmfa82606ccdf5e97ad1a2a86662c75794033d8e2e…

mmap引起的内存泄漏分析

最近遇到一个内存泄漏问题&#xff0c;由于问题出现在客户端&#xff0c;只能通过客户提供的Log来分析。 根据客户提供的/proc/meminfo数据发现&#xff0c;MemAvailable 由294072kB减小至18128kB&#xff0c;减小约269MB&#xff0c;引起该变化的最直接原因是PageTables由614…

Python云实例初始化和配置的工具库之cloud-init使用详解

概要 在云计算环境中,自动化配置和管理实例是非常重要的任务。cloud-init 是一个用于云实例初始化和配置的工具,广泛应用于各种云服务提供商(如 AWS、Azure、GCP 等)的实例启动过程。通过 cloud-init,用户可以在实例启动时自动执行脚本、安装软件包、配置网络等。本文将详…

20240613解决飞凌的OK3588-C的核心板的适配以太网RTL8211F-CG

20240613解决飞凌的OK3588-C的核心板的适配以太网RTL8211F-CG 2024/6/13 16:58 缘起&#xff1a;对于飞凌的OK3588-C的核心板&#xff0c;参照飞凌的底板/开发板。 ETH0空接&#xff0c;ETH1由RTL8211FSI-CG【20&#xffe5;】更换为RTL8211F-CG【4&#xffe5;】。 都是千兆网…

快速掌握 Python requests 库发送 JSON 数据的 POST 请求技巧

在现代 Web 开发中&#xff0c;客户端与服务器之间进行数据交换的需求越来越普遍。而在 Python 这个强大的编程语言中&#xff0c;requests 库是一个广泛使用且功能强大的 HTTP 请求库。特别是在进行 API 调用时&#xff0c;发送 POST 请求并附带 JSON 数据是一个非常常见的需求…

【React】useMemo

什么是 useMemo&#xff1f; useMemo 是 React 中的一个 Hook&#xff0c;它可以用来缓存计算结果&#xff0c;并在后续的渲染中重复利用这些计算结果。useMemo 接收两个参数&#xff1a;一个函数和一个依赖数组。当依赖数组中的任何一个值发生变化时&#xff0c;useMemo 会重新…

内网环境实现maven项目打包部署(包括踩坑!!)

内网环境实现maven项目打包部署&#xff08;包括踩坑&#xff01;&#xff01;&#xff09; 由于内网保密项目原因 拿我本地测试 过程&#xff1a; jdk1.8 java环境 maven setting配置加上 <!-- 仓库地址(本地上传到内网环境仓库) --> <localRepository>D:\ma…

flutter开发实战-创建一个微光加载效果

flutter开发实战-创建一个微光加载效果 当加载数据的时候&#xff0c;loading是必不可少的。从用户体验&#xff08;UX&#xff09;的角度来看&#xff0c;最重要的是向用户展示加载正在进行。向用户传达数据正在加载的一种流行方法是在与正在加载的内容类型近似的形状上显示带…

Python业务规则引擎库之rules使用详解

概要 在软件开发中,业务规则引擎是一种重要的工具,可以帮助开发者将复杂的业务逻辑从代码中解耦出来,并以更直观的方式进行管理和维护。rules 是一个轻量级的 Python 库,专门用于定义和执行业务规则。它提供了一种简洁且强大的方式来管理应用程序中的规则逻辑,使代码更加…