网易一面:单节点2000Wtps,Kafka怎么做的?

news2025/1/11 23:49:58

说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如网易、有赞、希音、百度、网易、滴滴的面试资格,遇到一几个很重要的面试题:

问题1:单节点2000Wtps,Kafka高性能原理是什么?

问题2:做过Kafka 进行性能压测吗?单个节点的极限处理能力是多少?是怎么做到的?

注意,单个节点的极限处理能力接近每秒 2000万 条消息,吞吐量达到每秒 600MB

那 Kafka 为什么这么快?如何做到这个高的性能?

尼恩提示,Kafka相关的问题,是开发的核心知识,又是线上的重点难题。

所以,这里尼恩给大家做一下系统化、体系化的线程池梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”

也一并把这个题目以及参考答案,收入咱们的 《尼恩Java面试宝典》V100版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到公号【技术自由圈】获取

这里,主要从这 3 个角度来分析

  • 生产端
  • 服务端 Broker
  • 消费端

先来看下生产端发送消息,Kafka 做了哪些优化?

文章目录

    • 说在前面
    • 一、生产端 Producer
      • 生产者发送消息流程
      • 前置知识:队列缓存+批量写入架构
      • 生产者发送消息流程源码
        • 1. 将消息封装成ProducerRecord对象:
        • 2. Serializer对消息的key和value做序列化:
        • 3. 经过序列化后,会根据自定义的分区器或者Kafka默认的分区器进行获取消息的所属的分区。
        • 4. 获取到消息所属的分区后,消息会被存放到消息缓冲区中(RecordAccumulator)中,
        • 5. 生产者会有一个send线程,用于不断的获取消息和发送消息。
        • 6. 每个请求都会缓存在一个inFlightRequest缓冲区内,里面为每一个Broker分配了一个队列。
        • 7. selector获取到Request会发往相对应的Broker节点。Broker节点收到Request后会进行ACK确认这个Request
        • 8. 当收到Broker对某个Request的ACK后,会删除inFlightRequest队列中这个Request。然后调用clear方法清除对应的ProducerBatch。
        • 9. 如果发送过程中产生了异常,消息发送会存在重试机制。条件为重试次数小于指定值&&异常为RetriableException
      • 生产端的高并发核心架构设计
    • 二、服务端 Broker
      • 1、PageCache 加速消息读写
      • 2、Kafka 的文件布局 以及 磁盘文件顺序写入
      • 3、零拷贝 sendfile:加速消费流程
    • 三、消费端 Consumer
    • 说在最后
    • 参考文献
    • 推荐阅读

一、生产端 Producer

先来回顾下 Producer 生产者发送消息的流程

Kafka的源码最核心的是由client模块和core模块构成,用一幅图大致介绍一下生产者发送消息的流程。

生产者发送消息流程

  1. 将消息封装成ProducerRecord对象
  2. Serializer对消息的key和value做序列化
  3. 根据Partitioner将消息分发到不同的分区,需要先获取集群的元数据
  4. RecordAccumulator封装很多分区的消息队列,每个队列代表一个分区,每个分区里面有很多的批次,每个批次里面由多条消息组成
  5. Sender会从RecordAccumulator拉取消息,封装成批次,发送请求
  6. 通过网络将请求发送到kafka集群

前置知识:队列缓存+批量写入架构

队列缓存+批量写入架构,是尼恩反复强调的一个高并发写入架构,

kafka、netty都用到这个架构

kafka的生产者,也用了这个架构。 设计了一个核心组件RecordAccumulator

  1. RecordAccumulator:每一个是生产上都会维护一个固定大小的内存空间,主要用于合并单条消息,进行批量发送,提高吞吐量,减少带宽消耗。
  2. RecordAccumulator的大小是可配置的,可以配置buffer.memory来修改缓冲区大小,默认值为:33554432(32M)
  3. RecordAccumulator内存结构分为两部分
  • 第一部分为已经使用的内存,这一部分主要存放了很多的队列。每一个主题的每一个分区都会创建一个队列,来存放当前分区下待发送的消息集合。
  • 第二部分为未使用的内存,这一部分分为已经池化后的内存和未池化的整个剩余内存(nonPooledAvailableMemory)。池化的内存的会根据batch.size(默认值为16K)的配置进行池化多个ByteBuffer,放入一个队列中。所有的剩余空间会形成一个未池化的剩余空间。

