从零开始读RocketMq源码(五)Message的消费流程解析

目录

前言

准备

拉取服务和重平衡服务启动

初识PullRequest

重平衡服务

对重平衡资源进行排序

MessageQueue消息队列集合来源

Consumer消费者集合数据来源

确定分配资源策略

执行分配策略

初始化ProcessQueue

初始化PullRequest

内存队列填充PullRequest

消息拉取服务

调用消息核心处理方法

调用远程Broker服务拉取消息

提交消费消息请求

初始化ConsumeRequest

消息监听器消费消息

总结


前言

上一篇我们对Consumer的启动流程就进行了解析,有了消费者那么消费的Message从何而来呢,这就是本篇学习的重点。本篇会讲到MessageQueue的分配、Message的拉取以及消费等,让我们一起来学习吧!

准备

源码地址:https://github.com/apache/rocketmq

目前最新版本为:5.2.0

那么我们在idea上切换分支为 release-5.2.0

拉取服务和重平衡服务启动

//源码位置
//包名:org.apache.rocketmq.client.impl.factory
//文件名:MQClientInstance
//行数:315
// Start pull service
this.pullMessageService.start();
// Start rebalance service
this.rebalanceService.start();
  • 拉取服务重平衡服务的启动其实就是在上一篇《从零开始读RocketMq源码(四)Consumer启动流程解析》中就已经执行了
  • 这里就是触发ConsumerBroker获取Message的源头,进入启动源码会发现其实也就是分别开启了两个独立的线程来运行的。消息的获取也是需要这两个线程相互合作才能完成。

为什么我们消息选择的是Push推模式但是这里服务启动的却是PullMessageService呢?

因为实际上,推模式下的实现还是基于消费者主动拉取的方式,推模式是通过一个长轮询的机制来实现的 , 消费者向 Broker 注册一个消息监听器,消费者内部维护一个线程,不断向 Broker 拉取消息,如果没有消息,则保持连接并等待消息到来。(下面会详细讲到)

初识PullRequest

我们按照启动顺序先对pullMessageService源码进行了解,进入线程的run()方法中

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:PullMessageService
//行数:131
MessageRequest messageRequest = this.messageRequestQueue.take();
if (messageRequest.getMessageRequestMode() == MessageRequestMode.POP) {this.popMessage((PopRequest) messageRequest);
} else {this.pullMessage((PullRequest) messageRequest);
}
  • 这里调用了内存队列take()方法,这个方法看着是否眼熟,因为我们在《Broker存储Message流程解析》中也使用过
  • take()方法的作用就是取出元素并从队列中删除,如果队列为空则会阻塞, 直到队列中有可用的消息请求为止。由此可见pullMessageService相当于内存队列中的消费者角色
  • 但是这次的内存队列使用的是LinkedBlockingQueue类型,Broker存储Message中却使用的是PriorityBlockingQueue类型
  • 内存队列获取出来的元素最终被转化为 PullRequest ,该对象在 RocketMQ 中用于封装消费者向 Broker 拉取消息时所需的所有信息。顾名思义这个对象就是向Broker发起拉取Message的请求对象。

扩展:LinkedBlockingQueue与PriorityBlockingQueue内存队列有什么区别吗?

LinkedBlockingQueue是一个 基于链表实现的阻塞队列按 FIFO(先进先出)顺序存储元素。插入元素时,会添加到队列的尾部,移除元素时,会从队列的头部取出 。 如果队列满了,插入操作会阻塞;如果队列空了,移除操作会阻塞。

PriorityBlockingQueue 基于优先级堆实现的阻塞队列元素按优先级顺序排列。默认情况下,使用元素的自然顺序(即元素需要实现 Comparable 接口) , 插入元素时,会根据优先级排序,移除元素时,总是移除优先级最高的元素 ,由于是无界队列,所以插入操作不会阻塞;如果队列空了,移除操作会阻塞。

因为项目刚初始化,所以messageRequestQueue队列中一定是空的,那么调用task()方法后,pullMessageService线程一直会是阻塞状态不能向下执行,那么什么地方会新增内存队列元素呢,那就只有先把另一个主角rebalanceService服务请出场了。

重平衡服务

rebalanceService的核心作用就是 负责定期执行消费者的负载均衡操作当消费者实例数量或者消息队列数量发送变更,确保消息队列均匀分布在多个消费者实例上,然后将 PullRequest 塞到messageRequestQueue内存队列中。

