【Flink】详解Flink的八种分区

news2024/11/28 18:52:17

简介

Flink是一个流处理框架,一个Flink-Job由多个Task/算子构成,逻辑层面构成一个链条,同时Flink支持并行操作,每一个并行度可以理解为一个数据管道称之为SubTask。我们画图来看一下:

Flink-job

数据会在多个算子的SubTask之间相互传递,算子之间的并行度可能是不同的,这样就产生了数据分区问题,其核心问题在于上游的某个SubTask的数据该发送到下游的哪一个SubTask中。为了解决分区相关问题,Flink提供了一系列分区算子,下面将详细为大家介绍分区算子和相关的分区器。

分区算子

Flink一共有6种(rescale和rebalance都是轮询算子)或者7种分区算子:

  • shuffle :调用shuffle方法将会随机分配,总体上服从均匀分布;
  • rebalance:调用rebalance方法将会轮询分配,对所有的并⾏⼦任务进⾏轮询分配,可能会导致TM之间的数据交换;
  • rescale:调用rescale方法将会以组为单位轮训分配,而不是整体进行轮训,为了避免TM之间的数据交互;
  • broadcast:调用broadcast方法将数据流广播给所有的下游子任务;
  • global:调用global方法将会进行全局分区,将上游所有数据发送到下游第一个分区中;
  • keyby:调用keyby方法将会按键分区。
  • 自定义规则:自定义数据分发策略。代表算子为partitionCustom。

分区器

概述

每一个分区算子的底层实际上对应一个分区器,一共8个分区器

  • GlobalPartitioner
  • ShufflePartitioner
  • RebalancePartitioner
  • RescalePartitioner
  • BroadcastPartitioner
  • ForwardPartitioner
  • KeyGroupStreamPartitioner
  • CustomPartitionerWrapper

各个分区器的继承关系如下:

在这里插入图片描述

接下来将详细介绍每一个分区算子和对应的分区器。

ChannelSelector

ChannelSelector是分区器共同实现的接口,定义分区器的基本行为。

public interface ChannelSelector<T extends IOReadableWritable> {

    // 初始化ChannelSelector,传入的参数为下游channel的数量
    void setup(int numberOfChannels);

    // 返回选择的channel索引编号,这个方法决定的上游的数据需要写入到哪个channel中
    // 这个方法的Partitioner子类重点需要实现的方法
    // 对于broadcast广播类型算子,不需要实现这个方法
    // 尽管broadcast不需要实现这个方法,但是还是重写了方法,throw new UnsupportedOperationException
    // 传入的参数为记录数据流中的元素,该方法需要根据元素来推断出需要发送到的下游channel
    int selectChannel(T record);

    // 返回是否为广播类型
    boolean isBroadcast();
}

StreamPartitioner

StreamPartitioner抽象类实现了StreamPartitioner接口,它的代码如下所示:

public abstract class StreamPartitioner<T>
        implements ChannelSelector<SerializationDelegate<StreamRecord<T>>>, Serializable {
    private static final long serialVersionUID = 1L;
	
    // 下游的channel数量
    protected int numberOfChannels;
	
    // 初始化的时候就知道下游的channel数量
    @Override
    public void setup(int numberOfChannels) {
        this.numberOfChannels = numberOfChannels;
    }
	
    // 肯定不是广播类型
    @Override
    public boolean isBroadcast() {
        return false;
    }

    public abstract StreamPartitioner<T> copy();

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        final StreamPartitioner<?> that = (StreamPartitioner<?>) o;
        return numberOfChannels == that.numberOfChannels;
    }

    @Override
    public int hashCode() {
        return Objects.hash(numberOfChannels);
    }

    // 决定了作业恢复时候上游遇到扩缩容的话,需要处理哪些上游状态保存的数据
    public SubtaskStateMapper getUpstreamSubtaskStateMapper() {
        return SubtaskStateMapper.ARBITRARY;
    }

    // 决定了作业恢复时候下游遇到扩缩容的话,需要处理哪些下游状态保存的数据
    public abstract SubtaskStateMapper getDownstreamSubtaskStateMapper();
	
    // 该方法定义了上下游之间的关系类型,如果返回True,表示上下游SubTask之间有明确的一一对应关系,如果返回False代表上下游SubTask之间没有明确的对应关系
    public abstract boolean isPointwise();
}

ShufflePartitioner

@PublicEvolving
public DataStream<T> shuffle() {
	return setConnectionType(new ShufflePartitioner<T>());
}