生产者发送消息流程源码

1. 将消息封装成ProducerRecord对象:

生产者生成某个消息后,ProducerRecord首先会经过一个或多个组成的拦截器链。

2. Serializer对消息的key和value做序列化:

当消息通过所有的拦截器之后,会进行序列化,会根据key和value的序列化配置进行序列化消息内容,生产者和消费者必须使用相同的key-value序列化方式。

// 消息key序列化
properties.setProperty(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());
// 消息value序列化
properties.setProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName());

3. 经过序列化后,会根据自定义的分区器或者Kafka默认的分区器进行获取消息的所属的分区。

自定义分区器可以参考下面。

Kafka默认的分区器规则:

  • 1)当消息的key存在时,首先获取当前topic下的所有分区数,然后对key进行求hash值,根据hash值和分区总数进行取余,获取所属的的分区。
  • 2)如果key不存在时,会根据topic获取一个递增的数值,然后通过和分区数进行取余,获取所属的分区。

Kafka默认分区器源码:

public class DefaultPartitioner implements Partitioner {

    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap<>();

    public void configure(Map<String, ?> configs) {}

    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
        int numPartitions = partitions.size();
        if (keyBytes == null) {
            int nextValue = nextValue(topic);
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return availablePartitions.get(part).partition();
            } else {
                // no partitions are available, give a non-available partition
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            // hash the keyBytes to choose a partition
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }

    private int nextValue(String topic) {
        AtomicInteger counter = topicCounterMap.get(topic);
        if (null == counter) {
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            AtomicInteger currentCounter = topicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }

    public void close() {}

}

自定义分区器:

public class CustomerPartitions implements Partitioner{
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        int partition = 0;
        if(key == null) {
        } else {
            String keyStr = key.toString();
            if(keyStr.contains("Test")) {
                partition = 1;
            } else {
                partition = 2;
            }
        }
        return partition;
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> configs) {

    }
}

4. 获取到消息所属的分区后,消息会被存放到消息缓冲区中(RecordAccumulator)中,

根据topic和分区可以确定一个双端队列(Deque)中,这个队列中每个节点为多个消息的合集(ProducerBatch),新的消息会被放到队列的最后一个节点上,存放会存在多种情况。

场景一:消息大小不足16K。

首先会根据topic和分区获取所属队列的最后一个ProducerBatch,

  • 如果最后一个ProducerBatch+当前消息 <= 16K时,会把当前消息存入这个ProducerBatch中,等待发送。
  • 如果最后一个ProducerBatch+当前消息 > 16K时,此时消息不会放入这个ProducerBatch中,而是会向池化的队列中获取一个ByteBuffer,把这个ByteBuffer放到队列的尾部,然后把消息放入这个新增的ProducerBatch中。
  • 如果最后一个ProducerBatch+当前消息 > 16K时,并且池化的队列中没有可用的ByteBuffer时,池化队列会向剩余的未使用的内存空间(nonPooledAvailableMemory)申请一个大小为16K的内存空间,添加到池化队列尾部。然后把这个新增的ByteBuffer添加到分区下的队列尾部,存储新的消息。

场景二:消息大小超过16K

  • 当消息超过16K时,任何一个ProducerBatch都无法存储这个消息。此时会直接向剩余的空间(nonPooledAvailableMemory)的进行分配和当前的消息大小一样的内存空间,加到队列的尾部,然后存储消息,等待发送。
  • 当剩余的空间(nonPooledAvailableMemory) < 消息大小时,nonPooledAvailableMemory会向池化队列获取空间,每次获取一个ByteBuffer(16K),直到nonPooledAvailableMemory的空间大于或等于消息大小时。获取的ByteBuffer会经过jvm的GC垃圾回收。过程比较慢。当nonPooledAvailableMemory空间大于获取等于消息大小时,会把分配消息大小的空间放入分区队列的尾部,把消息存入这个ProducerBatch内。