Producer生产者也使用到了该服务,作用则是将Message负载均衡到不同的MessageQueue中

进入run()方法

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalanceService
//行数:51
boolean balanced = this.mqClientFactory.doRebalance();
realWaitInterval = balanced ? waitInterval : minInterval;
  • 结合源码上下文会发现这是包裹在一个while()循环中,相当于一个定时任务
  • 当调用doRebalance()方法重平衡成功后,设置waitInterval= 20s后再次执行
  • 如果调用doRebalance()方法重平衡执行失败,设置minInterval= 1s后再次重试
  • 只要当前线程启动成功就会按照上面的逻辑周而复始

对重平衡资源进行排序

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalanceImpl
//行数:342
Collections.sort(mqAll);
Collections.sort(cidAll);
  1. mqAll为当前Topic中所有消息队列集合,然后进行排序
  2. cidAll为所有订阅了当前Topic的消费者实例id集合,然后进行排序

看到这里一头雾水,为什么要进行排序呢?

  • 因为重平衡操作都是单独在每一个客户端进行的,而不是统一在Broker服务上进行分配的,那么为了保证消费者实例与消息队列能够合理的负载均衡,并且让每个消费者拿到互不相同的MessageQueue,那么就需要进行排序
  • 又因为订阅相同的topic,那么他们获取的总的消息队列和消费者实例都是完全相同的,然后每个客户端进行相同的排序,那么排序结果都是一样的,后续会使用到这些排序后的结果来对每个客户端进行分配MessageQueue,因为每个客户端id都不一样,从而经过后面的分配算法保证买个消费者分配的MessageQueue都不一样。后续分配算法会具体讲到。

MessageQueue消息队列集合来源

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalanceImpl
//行数:325
Set<MessageQueue> mqSet = this.topicSubscribeInfoTable.get(topic);
//...
List<MessageQueue> mqAll = new ArrayList<>();
mqAll.addAll(mqSet);

mqAll集合最终是从Map集合中通过Topic获取:ConcurrentMap<String/* topic */, Set<MessageQueue>> topicSubscribeInfoTable;

秉承着所以数据都有迹可循的原则,不禁要问topicSubscribeInfoTable中的数据从哪里来的呢?

其实就在上一篇讲到的Consumer启动流程中的填入的数据,就在下面方法中

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:1001
this.updateTopicSubscribeInfoWhenSubscriptionChanged();

查看上方方法中的逻辑,会发现数据源又来自topic的订阅信息map:ConcurrentMap<String /* topic */, SubscriptionData> subscriptionInner通过subscriptionInner的循环处理来分别填充topic对应的MessageQueue集合的

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:1235
Map<String, SubscriptionData> subTable = this.getSubscriptionInner();

那么subscriptionInner中的数据又是什么时候填充的呢?

其实也是在最开始的Consumer启动的main方法设置订阅topic时中填充的。

//源码位置
//包名:org.apache.rocketmq.example.simple
//文件名:PushConsumer
//行数:39
consumer.subscribe(TOPIC, "*");

深入subscribe()方法,你就会发现填充数据的源码

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:1250
SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subExpression);
this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData);

这样我们就理清楚了整个MessageQueue集合的来源和去向,有头也有尾,也不会再一知半解了

Consumer消费者集合数据来源

//源码位置
//包名:org.apache.rocketmq.client.impl
//文件名:MQClientAPIImpl
//行数:1349
RemotingCommand response = this.remotingClient.invokeSync(MixAll.brokerVIPChannel(this.clientConfig.isVipChannelEnabled(), addr),request, timeoutMillis);

由源码可知,消费者客户端集合cidAll是直接调用Broker服务来获取的

确定分配资源策略

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalanceImpl
//行数:345
AllocateMessageQueueStrategy strategy = this.allocateMessageQueueStrategy;
List<MessageQueue> allocateResult = null;
try {allocateResult = strategy.allocate(this.consumerGroup,this.mQClientFactory.getClientId(),mqAll,cidAll);
} catch (Throwable e) {log.error("allocate message queue exception. strategy name: {}, ex: {}", strategy.getName(), e);return false;
}
  • 调用 strategy.allocate()方法对当前消费者实例分配消息队列,并返回一个集合allocateResult