可以看到shuffle算子对应的分区器是【ShufflePartitioner】。

public class ShufflePartitioner<T> extends StreamPartitioner<T> {
    private static final long serialVersionUID = 1L;

    private Random random = new Random();
	
    // 重要
    // 随机返回一个下游Channel,由于random.nextInt符合均匀分布,所以shuffle的数据分布也符合均匀分布
    @Override
    public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        return random.nextInt(numberOfChannels);
    }

    @Override
    public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
        return SubtaskStateMapper.ROUND_ROBIN;
    }

    @Override
    public StreamPartitioner<T> copy() {
        return new ShufflePartitioner<T>();
    }
	
    // ShufflePartitioner上下游Subtask之间没有明确对应关系
    @Override
    public boolean isPointwise() {
        return false;
    }

    @Override
    public String toString() {
        return "SHUFFLE";
    }
}

图例

shuffle

GlobalPartitioner

public DataStream<T> global() {
	return setConnectionType(new GlobalPartitioner<T>());
}

可以看到global对应的分区器是【GlobalPartitioner】。

public class GlobalPartitioner<T> extends StreamPartitioner<T> {
    private static final long serialVersionUID = 1L;
	
    // 数据永远发往下游第一个SubTask。
    @Override
    public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        return 0;
    }

    @Override
    public StreamPartitioner<T> copy() {
        return this;
    }
	
    // 恢复任务的时候将会恢复到第一个任务。
    @Override
    public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
        return SubtaskStateMapper.FIRST;
    }
	
    // ShufflePartitioner上下游Subtask之间没有明确对应关系
    @Override
    public boolean isPointwise() {
        return false;
    }

    @Override
    public String toString() {
        return "GLOBAL";
    }
}

图例

在这里插入图片描述

ForwardPartitioner

public class ForwardPartitioner<T> extends StreamPartitioner<T> {
    private static final long serialVersionUID = 1L;
	
    // 还是发往下游第一个SubTask,不同的是这里的下游SubTask是在本地的。
    @Override
    public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        return 0;
    }

    public StreamPartitioner<T> copy() {
        return this;
    }
	
    // 上下游SubTask是一一对应的,如果上下游算子并行度不一致就会报错
    @Override
    public boolean isPointwise() {
        return true;
    }

    @Override
    public String toString() {
        return "FORWARD";
    }

    @Override
    public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
        return SubtaskStateMapper.UNSUPPORTED;
    }

    @Override
    public SubtaskStateMapper getUpstreamSubtaskStateMapper() {
        return SubtaskStateMapper.UNSUPPORTED;
    }
}

ForwardPartitionerStreamGraphaddEdgeInternal方法中自动创建(生成StreamGraph的过程),代码片段如下所示:

// ...
if (partitioner == null
        && upstreamNode.getParallelism() == downstreamNode.getParallelism()) {
    // 只有在上游和下游的并行度相同且没有指定相关分区器的时候,才会使用ForwardPartitioner
    partitioner = new ForwardPartitioner<Object>();
} else if (partitioner == null) {
    // 否 则使用RebalancePartitioner
    partitioner = new RebalancePartitioner<Object>();
}

// 这里还会再次检测上游和下游的并行度是否一致
// 防止用户强行指定使用ForwardPartitioner时候上下游的并行度不一致
if (partitioner instanceof ForwardPartitioner) {
    if (upstreamNode.getParallelism() != downstreamNode.getParallelism()) {
        throw new UnsupportedOperationException(
                "Forward partitioning does not allow "
                        + "change of parallelism. Upstream operation: "
                        + upstreamNode
                        + " parallelism: "
                        + upstreamNode.getParallelism()
                        + ", downstream operation: "
                        + downstreamNode
                        + " parallelism: "
                        + downstreamNode.getParallelism()
                        + " You must use another partitioning strategy, such as broadcast, rebalance, shuffle or global.");
    }
}
// ...

或者调用forward算子创建,这个方法基本不使用。

public DataStream<T> forward() {
    return setConnectionType(new ForwardPartitioner<T>());
}

图例

在这里插入图片描述

RebalancePartitioner

public DataStream<T> rebalance() {
	return setConnectionType(new RebalancePartitioner<T>());
}

可以看到rebalance对应的分区器是【RebalancePartitioner】。

public class RebalancePartitioner<T> extends StreamPartitioner<T> {
    private static final long serialVersionUID = 1L;
	