5. 生产者会有一个send线程,用于不断的获取消息和发送消息。

sender线程会不断的扫描RecordAccumulator中所有的ProducerBatch,如果ProducerBatch达到batch.size(默认16K)大小或者最早的一个消息已经等待超过linger.ms(默认为0)时,这个ProducerBatch会被sender线程收集到。由于不同的topic和分区会被分到不同的Broker节点上,sender线程会把发送到相同Broker姐节点的ProducerBatch合并在一个Request请求中,一个Request请求不会超过max.request.size(默认1048576B = 1M)

6. 每个请求都会缓存在一个inFlightRequest缓冲区内,里面为每一个Broker分配了一个队列。

新的请求会放在队列尾部,每个队列最多能够容纳max.in.flight.requests.per.connection(默认值为5)个Request,队列满了不会产生新的Request。

7. selector获取到Request会发往相对应的Broker节点。Broker节点收到Request后会进行ACK确认这个Request

acks 有三个配置值:[-1 , 0 , 1]
acks = -1 表示不需要收到leader节点的ACK回复就会发送下一个Request。高吞吐,低一致性
acks = 0 表示只需要接收到leader节点的ACK后就可以发送下一个Request。
acks = 1 表示 需要接收到leaer节点和ISR节点的ACK后才会发送下一个Request。一致性较高

8. 当收到Broker对某个Request的ACK后,会删除inFlightRequest队列中这个Request。然后调用clear方法清除对应的ProducerBatch。

RecordAccumulator Clear清理场景:针对2.4.1.1,2.4.1.2,2.4.1.3,ProducerBatch都会标记为删除,然后放入池化队列中,不会进行GC。2.4.1.3中从nonPooledAvailableMemory获取的内存也不会归还給nonPooledAvailableMemory,任然存放在池化队列中。
针对2.4.2.1,2.4.2.2,超过16K的消息内存空间会被GC进行回收,然后作为nonPooledAvailableMemory的一部分

9. 如果发送过程中产生了异常,消息发送会存在重试机制。条件为重试次数小于指定值&&异常为RetriableException

private boolean canRetry(ProducerBatch batch, ProduceResponse.PartitionResponse response) {
    return batch.attempts() < this.retries &&
        ((response.error.exception() instanceof RetriableException) ||
         (transactionManager != null && transactionManager.canRetry(response, batch)));
}

生产端的高并发核心架构设计

在消息发送时候,可发现这两个亮点**:批量消息自定义协议格式。**

  • 批量发送:减少了与服务端 Broker 处理请求的次数,从而提升总体的处理能力。

调用 send() 方法时,不会立刻把消息发送出去,而是缓存起来,选择恰当时机把缓存里的消息划分成一批数据,按批次发送给服务端 Broker。

  • 自定义协议格式:序列化方式和压缩格式都能减少数据体积,从而节省网络资源消耗。

各种压缩算法对比

  • 吞吐量方面:LZ4 > Snappy > zstd 和 GZIP
  • 压缩比方面:zstd > LZ4 > GZIP > Snappy
Compressor nameRatioCompressionDecompress.
zstd 1.3.4-12.877470 MB/s1380 MB/s
zlib 1.2.11-12.743110 MB/s400 MB/s
brotli 1.0.2-02.701410 MB/s430 MB/s
quicklz 1.5.0-12.238550 MB/s710 MB/s
lzo1x2.09-12.108650 MB/s830 MB/s
lz4 1.8.12.101750 MB/s3700 MB/s
snappy 1.1.42.091530 MB/s1800 MB/s
lzf 3.6-12.077400 MB/s860 MB/s

二、服务端 Broker