进入分配方法中,会发现官方实现了多种分配策略

那么我们使用的是哪一种呢?其实在我们启动消费者服务实例化消费者对象时就已经设置了默认策略了

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumer
//行数:308
public DefaultMQPushConsumer(final String consumerGroup) {this(consumerGroup, null, new AllocateMessageQueueAveragely());
}

new AllocateMessageQueueAveragely() 就是为我们默认指定的分配策略,即平均哈希队列算法

执行分配策略

//源码位置
//包名:org.apache.rocketmq.client.consumer.rebalance
//文件名:AllocateMessageQueueAveragely
//行数:32
List<MessageQueue> result = new ArrayList<>();
if (!check(consumerGroup, currentCID, mqAll, cidAll)) {return result;
}
int index = cidAll.indexOf(currentCID);
int mod = mqAll.size() % cidAll.size();
int averageSize =
mqAll.size() <= cidAll.size() ? 1 : (mod > 0 && index < mod ? mqAll.size() / cidAll.size()+ 1 : mqAll.size() / cidAll.size());
int startIndex = (mod > 0 && index < mod) ? index * averageSize : index * averageSize + mod;
int range = Math.min(averageSize, mqAll.size() - startIndex);
for (int i = 0; i < range; i++) {result.add(mqAll.get((startIndex + i) % mqAll.size()));
}

这就是给当前消费者分配消息队列的算法逻辑

  • index表示当前消费者实例所有排序后的消费者实例集合cidAll中的下标位置,每个currentCID都是唯一的。
  • mod:表示mqAll集合中的MessageQueue能否被cidAll集合中的消费者实例均匀分配。
  • averageSize:表示当前消费者平均能被分配到的MessageQueue数量。
  • startIndex:表了当前这个 Consumer 从 MessageQueue 数组的哪个位置开始取。
  • range: 代表当前这个 Consumer 获取到了多少个 MessageQueue

总结最终分配逻辑可以理解为两步所有消费者客户端都会分配到相同数量的MessageQueue,对剩余无法平均分配的MessageQueue按照cidAll集合的顺序进行分配。但其实源码中是一次性算出分配结果的。

初始化ProcessQueue

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalanceImpl
//行数:528
ProcessQueue pq = createProcessQueue(topic);
pq.setLocked(true);
long nextOffset = this.computePullFromWhere(mq);
if (nextOffset >= 0) {ProcessQueue pre = this.processQueueTable.putIfAbsent(mq, pq);//...
}
  • 第一步初始化创建ProcessQueue处理队列
  • 最后将队列设置到processQueueTable的Map中,ConcurrentMap<MessageQueue, ProcessQueue> processQueueTable主要用于消费者负载均衡和消息消费管理,确保消息队列能够被正确地分配和处理
  • 该Map在上一篇消费者启动中讲到过,数据用于处理定时任务清除过期的消息
  • putIfAbsent()方法为如果存在相同的Key,就将原来的Value返回,不存在则返回null,同时put数据

初始化PullRequest

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalanceImpl
//行数:532
if (pre != null) {log.info("doRebalance, {}, mq already exists, {}", consumerGroup, mq);
} else {log.info("doRebalance, {}, add a new mq, {}", consumerGroup, mq);PullRequest pullRequest = new PullRequest();pullRequest.setConsumerGroup(consumerGroup);pullRequest.setNextOffset(nextOffset);pullRequest.setMessageQueue(mq);pullRequest.setProcessQueue(pq);pullRequestList.add(pullRequest);changed = true;
}
  • 到这里就正式封装PullRequest拉取请求对象了,紧接着上面初始化ProcessQueue代码可知,只有当该MessageQueue是一个全新并且之前不存在的消息队列时才会进行拉取请求
  • 初始化PullRequest从而和本篇前面讲到的pullMessageService有了关联

内存队列填充PullRequest

循环处理PullRequest对象

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:RebalancePushImpl
//行数:266
for (PullRequest pullRequest : pullRequestList) {if (delay <= 0) {this.defaultMQPushConsumerImpl.executePullRequestImmediately(pullRequest);} else {this.defaultMQPushConsumerImpl.executePullRequestLater(pullRequest, delay);}
}