    // 记录要接受数据的下游Channel编号
    private int nextChannelToSendTo;

    @Override
    public void setup(int numberOfChannels) {
        super.setup(numberOfChannels);

        nextChannelToSendTo = ThreadLocalRandom.current().nextInt(numberOfChannels);
    }
	
    // 采用取余的方式找出发送的下游channel
    @Override
    public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        nextChannelToSendTo = (nextChannelToSendTo + 1) % numberOfChannels;
        return nextChannelToSendTo;
    }
	
    // 恢复的时候将保存数据轮询发送
    @Override
    public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
        return SubtaskStateMapper.ROUND_ROBIN;
    }

    public StreamPartitioner<T> copy() {
        return this;
    }
 	
    // 上下游SubTask之间没有意义对应关系
    @Override
    public boolean isPointwise() {
        return false;
    }

    @Override
    public String toString() {
        return "REBALANCE";
    }
}

图例

在这里插入图片描述

RescalePartitioner

public DataStream<T> rescale() {
	return setConnectionType(new RescalePartitioner<T>());
}

可以看到rescale对应的分区器是【RescalePartitioner】。跟rebalance不同,例如上游并行度是2,下游是4,则上游一个并行度以循环的方式将记录输出到下游的两个并行度上;上游另一个并行度以循环的方式将记录输出到下游另两个并行度上。如果上游并行度是4,下游并行度是2,则上游两个并行度将记录输出到下游一个并行度上;上游另两个并行度将记录输出到下游另一个并行度上。(可以理解是一种负载均衡的轮询

public class RescalePartitioner<T> extends StreamPartitioner<T> {
    private static final long serialVersionUID = 1L;

    private int nextChannelToSendTo = -1;
	
    // 采用的方式和rebalance一致,都是轮询的策略
    @Override
    public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        if (++nextChannelToSendTo >= numberOfChannels) {
            nextChannelToSendTo = 0;
        }
        return nextChannelToSendTo;
    }
	
    // 恢复的时候不支持扩缩容,因为原本的对应关系已经被破坏了
    @Override
    public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
        return SubtaskStateMapper.UNSUPPORTED;
    }
	
    // 恢复的时候不支持扩缩容,因为原本的对应关系已经被破坏了
    @Override
    public SubtaskStateMapper getUpstreamSubtaskStateMapper() {
        return SubtaskStateMapper.UNSUPPORTED;
    }

    public StreamPartitioner<T> copy() {
        return this;
    }

    @Override
    public String toString() {
        return "RESCALE";
    }
	
    // 这是有一一对应关系的分区方式
    @Override
    public boolean isPointwise() {
        return true;
    }
}

图例

在这里插入图片描述

KeyGroupPartitioner

public <K> KeyedStream<T, K> keyBy(KeySelector<T, K> key) {
	Preconditions.checkNotNull(key);
	return new KeyedStream<>(this, clean(key));
}

// 调用keyby返回一个KeyedStream
// 在KeyedStream底层用一个PartitionTransformation包装了KeyGroupStreamPartitioner(键提取器,和默认最大键组数)
// 
 public KeyedStream(
            DataStream<T> dataStream,
            KeySelector<T, KEY> keySelector,
            TypeInformation<KEY> keyType) {
        this(
                dataStream,
                new PartitionTransformation<>(
                        dataStream.getTransformation(),
                        new KeyGroupStreamPartitioner<>(
                                keySelector,
                                StreamGraphGenerator.DEFAULT_LOWER_BOUND_MAX_PARALLELISM)),
                keySelector,
                keyType);
    }

以下是【KeyGroupStreamPartitioner】的源码分析

public class KeyGroupStreamPartitioner<T, K> extends StreamPartitioner<T>
        implements ConfigurableStreamPartitioner {
    private static final long serialVersionUID = 1L;

    private final KeySelector<T, K> keySelector;

    private int maxParallelism;

    @Override
    public int selectChannel(SerializationDelegate<StreamRecord<T>> record) {
        K key;
        try {
            // 通过keySelector获取键
            key = keySelector.getKey(record.getInstance().getValue());
        } catch (Exception e) {
            throw new RuntimeException(
                    "Could not extract key from " + record.getInstance().getValue(), e);
        }
        // 
        return KeyGroupRangeAssignment.assignKeyToParallelOperator(
                key, maxParallelism, numberOfChannels);
    }

    @Override
    public SubtaskStateMapper getDownstreamSubtaskStateMapper() {
        return SubtaskStateMapper.RANGE;
    }
	
    // 上下游SubTask没有一一对应关系
    @Override
    public boolean isPointwise() {
        return false;
    }
	
    // 这里是检查是否配置了最大并行度(最大建组数),如果有配置则替代默认值
    @Override
    public void configure(int maxParallelism) {
        KeyGroupRangeAssignment.checkParallelismPreconditions(maxParallelism);
        this.maxParallelism = maxParallelism;
    }
}