Broker 的高性能主要从这 3 个方面体现:

  • PageCache 缓存
  • Kafka 的文件布局 以及 磁盘文件顺序写入
  • 零拷贝 sendfile:加速消费流程

下面展开讲讲。

1、PageCache 加速消息读写

使用 PageCache 主要能带来如下好处:

  • 写入文件的时候:操作系统会先把数据写入到内存中的 PageCache,然后再一批一批地写到磁盘上,从而减少磁盘 IO 开销。

  • 读取文件的时候:也是从 PageCache 中来读取数据。

如果消息刚刚写入到服务端就会被消费,按照 LRU 的“优先清除最近最少使用的页”这种策略,读取的时候,对于这种刚刚写入的 PageCache,命中的几率会非常高。

2、Kafka 的文件布局 以及 磁盘文件顺序写入

文件布局如下图所示:

主要特征是:文件的组织方式是“topic + 分区”,每一个 topic 可以创建多个分区,每一个分区包含单独的文件夹。

Kafka 在分区级别实现文件顺序写:即多个文件同时写入,更能发挥磁盘 IO 的性能。

  • 相对比 RocketMQ:RocketMQ 在消息写入时追求极致的顺序写,所有的消息不分主题一律顺序写入 commitlog 文件, topic 和 分区数量的增加不会影响写入顺序。
  • 弊端: Kafka 在消息写入时的 IO 性能,会随着 topic 、分区数量的增长先上升,后下降。

所以使用 Kafka 时,要警惕 Topic 和 分区数量。

3、零拷贝 sendfile:加速消费流程

当不使用零拷贝技术读取数据时

流程如下

1)消费端 Consumer:向 Kafka Broker 请求拉取消息

2)Kafka Broker 从 OS Cache 读取消息到 应用程序的内存空间:

  • 若 OS Cache 中有消息,则直接读取;
  • 若 OS Cache 中无消息,则从磁盘里读取。

3)再通过网卡,socket 将数据发送给 消费端Consumer

当使用零拷贝技术读取数据

Kafka 使用零拷贝技术可以把这个复制次数减少一次,直接从 PageCache 中把数据复制到 Socket 缓冲区中。

  • 这样不用将数据复制到用户内存空间。
  • DMA 控制器直接完成数据复制,不需要 CPU 参与,速度更快。

三、消费端 Consumer

消费者只从 Leader分区批量拉取消息。

为了提高消费速度,多个消费者并行消费比不可少。Kafka 允许创建消费组(唯一标识 group.id),在同一个消费组的消费者共同消费数据。

举个例子

  • 有两个 Kafka Broker,即有 2个机子
  • 有一个主题:TOPICA,有 3 个分区(0, 1, 2)

如上图,举例 4 中情况

  • group.id = 1,有一个消费者:这个消费者要处理所有数据,即 3 个分区的数据。

  • group.id = 2,有两个消费者:consumer 1消费者需处理 2个分区的数据,consumer2 消费者需处理 1个分区的数据。

  • group.id = 3,有三个消费者**:消费者数量与分区数量相等,**刚好每个消费者处理一个分区。

  • group.id = 4,有四个消费者**:消费者数量 > 分区数量,**第四个消费者则会处于空闲状态。

说在最后

kafka相关面试题,是非常常见的面试题。

以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。

最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

学习过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

参考文献

https://juejin.cn/post/7134463012563320868

推荐阅读

《炸裂,靠“吹牛”过京东一面,月薪40K》

《太猛了,靠“吹牛”过顺丰一面,月薪30K》

《炸裂了…京东一面索命40问,过了就50W+》

《问麻了…阿里一面索命27问,过了就60W+》

《百度狂问3小时,大厂offer到手,小伙真狠!》

《饿了么太狠:面个高级Java,抖这多硬活、狠活》

《字节狂问一小时,小伙offer到手,太狠了!》

《收个滴滴Offer:从小伙三面经历,看看需要学点啥?》

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓

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

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

相关文章

测试人员如何通过AI提高工作效率!