循环逻辑处理中,对每次PullRequest对象的处理延迟5s,可以看到这里已经进入了PullMessageService服务中

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:PullMessageService
//行数:45
this.scheduledExecutorService.schedule(new Runnable() {@Overridepublic void run() {PullMessageService.this.executePullRequestImmediately(pullRequest);}
}, timeDelay, TimeUnit.MILLISECONDS);

最后就是将PullRequest对象put进内存队列LinkedBlockingQueue<MessageRequest> messageRequestQueue中,从而激活PullMessageService服务,结束阻塞状态开始执行拉取逻辑。

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:PullMessageService
//行数:58
this.messageRequestQueue.put(pullRequest);

消息拉取服务

上面RebalanceService服务完成内存队列PullRequest入栈后,那么紧接着PullMessageService服务开始处理PullRequest。

调用消息核心处理方法

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:480
this.pullAPIWrapper.pullKernelImpl(pullRequest.getMessageQueue(),subExpression,subscriptionData.getExpressionType(),subscriptionData.getSubVersion(),pullRequest.getNextOffset(),this.defaultMQPushConsumer.getPullBatchSize(),this.defaultMQPushConsumer.getPullBatchSizeInBytes(),sysFlag,commitOffsetValue,BROKER_SUSPEND_MAX_TIME_MILLIS,CONSUMER_TIMEOUT_MILLIS_WHEN_SUSPEND,CommunicationMode.ASYNC,pullCallback
);

我们这里关注两个点:

  • CommunicationMode.ASYNC:表示向Broker服务拉取消息是一个异步的操作
  • pullCallback:异步回调后处理的逻辑就封装在该对象中

调用远程Broker服务拉取消息

//源码位置
//包名:org.apache.rocketmq.client.impl
//文件名:MQClientAPIImpl
//行数:1024this.remotingClient.invokeAsync(addr, request, timeoutMillis, new InvokeCallback() {//...public void operationSucceed(RemotingCommand response) {try {PullResult pullResult = MQClientAPIImpl.this.processPullResponse(response, addr);pullCallback.onSuccess(pullResult);} catch (Exception e) {pullCallback.onException(e);}}//...}

由源码我们可知,pullCallback回调对象提供了

  • pullCallback.onSuccess(pullResult):请求成功后处理逻辑方法
  • pullCallback.onException(e):请求异常处理逻辑方法

提交消费消息请求

在调用this.pullAPIWrapper.pullKernelImpl核心方法之前,就已经重写了pullCallback的回调方法

并在onSuccess()实现中进行提交消费请求操作

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:DefaultMQPushConsumerImpl
//行数:370
DefaultMQPushConsumerImpl.this.consumeMessageService.submitConsumeRequest(pullResult.getMsgFoundList(),processQueue,pullRequest.getMessageQueue(),dispatchToConsume);

初始化ConsumeRequest

ConsumeRequest消费请求是PullRequest拉取请求之后的又一请求对象

顾名思义这也是消息最终被消费的请求了

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:ConsumeMessageConcurrentlyService
//行数:211
ConsumeRequest consumeRequest = new ConsumeRequest(msgThis, processQueue, messageQueue);
try {this.consumeExecutor.submit(consumeRequest);
} catch (RejectedExecutionException e) {for (; total < msgs.size(); total++) {msgThis.add(msgs.get(total));}this.submitConsumeRequestLater(consumeRequest);
}
  • ConsumeRequest加入到线程池 ThreadPoolExecutor consumeExecutor;相当于就是为当前消费请求单独创建一个线程来异步处理
  • 如果线程池加入异常,则会延迟5s后再次重试一次

消息监听器消费消息

直接进入ConsumeRequest线程的run()方法中

//源码位置
//包名:org.apache.rocketmq.client.impl.consumer
//文件名:ConsumeMessageConcurrentlyService
//行数:211
MessageListenerConcurrently listener = ConsumeMessageConcurrentlyService.this.messageListener;
ConsumeConcurrentlyContext context = new ConsumeConcurrentlyContext(messageQueue);
ConsumeConcurrentlyStatus status = null;
//...
status = listener.consumeMessage(Collections.unmodifiableList(msgs), context);
  • 这里就是消息消费的最后一步,将Message放入监听器MessageListenerConcurrentlyconsumeMessage()方法中。

Message最终在消费者启动中main()方法的注册监听器的地方打印出来,最后返回消费成功状态ConsumeConcurrentlyStatus.CONSUME_SUCCESS