// 包装了一层检查一下键是否是null
// key:键;
// maxParallelis:支持的最大并行度,也就是键组的数量
// parallelism:当前并行度
public static int assignKeyToParallelOperator(Object key, int maxParallelism, int parallelism) {
	Preconditions.checkNotNull(key, "Assigned key must not be null!");
	return computeOperatorIndexForKeyGroup(maxParallelism, parallelism, assignToKeyGroup(key, maxParallelism));
}

// 分配键组
// key:键;
// maxParallelis:支持的最大并行度,也就是键组的数量
public static int assignToKeyGroup(Object key, int maxParallelism) {
    Preconditions.checkNotNull(key, "Assigned key must not be null!");
	return computeKeyGroupForKeyHash(key.hashCode(), maxParallelism);
}

// 通过键组ID*当前并行度/最大键组数量默认128来分配数据流向的channel
// maxParallelis:支持的最大并行度,也就是键组的数量
// parallelism:当前并行度
// keyGroupId:键组ID
public static int computeOperatorIndexForKeyGroup(int maxParallelism, int parallelism, int keyGroupId) {
	return keyGroupId * parallelism / maxParallelism;
}

图例

在这里插入图片描述

Flink如何使用分区器

Flink通过RecordWriter向下游写入输入。RecordWriter通过RecordWriterBuilder创建。

public RecordWriter<T> build(ResultPartitionWriter writer) {
    if (selector.isBroadcast()) {
        return new BroadcastRecordWriter<>(writer, timeout, taskName);
    } else {
        return new ChannelSelectorRecordWriter<>(writer, selector, timeout, taskName);
    }
}

build方法中会调用【selector】的isBroadcast方法,如果是广播类型,则创建【BroadcastRecordWriter】对象来写数据,否则创建【ChannelSelectorRecordWriter】对象来写数据。

以下是【BroadcastRecordWriter】对象的源码分析:

public final class BroadcastRecordWriter<T extends IOReadableWritable> extends RecordWriter<T> {

	broadcastEmit方法
    // writer都是调用emit方法,在BroadcastRecordWriter中进行了包装,实质调用的是broadcastEmit方法
    @Override
    public void emit(T record) throws IOException {
        broadcastEmit(record);
    }

    @Override
    public void broadcastEmit(T record) throws IOException {
        // 检查
        checkErroneous();
		// 先使用序列化器将数据序列化,然后进行广播
        targetPartition.broadcastRecord(serializeRecord(serializer, record));

        if (flushAlways) {
            flushAll();
        }
    }
}

以下是【ChannelSelectorRecordWriter】对象源码分析:

public final class ChannelSelectorRecordWriter<T extends IOReadableWritable>
        extends RecordWriter<T> {

    private final ChannelSelector<T> channelSelector;

    @Override
    public void emit(T record) throws IOException {
        // 分区器根据当前记录计算出下游Subtask的索引,然后发送
        emit(record, channelSelector.selectChannel(record));
    }
    
    protected void emit(T record, int targetSubpartition) throws IOException {
        checkErroneous();
		
        // 先进行序列化操作
        // targetSubpartition就是上一步中分区器计算的SubTask索引
        targetPartition.emitRecord(serializeRecord(serializer, record), targetSubpartition);

        if (flushAlways) {
            targetPartition.flush(targetSubpartition);
        }
    }
}

总结

  1. Flink本身提供了多种分区API,在底层使用的都是分区器,Flink一般提供了7种分区器;
  2. 按键分区本质上是按键组分区,通过分配键组的方式分配键;
  3. rescale本地轮流分配)和rebalance轮流分配)有区别,前者考虑了TM之间数据传输的问题,可以理解是一种软负载均衡的轮询;

往期回顾

  1. 【Flink】浅谈Flink背压问题(1)
  2. 【分布式】浅谈CAP、BASE理论(1)