随着AI技术的兴起&#xff0c;像OpenAI推出的ChatGPT、Microsoft发布的Microsoft 365 Copilot、阿里的通义千问、百度的文心一言、华为的盘古大模型等。很多测试人员开始担心&#xff0c;岗位是否会被AI取代&#xff1f;其实取代你的不是AI&#xff0c;而是会使用AI的测试人&am…

[论文分享]VOLO: Vision Outlooker for Visual Recognition

VOLO: Vision Outlooker for Visual Recognition 概述 视觉 transformer&#xff08;ViTs&#xff09;在视觉识别领域得到了广泛的探索。由于编码精细特征的效率较低&#xff0c;当在 ImageNet 这样的中型数据集上从头开始训练时&#xff0c;ViT 的性能仍然不如最先进的 CNN。…

解密长短时记忆网络(LSTM):从理论到PyTorch实战演示

目录 1. LSTM的背景人工神经网络的进化循环神经网络&#xff08;RNN&#xff09;的局限性LSTM的提出背景 2. LSTM的基础理论2.1 LSTM的数学原理遗忘门&#xff08;Forget Gate&#xff09;输入门&#xff08;Input Gate&#xff09;记忆单元&#xff08;Cell State&#xff09;…

【洛谷】P1678 烦恼的高考志愿

原题链接&#xff1a;https://www.luogu.com.cn/problem/P1678 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 将每个学校的分数线用sort()升序排序&#xff0c;再二分查找每个学校的分数线&#xff0c;通过二分找到每个同学估分附近的分数线。 最后…

【Java】对象与类

【Java】对象与类 文章目录 【Java】对象与类1、学习背景2、定义&使用2.1 创建类2.2 创建对象 3、static关键字3.1 修饰变量3.2 修饰方法3.3 修饰代码块3.4 修饰内部类 4、this关键字5、封装特性5.1 访问修饰符5.2 包的概念 6、构造方法7、代码块7.1 普通代码块7.2 成员代码…

信息安全:入侵检测技术原理与应用.(IDS)

信息安全&#xff1a;入侵检测技术原理与应用. 入侵检测是网络安全态势感知的关键核心技术&#xff0c;支撑构建网络信息安全保障体系。入侵是指违背访问目标的安全策略的行为。入侵检测通过收集操作系统、系统程序、应用程序、网络包等信息&#xff0c;发现系统中违背安全策略…

无公网IP内网穿透使用vscode配置SSH远程ubuntu随时随地开发写代码

文章目录 前言1、安装OpenSSH2、vscode配置ssh3. 局域网测试连接远程服务器4. 公网远程连接4.1 ubuntu安装cpolar内网穿透4.2 创建隧道映射4.3 测试公网远程连接 5. 配置固定TCP端口地址5.1 保留一个固定TCP端口地址5.2 配置固定TCP端口地址5.3 测试固定公网地址远程 前言 远程…

【QT5-自我学习-线程qThread移植与使用-通过代码完成自己需要功能-移植小记3】

【QT5-自我学习-线程qThread移植与使用-通过代码完成自己需要功能-移植小记3】 1、前言2、实验环境3、自我总结&#xff08;1&#xff09;文件的编写&#xff08;2&#xff09;信号与槽的新理解&#xff08;3&#xff09;线程数据的传递 4、移植步骤第一步&#xff1a;添加新文…

在Linux系统上安装和配置Redis数据库,无需公网IP即可实现远程连接的详细解析

文章目录 1. Linux(centos8)安装redis数据库2. 配置redis数据库3. 内网穿透3.1 安装cpolar内网穿透3.2 创建隧道映射本地端口 4. 配置固定TCP端口地址4.1 保留一个固定tcp地址4.2 配置固定TCP地址4.3 使用固定的tcp地址连接 Redis作为一款高速缓存的key value键值对的数据库,在…

React组件间数据传递(弹框和高阶组件(HOC)特性实现)