//源码位置
//包名:org.apache.rocketmq.example.simple
//文件名:PushConsumer
//行数:37
consumer.registerMessageListener(new MessageListenerConcurrently() {@Overridepublic ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;}
});

Consumer端消费Message简易流程图如下

总结

根据上一篇消费者的启动到本篇消息的拉取与消费,完成了一个Message在消费者端的闭环。本篇我们也学到了一个新的内存队列LinkedBlockingQueue,也讲到了与PriorityBlockingQueue的区别,至此消费者端的源码基本学习完了,希望从源码中大家都有所收获!

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

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

相关文章

【学习笔记】无人机系统(UAS)的连接、识别和跟踪(二)-定义和缩写

引言 3GPP TS 23.256 技术规范&#xff0c;主要定义了3GPP系统对无人机&#xff08;UAV&#xff09;的连接性、身份识别、跟踪及A2X&#xff08;Aircraft-to-Everything&#xff09;服务的支持。 3GPP TS 23.256 技术规范&#xff1a; 【免费】3GPPTS23.256技术报告-无人机系…

从操作系统层面认识Linux

描述进程-PCB Linux操作系统下的PCB是: task_struct https://www.cnblogs.com/tongyan2/p/5544887.htmlhttps://www.cnblogs.com/tongyan2/p/5544887.html校招必背操作系统面试题-什么是 PCB&#xff08;进程控制块&#xff09; &#xff1f;_哔哩哔哩_bilibili校招必背操作系…

notes for datawhale summer camp NPL task3

了解 Transformer 模型&#xff0c;并基于 Transformer 模型实现在机器翻译任务上的应用&#xff01; Transformer 介绍 基于循环或卷积神经网络的序列到序列建模方法是现存机器翻译任务中的经典方法。然而&#xff0c;它们在建模文本长程依赖方面都存在一定的局限性。 为了…

Mac 安装MySQL 配置环境变量 修改密码

文章目录 1 下载与安装2 配置环境变量3 数据库常用命令3.1 Mac使用设置管理mysql服务启停 4 数据库修改root密码4.1 知道当前密码4.2 忘记当前密码4.3 问题 参考 1 下载与安装 官网&#xff1a;https://www.mysql.com/ 找到开源下载方式 下载社区版 2 配置环境变量 对于Mac…

【Spring全家桶系列之核心篇 | Spring Cloud】 - 第七章 掌握Gateway核心技术,实现高效路由与转发

目录 前言示例创建一个服务提供者创建网关 创建common子项目 前言 Spring Cloud Gateway 是一个基于 Spring Boot 的非阻塞 API 网关服务&#xff0c;它提供了动态路由、请求断言、过滤器等功能。 以下是关于 Spring Cloud Gateway 的示例&#xff1a; 示例 创建一个服务提…

深度挖掘行情接口:股票市场中的关键金融数据API接口解析

在股票市场里&#xff0c;存在若干常见的股票行情数据接口&#xff0c;每一种接口皆具备独特的功能与用途。以下为一些常见的金融数据 API 接口&#xff0c;其涵盖了广泛的金融数据内容&#xff0c;其中就包含股票行情数据&#xff1a; 实时行情接口 实时行情接口&#xff1a…

使用小波分析实现文字种类自动识别

文章目录 数据简介开始实验小波分解得出结果结果分析误差分析 数据简介 各找一篇中文&#xff0c;日文&#xff0c;韩文&#xff0c;英文&#xff0c;俄文较长的学术论文。将论文转化为JPG格式。拆分每张JPG生成更多小的JPG。最终获得很多5个不同语言的JPG并且自带标签。数据链…

Python基础语法篇(下)+ 数据可视化

Python基础语法&#xff08;下&#xff09; 数据可视化 一、函数&#xff08;一&#xff09;函数的定义&#xff08;二&#xff09;函数的调用和传参 二、文件操作&#xff08;一&#xff09;文件读取和写入&#xff08;二&#xff09;文件对象及方法&#xff08;三&#xff09…

[003-02-10].第10节:Docker环境下搭建Redis主从复制架构

我的博客大纲 我的后端学习大纲 我的Redis学习大纲 1.cluster&#xff08;集群&#xff09;模式-docker版 哈希槽分区进行亿级数据存储 1.1.面试题&#xff1a;1~2亿条数据需要缓存&#xff0c;请问如何设计这个存储案例 1.回答&#xff1a;单机单台100%不可能&#xff0c;肯…