文中难免会出现一些描述不当之处(尽管我已反复检查多次),欢迎在留言区指正,列表相关的知识点也可进行分享。

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

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

相关文章

【奇妙的数据结构世界】用图像和代码对链表的使用进行透彻学习 | C++

第九章 链表 目录 第九章 链表 ●前言 ●一、链表是什么&#xff1f; 1.简要介绍 2.具体情况 ●二、链表操作的关键代码段 1.类型定义 2.常用操作 ●总结 前言 简单来说&#xff0c;数据结构是一种辅助程序设计并且进行优化的方法论&#xff0c;它不仅讨论数据的存储与处…

打工人必知必会(一)——规章制度保险劳动合同变更

目录 参考 1、规章制度的生效要件 2、工资的发放形式 3、社会保险的基本规定 4、基本养老保险 5、医疗保险、失业保险、工伤保险、生育保险 6、劳动合同的变更 第一节 协商变更劳动合同 第二节 单方变更劳动合同 参考 《HR全程法律顾问&#xff1a;企业人力资源管理高…

5-6中央处理器-多处理器系统硬件多线程

文章目录一.多处理器系统&#xff08;一&#xff09;计算机体系结构分类1.单指令单数据流SISD2.单指令多数据流SIMD3.多指令单数据流MISD4.多指令多数据流MIMD&#xff08;1&#xff09;(共享内存)多处理器系统/多核处理器&#xff08;2&#xff09;多计算机系统&#xff08;二…

逆水寒魔兽老兵服副本攻略及代码分析(英雄武林风云录,后续更新舞阳城、扬州、清明等副本攻略)

文章目录一、武林风云录1&#xff09;老一&#xff1a;陈斩槐&#xff08;只有四个机制&#xff0c;dps压力不大&#xff0c;留爆发打影子就行&#xff09;&#xff08;1&#xff09;点名红色扇形区域&#xff08;2&#xff09;点名红色长条&#xff0c;注意最后还有一段大劈&a…

MongoDB入门(特点,使用场景,命令行操作,SpringData-MongoDB)

今天我们将通过这一篇博客来了解MongoDB的体系结构&#xff0c;命令行操作和在JAVA 当中使用SpringData-MongoDB 来 操作MongoDB。 如果没有安装的小伙伴 可以看一下 这篇文章 (59条消息) 开源的文档型数据库–MongoDB&#xff08;安装&#xff09;_一切总会归于平淡的博客-CS…

LeetCode[128]最长连续序列

难度&#xff1a;中等题目&#xff1a;给定一个未排序的整数数组 nums&#xff0c;找出数字连续的最长序列&#xff08;不要求序列元素在原数组中连续&#xff09;的长度。请你设计并实现时间复杂度为 O(n)的算法解决此问题。示例 1&#xff1a;输入&#xff1a;nums [100,4,2…

java中new的含义如何理解?

在Java中&#xff0c;new关键字被使用来创建一个新的对象&#xff0c;可以理解为创建的意思。使用关键字new来创建一个对象也叫类的实例化&#xff0c;使用new创建对象时&#xff0c;会调用构造方法初始化对象声明对象Cat cat 在栈内存中实例化对象 new Cat(参数); 在堆内存中每…

C++:类的static成员,友元和构造函数初始化列表

目录 一.类的构造函数的初始化列表 1.类的构造函数初始化列表的引入和介绍 2.初始化列表用于类的类对象成员的拷贝构造函数的调用 3.初始化列表的使用细则 4.使用初始化列表的一个注意事项 二.explicit关键字 三.C类的static成员 1.类中static修饰的成员变量 2.类中st…

Lesson 4.2 逻辑回归参数估计:极大似然估计、相对熵与交叉熵损失函数

文章目录一、逻辑回归参数估计基本思路1. 构建损失函数2. 损失函数求解二、利用极大似然估计进行参数估计三、熵、相对熵与交叉熵1. 熵&#xff08;entropy&#xff09;的基本概念与计算公式2. 熵的基本性质3. 相对熵&#xff08;relative entropy&#xff09;与交叉熵&#xf…

LeetCode[947]移除最多的同行或同列石头

难度&#xff1a;中等题目&#xff1a;n块石头放置在二维平面中的一些整数坐标点上。每个坐标点上最多只能有一块石头。如果一块石头的 同行或者同列 上有其他石头存在&#xff0c;那么就可以移除这块石头。给你一个长度为 n的数组 stones&#xff0c;其中 stones[i] [xi, yi]…