前言 在现代前端开发中&#xff0c;React 已经成为了最受欢迎的 JavaScript 库之一。而在复杂的应用中&#xff0c;不同组件之间的数据传递问题显得尤为关键。在本文中&#xff0c;我们将探讨一种高效的方法&#xff0c;即如何利用弹框和高阶组件特性来实现 React 组件间的数据…

vue3 基础知识 ( webpack 基础知识)05

你好 文章目录 一、组件二、如何支持SFC三、webpack 打包工具四、webpack 依赖图五、webpack 代码分包 一、组件 使用组件中我们可以获得非常多的特性&#xff1a; 代码的高亮&#xff1b;ES6、CommonJS的模块化能力&#xff1b;组件作用域的CSS&#xff1b;可以使用预处理器来…

芯片行业震荡期,数字后端还可以入吗?

自去年开始&#xff0c;芯片行业仿佛进入了动荡期&#xff0c;经历了去年秋招和今年春招的小伙伴都知道&#xff0c;如今找工作有多难。 半导体行业人才缩减、各大厂裁员&#xff0c;在加上高校毕业生人数破千万&#xff0c;对于即将踏入IC这个行业的应届生来说&#xff0c;今…

存储过程的使用

一、实验目的 熟练掌握使用 SQL SERVER 2000 创建和执行存储过程的方法。 熟练掌握存储过程的删除操作。 二、实验准备 1&#xff0e;熟悉 SQL SERVER 2000 设计环境&#xff1b; 2&#xff0e;熟悉存过过程的创建方法、步骤 三、实验内容 1、利用企业管理器或查询分析器…

在线OJ平台项目

一、项目源码 Online_Judge yblhlk/Linux课程 - 码云 - 开源中国 (gitee.com) 二、所用技术与开发环境 1.所用技术: MVC架构模式 (模型&#xff0d;视图&#xff0d;控制器) 负载均衡系统设计 多进程、多线程编程 C面向对象编程 & C 11 & STL 标准库 C Boost 准标…

建议收藏|软考机构推荐看这一篇就够了

需要最近因为软考改革成机考&#xff0c;大家都在问还有没有必要找机构学&#xff1f;本来已经进入自学阶段的考生&#xff0c;也纷纷开始慌张机考改革会不会影响考试难度&#xff1f;今天胖圆给大家总结一下软考要不要报机构&#xff1f;市面上的软考培训机构如何选择&#xf…

高手速成|数据库脚本生成工具

高手速成|数据库脚本生成工具 文章目录 高手速成|数据库脚本生成工具前言1、软件的安装及使用2、建立新工程3、创建Conceptual Data Model&#xff08;概念数据模型&#xff09;4、将E-R图转化为其他数据库模型5、导出DBMS代码&#xff08;Sql执行脚本&#xff09;6、执行sql脚…

【boost网络库从青铜到王者】第六篇:asio网络编程中的socket异步读(接收)写(发送)

文章目录 1、简介2、异步写 void AsyncWriteSomeToSocketErr(const std::string& buffer)3、异步写void AsyncWriteSomeToSocket(const std::string& buffer)4、异步写void AsyncSendToSocket(const std::string& buffer)5、异步读void AsyncReadSomeToSocket(cons…

Java 8 Stream 之 collect() 的奇技淫巧

来源&#xff1a;blog.csdn.net/qq_35387940/ article/details/127008965 前言 正文 第一个小玩法 第二个小玩法 前言 本身我是一个比较偏向少使用Stream的人&#xff0c;因为调试比较不方便。 但是, 不得不说&#xff0c;stream确实会给我们编码带来便捷。 所以还是忍…

python开发环境搭建

1、安装python 下载地址&#xff1a;https://www.python.org/downloads/windows/ 双击安装。 安装完验证&#xff1a; 2、安装IDE 下载地圵&#xff1a;https://www.jetbrains.com/zh-cn/pycharm/download/?sectionwindows 免费版本 3、安装依赖包 在项目的根目录下…

【RuoYi移动端】HBuild工具插件安装和系统配置manifest.json

一、点【工具】-【插件安装】安装如下工具 二、点【manifest.json】