Adobe国际认证详解-影视后期

在当今的数字媒体时代&#xff0c;影视后期制作作为创意产业的核心环节&#xff0c;对于专业技能的要求日益提高。Adobe国际认证&#xff0c;作为全球创意设计领域的重要标杆&#xff0c;为影视后期制作人员提供了一个展示自我、提升技能的国际舞台。 何为影视后期&#xff1f;…

matlab 异常值检测与处理——Robust Z-score法

目录 一、算法原理1、概述2、主要函数3、参考文献二、代码实现三、结果展示四、相关链接本文由CSDN点云侠翻译,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、概述 Robust Z-score法也被称为中位数绝对偏差法。它类似于Z-sc…

10. Hibernate LazyFetch

1. 前言 本节和大家一起聊聊 Hibernate 中的 Lazy 和 Fetch 的区别&#xff0c;及两者适合的开发场景。通过本节课程的学习&#xff0c;你将了解到&#xff1a; 什么是延迟加载&#xff1b;延迟加载的意义。 2. 又见 get() 和 load() Session 对象提供了 2 个方法用来查询 &…

基于深度残差网络迁移学习的浸润性导管癌检测

1. 引言 癌症是一种异常细胞不受控制地分裂损害健康组织的疾病。皮肤或覆盖我们内脏的组织中的癌细胞被称为癌。乳房中的大多数癌是导管癌。侵袭性导管癌(Invasive Ductal Carcinoma, IDC)始于乳管&#xff0c;侵犯乳房周围纤维组织&#xff0c;晚期可通过血液扩散至淋巴结或身…

【鸿蒙学习笔记】位置设置・position・绝对定位・子组件相对父组件

官方文档&#xff1a;位置设置 目录标题 position・绝对定位・子组件相对父组件Row Text position position・绝对定位・子组件相对父组件 正→ ↓ Row Text position Entry Component struct Loc_position {State message: string Hello World;build() {Column() {Co…

二叉树的前、中、后序遍历(递归法、迭代法)leetcode144/94/145

leetcode144、二叉树的前序遍历 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,2,3] 示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[] 示例 3&#xff1a;…

求职学习day5

安排明天hr面 投一下平安可能。 hr面准备&#xff0c;复习java核心技术&#xff0c;复习java项目。 正视自己&#xff0c;调整心态。 也是很早接触了javaguide但是没有持续学习&#xff0c;项目介绍 | JavaGuide&#xff0c;面试前复习一下感觉还是很有收获的。 还有一些…

【QT】label中添加QImage图片并旋转(水平翻转、垂直翻转、顺时针旋转、逆时针旋转)

目录 0.简介 1.详细代码及解释 1&#xff09;原label显示在界面上 2&#xff09;水平翻转 3&#xff09;垂直翻转 4&#xff09;顺时针旋转45度 5&#xff09;逆时针旋转 0.简介 环境&#xff1a;windows11 QtCreator 背景&#xff1a;demo&#xff0c;父类为QWidget&a…

Go语言并发编程-Channel通信_2

Channel通信 Channel概述 不要通过共享内存的方式进行通信&#xff0c;而是应该通过通信的方式共享内存 这是Go语言最核心的设计模式之一。 在很多主流的编程语言中&#xff0c;多个线程传递数据的方式一般都是共享内存&#xff0c;而Go语言中多Goroutine通信的主要方案是Cha…

笔记 7 :linux 011 注释,函 bread () , get_hash_table () , find_buffer ()

&#xff08;57&#xff09;接着介绍另一个读盘块的函数 bread&#xff08;&#xff09;&#xff1a; &#xff08;58&#xff09;因为 函数 get_blk&#xff08;&#xff09;大量调用了其它函数&#xff0c;一版面列举不完&#xff0c;故对其调用的函数先行注释&#xff1a;ge…

【驱动程序】霍尔编码器电机_CubeMX_HAL库

【驱动程序】霍尔编码器电机_CubeMX_HAL库 电机型号&#xff1a;MG310 霍尔编码器电机 驱动模块&#xff1a;L298N 接线 注&#xff1a; L298N 12V接线柱位置可以接50V~5V当跳线帽接入时&#xff0c;5V接线柱为5V输出&#xff0c;可以给驱动板供电当跳线帽拔出时&#xff0…