MATLAB算法实战应用案例精讲-【人工智能】Grover量子搜索算法(补充篇)

前言 因为量子计算的并行性, 搜索问题, 比如说数据库搜索, 最短路径问题, 加密问题, 图形着色问题等, 都被视为可以做到量子加速. Grover 算法,有时也称为量子搜索算法(quantum search algorithm),指一种在量子计算机上运行的非结构化搜索算法,是量子计算的典型算法…

LeetCode[765]情侣牵手

难度&#xff1a;困难题目&#xff1a;n对情侣坐在连续排列的 2n个座位上&#xff0c;想要牵到对方的手。人和座位由一个整数数组 row表示&#xff0c;其中 row[i]是坐在第 i 个座位上的人的 ID。情侣们按顺序编号&#xff0c;第一对是 (0, 1)&#xff0c;第二对是 (2, 3)&…

#A. Balanced Lineup排队(rmq模板题)

题目思路建议先看看详解rmq问题很明显这道题意是跟你一段数列&#xff0c;并给出多次询问,询问区间内最大值和最小值的差。如果去暴力枚举显然会超时,所以要用st算法来解决。我们要建立两个RMQ预处理内容&#xff0c;分别处理最大值和最小值。建一个mx[i][j]代表从i开始,长度为…

精品图表Crack:TeeChart ActiveX version 2023.1

TeeChart ActiveX version 2023 数据可视化专家,Visual Studio.Net、Visual Basic、Visual Studio 6和 IIS / ASP的图表组件 概述 TeeChart Pro ActiveX 图表组件库提供数百种 2D 和 3D 图形样式、56 种数学和统计函数供您选择&#xff0c;以及无限数量的轴和 14 个工具箱组件…

DFS(五)最小轮盘锁

752. 打开转盘锁 你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字&#xff1a; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 。每个拨轮可以自由旋转&#xff1a;例如把 9 变为 0&#xff0c;0 变为 9 。每次旋转都只能旋转一个拨轮的一位数字。 锁的初始数字为 0000 &#xff0c;一…

【2023.01.26】定时执行专家 V6.6 兔年春节版 - 更新日志

目录 ◆ 最新版下载链接 ◆ 软件更新日志 – TimingExecutor Full Change Log ▼ 2023-01-23 V6.6 ▼ 2023-01-20 V6.5 ▼ 2022-12-25 V6.4 ▼ 2022-11-15 V6.3 ▼ 2022-10-01 V6.2 ▼ 2022-07-30 V6.1&#xff08;Build 769.30072022&#xff09; ▼ 2022-0…

Linux-Ubuntu入门到精通之远程操作指令

1️⃣shutdown 2️⃣查看或配置网卡信息 3️⃣网卡和IP地址 4️⃣ifconfig 5️⃣ping 6️⃣远程登录和复制文件 7️⃣ ssh 基础&#xff08;重点&#xff09; 8️⃣域名 和 端口号 9️⃣SSH 客户端的简单使用 1️⃣0️⃣Windows 下 SSH 客户端的安装 Putty &#xff1a;http:/…

数据结构 | 海量数据处理 | 位图和哈希切分的常见应用 | 布隆过滤器的使用场景

文章目录位图应用question 1question 2question 3位图的作用哈希切分布隆过滤器作为一种数据结构&#xff0c;哈希桶有着不同于其他数据结构的思想——直接映射&#xff0c;这使得在哈希结构中查找数据的效率达到了最快的O(1)&#xff0c;比起搜索树的比较数据大小&#xff0c;…

数学建模——降维算法

降维 降维的意义 降低无效、错误数据对建模的影响&#xff0c;提高建模的准确性少量切具有代表性的数据将大幅缩减挖掘所需的时间降低存储数据的成本 需要降维的情况 维度灾难。很难有一个简洁的模型在高维空间中依旧具有鲁棒性&#xff0c;而随着模型复杂度的增加&#xf…

【LeetCode每日一题:1663. 具有给定数值的最小字符串~~~递归+DFS+贪心】

题目描述 小写字符 的 数值 是它在字母表中的位置&#xff08;从 1 开始&#xff09;&#xff0c;因此 a 的数值为 1 &#xff0c;b 的数值为 2 &#xff0c;c 的数值为 3 &#xff0c;以此类推。 字符串由若干小写字符组成&#xff0c;字符串的数值 为各字符的数值之和。例如…