RocketMQ5.0顺序消息设计实现

前言

顺序消息是 RocketMQ 提供的一种高级消息类型,支持消费者按照发送消息的先后顺序获取消息,从而实现业务场景中的顺序处理。
顺序消息的顺序关系通过消息组(MessageGroup)判定和识别,发送顺序消息时需要为每条消息设置归属的消息组,相同消息组的多条消息之间遵循先进先出的顺序关系,不同消息组、无消息组的消息之间不涉及顺序性。比如:一条订单从创建到完结整个生命周期内产生的消息,如果要保证消费的顺序性,则可以用订单号作为 MessageGroup。

RocketMQ 4.x 实现顺序消息相对容易,因为采用的是队列模型,一个队列只能被一个消费者消费,而队列本身是能保证先进先出的,此时只要保证消费者单线程串行消费即可。
到了 RocketMQ 5.0 时代,Pop 模式下因为采用的是消息模型,消费者可以消费所有队列的消息,顺序消息的实现也将变得更加复杂。

如何保证消息的顺序

顺序消息需要依赖生产者、Broker、消费者共同保证。

生产顺序性
首先是消息生产的顺序性,相同 MessageGroup 必须保证单一生产者、单线程同步发送。多生产者实例或者多线程并发发送消息,都无法保证消息是顺序到达 Broker 的,消息源头的顺序性都无法保证,后续流程的顺序就更是无从谈起了。

存储顺序性
消息发送到 Broker 必须按照到达顺序有序存储,这一点很容易实现。因为队列天生是先进先出的,但是一个 Topic 下可能会有多个队列,此时保证相同 MessageGroup 的消息被发送到同一个队列是重点,这个可以通过计算 MessageGroup 哈希值对队列数取模实现。

投递顺序性
消费者来拉取顺序消息时,Broker 得知道之前投递的消息是否全部被消费完了,如果还在消费中,则当前队列不能再继续投递了,消费者必须等待其它拉取到消息的消费者消费完毕后才能接着拉取后面的消息。

消费顺序性
消费者在拉取到消息后,必须保证单线程顺序消费,如果并发消费也是不能保证顺序的。

设计实现

生产端的顺序需要调用方自行保证,这个没啥好说的。
存储端的顺序,队列本身能保证先进先出,只要保证相同 MessageGroup 投递到同一个目标队列即可。Proxy 用一个叫 SendMessageQueueSelector 的组件对消息的 MessageGroup 计算一致性哈希后取模得到目标队列。

@Override
public AddressableMessageQueue select(ProxyContext ctx, MessageQueueView messageQueueView) {try {apache.rocketmq.v2.Message message = request.getMessages(0);String shardingKey = null;if (request.getMessagesCount() == 1) {// 分片键 也就是MessageGroupshardingKey = message.getSystemProperties().getMessageGroup();}AddressableMessageQueue targetMessageQueue;if (StringUtils.isNotEmpty(shardingKey)) {// 根据写队列数计算一致性哈希List<AddressableMessageQueue> writeQueues = messageQueueView.getWriteSelector().getQueues();int bucket = Hashing.consistentHash(shardingKey.hashCode(), writeQueues.size());targetMessageQueue = writeQueues.get(bucket);} else {targetMessageQueue = messageQueueView.getWriteSelector().selectOne(false);}return targetMessageQueue;} catch (Exception e) {return null;}
}

顺序消费

RocketMQ 5.0 消费者在启动时就会和 Proxy 建立 TCP 长连接,查询订阅的 Topic 路由数据TopicRouteData。紧接着调用 telemetry 接口发送 SETTINGS 命令同步设置,要同步哪些设置呢?
RocketMQ 5.0 的客户端 SDK 要做轻量化,客户端最好啥也不知道,一些策略和配置最好靠服务端下发,同步设置就是干这个的。

请求体表明了消费者所属的消费组,以及消息订阅配置:

client_type: PUSH_CONSUMER
access_point {scheme: IPv4addresses {host: "127.0.0.1"port: 8081}
}
request_timeout {seconds: 3
}
subscription {group {name: "G_fifo"}subscriptions {topic {name: "fifo"}expression {type: TAGexpression: "*"}}
}
user_agent {language: JAVAversion: "5.0.4"platform: "Mac OS X 10.16"hostname: "localhost-5.local"
}

Proxy 返回的设置信息包含:消息消费失败的重试策略、消费者是否要顺序消费、单次最大消息拉取数量、以及无消息时的长轮询挂起时间。

client_type: PUSH_CONSUMER
access_point {scheme: IPv4addresses {host: "127.0.0.1"port: 8081}
}
backoff_policy {max_attempts: 17customized_backoff {next {seconds: 1}next {seconds: 5}next {seconds: 10}next {seconds: 30}next {seconds: 60}next {seconds: 120}next {seconds: 180}next {seconds: 240}next {seconds: 300}next {seconds: 360}next {seconds: 420}next {seconds: 480}next {seconds: 540}next {seconds: 600}next {seconds: 1200}next {seconds: 1800}next {seconds: 3600}next {seconds: 7200}}
}
request_timeout {seconds: 3
}
subscription {group {name: "G_fifo"}subscriptions {topic {name: "fifo"}expression {type: TAGexpression: "*"}}fifo: truereceive_batch_size: 32long_polling_timeout {seconds: 20}
}
user_agent {language: JAVAversion: "5.0.4"platform: "Mac OS X 10.16"hostname: "localhost-5.local"
}
metric {
}

对于顺序消息来说,最重要的配置项就是fifo: true,它决定了消费者是多线程并发消费还是单线程串行消费,消费者会根据配置创建对应的 ConsumeService。顾名思义,FifoConsumeService 是用来消费顺序消息的,StandardConsumeService 用来消费普通消息。

private ConsumeService createConsumeService() {final ScheduledExecutorService scheduler = this.getClientManager().getScheduler();if (pushSubscriptionSettings.isFifo()) {return new FifoConsumeService(clientId, messageListener, consumptionExecutor, this, scheduler);}return new StandardConsumeService(clientId, messageListener, consumptionExecutor, this, scheduler);
}

FifoConsumeService 会按照顺序消费拉取到的消息,而且会等待上一个消息消费完毕才会去消费下一个。

@Override
public void consume(ProcessQueue pq, List<MessageViewImpl> messageViews) {// 基于迭代器消费consumeIteratively(pq, messageViews.iterator());
}public void consumeIteratively(ProcessQueue pq, Iterator<MessageViewImpl> iterator) {if (!iterator.hasNext()) {return;}final MessageViewImpl messageView = iterator.next();if (messageView.isCorrupted()) {// 消息损坏consumeIteratively(pq, iterator);return;}// 触发MessageListener消费消息final ListenableFuture<ConsumeResult> future0 = consume(messageView);// 处理消费结果ListenableFuture<Void> future = Futures.transformAsync(future0, result -> pq.eraseFifoMessage(messageView,result), MoreExecutors.directExecutor());// 等待消息消费完毕再递归消费下一个消息future.addListener(() -> consumeIteratively(pq, iterator), MoreExecutors.directExecutor());
}

对于顺序消息来说,消费失败是个麻烦事儿。因为要保证消息的顺序,上一个消息没消费成功,下一个消息就无法被消费,容易导致消息堆积。RocketMQ 的策略是重试几次,还是不行就发到死信队列,方法是ProcessQueueImpl#eraseFifoMessage

@Override
public ListenableFuture<Void> eraseFifoMessage(MessageViewImpl messageView, ConsumeResult consumeResult) {statsConsumptionResult(consumeResult);final RetryPolicy retryPolicy = consumer.getRetryPolicy();// 最大重试次数final int maxAttempts = retryPolicy.getMaxAttempts();int attempt = messageView.getDeliveryAttempt();final MessageId messageId = messageView.getMessageId();final ConsumeService service = consumer.getConsumeService();final ClientId clientId = consumer.getClientId();// 失败且没超过最大重试次数if (ConsumeResult.FAILURE.equals(consumeResult) && attempt < maxAttempts) {// 下一个延迟时间final Duration nextAttemptDelay = retryPolicy.getNextAttemptDelay(attempt);attempt = messageView.incrementAndGetDeliveryAttempt();log.debug("Prepare to redeliver the fifo message because of the consumption failure, maxAttempt={}," +" attempt={}, mq={}, messageId={}, nextAttemptDelay={}, clientId={}", maxAttempts, attempt, mq,messageId, nextAttemptDelay, clientId);// 丢到线程池定时调度执行final ListenableFuture<ConsumeResult> future = service.consume(messageView, nextAttemptDelay);return Futures.transformAsync(future, result -> eraseFifoMessage(messageView, result),MoreExecutors.directExecutor());}boolean ok = ConsumeResult.SUCCESS.equals(consumeResult);// 超过重试次数还是失败 发到死信队列ListenableFuture<Void> future = ok ? ackMessage(messageView) : forwardToDeadLetterQueue(messageView);future.addListener(() -> evictCache(messageView), consumer.getConsumptionExecutor());return future;
}

至此,消费端对于顺序消息的处理就结束了。核心是如果消费组配置的是顺序投递,消费者在拉取到消息后会单线程同步消费消息。

顺序投递

消费者的顺序性还是比较容易保证的,整个链路里最复杂的必须是 Broker 投递的顺序性,因为 Broker 得记录队列里上一批拉取到的消息是否全部消费完,根据此来判断要不要继续投递后面的消息。
Broker 引入一个新组件 ConsumerOrderInfoManager,来管理消费者顺序消息的消费情况。它继承了 ConfigManager,所以支持数据的持久化。
image.png
它内部使用一个双层嵌套 Map 来记录消费组对于某个队列的顺序消息消费情况,所谓的数据持久化就是把这个 Map 序列化成 JSON 后落地到磁盘。

private ConcurrentHashMap<String/* topic@group*/, ConcurrentHashMap<Integer/*queueId*/, OrderInfo>> table =new ConcurrentHashMap<>(128);

落盘的文件路径是{storeHome}/config/consumerOrderInfo.json,内容大概长这样:

{"table":{"fifo@G_fifo":{0:{"cm":1,"i":60000,"l":1703644544701,"o":[460],"oc":{},"popTime":1703644544701}}}
}

核心是 OrderInfo 类,它记录了消费者针对某个队列拉取到的最新一批顺序消息的消费情况。offsetList 记录了消息的偏移量,可以根据此来定位消息;commitOffsetBit 记录了各消息的消费情况,它是一个位图,消息提交以后会把对应的比特位设为1。

public static class OrderInfo {// 各消息的偏移量(增量编码)private List<Long> offsetList;// 消耗次数private int consumedCount;// 最近一次消费的时间戳 其实是拉取时间private long lastConsumeTimestamp;// 消息提交位图private long commitOffsetBit;
}

消费者在拉取消息时,Broker 会给投递的这一批顺序消息记录一个 OrderInfo

private long popMsgFromQueue() {......if (isOrder) {// 顺序消息 给拉取到的这一批消息记录OrderInfoint count = brokerController.getConsumerOrderInfoManager().update(topic,requestHeader.getConsumerGroup(),queueId, getMessageTmpResult.getMessageQueueOffset());this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(),requestHeader.getConsumerGroup(), topic, queueId, offset);ExtraInfoUtil.buildOrderCountInfo(orderCountInfo, isRetry, queueId, count);} else {// 普通消息 追加CheckPointappendCheckPoint(requestHeader, topic, reviveQid, queueId, offset, getMessageTmpResult, popTime, this.brokerController.getBrokerConfig().getBrokerName());}......
}

方法是ConsumerOrderInfoManager#update,主要是构建一个 OrderInfo 对象存入 Map

public int update(String topic, String group, int queueId, List<Long> msgOffsetList) {String key = topic + TOPIC_GROUP_SEPARATOR + group;ConcurrentHashMap<Integer/*queueId*/, OrderInfo> qs = table.get(key);if (qs == null) {qs = new ConcurrentHashMap<>(16);ConcurrentHashMap<Integer/*queueId*/, OrderInfo> old = table.putIfAbsent(key, qs);if (old != null) {qs = old;}}OrderInfo orderInfo = qs.get(queueId);// 转增量编码List<Long> simple = OrderInfo.simpleO(msgOffsetList);if (orderInfo != null && simple.get(0).equals(orderInfo.getOffsetList().get(0))) {if (simple.equals(orderInfo.getOffsetList())) {orderInfo.setConsumedCount(orderInfo.getConsumedCount() + 1);} else {// reset, because msgs are changed.orderInfo.setConsumedCount(0);}orderInfo.setLastConsumeTimestamp(System.currentTimeMillis());orderInfo.setOffsetList(simple);orderInfo.setCommitOffsetBit(0);} else {// 构建新的OrderInfo覆盖掉上一批orderInfo = new OrderInfo();orderInfo.setOffsetList(simple);orderInfo.setLastConsumeTimestamp(System.currentTimeMillis());orderInfo.setConsumedCount(0);orderInfo.setCommitOffsetBit(0);qs.put(queueId, orderInfo);}return orderInfo.getConsumedCount();
}

假设此时又有其它消费者来拉取同一队列的消息,Broker 会先定位到对应的 OrderInfo,再判断是否要继续投递后面的消息:

private long popMsgFromQueue() {......if (isOrder && brokerController.getConsumerOrderInfoManager().checkBlock(topic,requestHeader.getConsumerGroup(), queueId, requestHeader.getInvisibleTime())) {// 之前拉取的一批消息还没全部commit,不能拉取新消息return this.brokerController.getMessageStore().getMaxOffsetInQueue(topic, queueId) - offset + restNum;}......
}

方法是ConsumerOrderInfoManager#checkBlock,只有当下面两个条件都满足,Broker 才会拒绝投递:

  • 上一批消息的拉取时间还没超过消息的不可见时间(60s)
  • 上一批消息还没全部提交
public boolean checkBlock(String topic, String group, int queueId, long invisibleTime) {String key = topic + TOPIC_GROUP_SEPARATOR + group;ConcurrentHashMap<Integer/*queueId*/, OrderInfo> qs = table.get(key);if (qs == null) {qs = new ConcurrentHashMap<>(16);ConcurrentHashMap<Integer/*queueId*/, OrderInfo> old = table.putIfAbsent(key, qs);if (old != null) {qs = old;}}OrderInfo orderInfo = qs.get(queueId);if (orderInfo == null) {// 当前队列还没拉取过,可以直接拉return false;}// 距离最后一次消费时间是否小于不可见时间60sboolean isBlock = System.currentTimeMillis() - orderInfo.getLastConsumeTimestamp() < invisibleTime;/*** 没超过不可见时间,则必须等这一批消息全部commit才能继续拉取*/return isBlock && !orderInfo.isDone();
}

判断消息是否全部提交的方法是OrderInfo#isDone,其实就是判断 commitOffsetBit 位图对应的位是否全部为1:

public boolean isDone() {if (offsetList == null || offsetList.isEmpty()) {return true;}int num = offsetList.size();for (byte i = 0; i < num; i++) {if ((commitOffsetBit & (1L << i)) == 0) {return false;}}return true;
}

消息投递后,消费者会按照顺序串行消费并上报消费结果,即 ack 消息。Broker 在处理消息的 ack 请求时会判断 ack 的是不是顺序消息,如果是就会更新 OrderInfo 位图。然后再判断 OrderInfo 里的这一批消息是否全部提交,如果是就提交消费位点,同时通知其它被挂起的请求拉取消息。

private RemotingCommand processRequest(){......if (rqId == KeyBuilder.POP_ORDER_REVIVE_QUEUE) {// 顺序消息String lockKey = requestHeader.getTopic() + PopAckConstants.SPLIT+ requestHeader.getConsumerGroup() + PopAckConstants.SPLIT + requestHeader.getQueueId();long oldOffset = this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getConsumerGroup(),requestHeader.getTopic(), requestHeader.getQueueId());if (requestHeader.getOffset() < oldOffset) {return response;}// 加锁while (!this.brokerController.getPopMessageProcessor().getQueueLockManager().tryLock(lockKey)) {}try {oldOffset = this.brokerController.getConsumerOffsetManager().queryOffset(requestHeader.getConsumerGroup(),requestHeader.getTopic(), requestHeader.getQueueId());if (requestHeader.getOffset() < oldOffset) {return response;}// 更新位图long nextOffset = brokerController.getConsumerOrderInfoManager().commitAndNext(requestHeader.getTopic(), requestHeader.getConsumerGroup(),requestHeader.getQueueId(), requestHeader.getOffset());if (nextOffset > -1) {// 这一批顺序消息全部消费掉了,提交消费位点this.brokerController.getConsumerOffsetManager().commitOffset(channel.remoteAddress().toString(),requestHeader.getConsumerGroup(), requestHeader.getTopic(),requestHeader.getQueueId(),nextOffset);// 通知其它被挂起的请求开始拉取消息this.brokerController.getPopMessageProcessor().notifyMessageArriving(requestHeader.getTopic(), requestHeader.getConsumerGroup(),requestHeader.getQueueId());} else if (nextOffset == -1) {String errorInfo = String.format("offset is illegal, key:%s, old:%d, commit:%d, next:%d, %s",lockKey, oldOffset, requestHeader.getOffset(), nextOffset, channel.remoteAddress());POP_LOGGER.warn(errorInfo);response.setCode(ResponseCode.MESSAGE_ILLEGAL);response.setRemark(errorInfo);return response;}} finally {this.brokerController.getPopMessageProcessor().getQueueLockManager().unLock(lockKey);}return response;}......
}

更新位图的方法是ConsumerOrderInfoManager#commitAndNext,它会把 commitOffsetBit 对应的比特位设为1,然后返回值代表消息是否全被消费掉了,通知外层要提交消费位点。-2 代表还没消费完、大于等于0表示需要提交消费位点。

public long commitAndNext(String topic, String group, int queueId, long offset) {String key = topic + TOPIC_GROUP_SEPARATOR + group;ConcurrentHashMap<Integer/*queueId*/, OrderInfo> qs = table.get(key);if (qs == null) {return offset + 1;}OrderInfo orderInfo = qs.get(queueId);if (orderInfo == null) {log.warn("OrderInfo is null, {}, {}, {}", key, offset, orderInfo);return offset + 1;}List<Long> offsetList = orderInfo.getOffsetList();if (offsetList == null || offsetList.isEmpty()) {log.warn("OrderInfo is empty, {}, {}, {}", key, offset, orderInfo);return -1;}Long first = offsetList.get(0);int i = 0, size = offsetList.size();for (; i < size; i++) {long temp;if (i == 0) {temp = first;} else {temp = first + offsetList.get(i);}if (offset == temp) {break;}}if (i >= size) {log.warn("OrderInfo not found commit offset, {}, {}, {}", key, offset, orderInfo);return -1;}// 更新Commit位图 对应位设为1orderInfo.setCommitOffsetBit(orderInfo.getCommitOffsetBit() | (1L << i));if (orderInfo.isDone()) {// 这一批消息全部Commit了if (size == 1) {return offsetList.get(0) + 1;} else {return offsetList.get(size - 1) + first + 1;}}// 无需commitreturn -2;
}

尾巴

RocketMQ 顺序消息需要多端共同保证,包括:生产端顺序性、存储端顺序性、投递端顺序性、消费端顺序性。5.0 和 4.x 最大的区别就是,Pop 模式下的消息模型允许消费者消费所有队列,Broker 投递的顺序性是实现难点。RocketMQ 给出的解决方案是用一个嵌套 Map 维护 OrderInfo,用来管理消费组针对某个队列的消费情况。Broker 在投递消息前会针对这一批消息构建一个 OrderInfo 对象存储下来,在收到消费者发送的 ack 请求时更新对应的位图。下一个消费者来拉取消息时,Broker 会判断对应的 OrderInfo 里的消息是否全部提交,如果还有消息没提交,是不会投递后面的消息的,以此来保证消息投递的顺序性。

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

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

相关文章

RocketMQ5.0Pop消费模式

前言 RocketMQ 5.0 消费者引入了一种新的消费模式&#xff1a;Pop 消费模式&#xff0c;目的是解决 Push 消费模式的一些痛点。 RocketMQ 4.x 之前&#xff0c;消费模式分为两种&#xff1a; Pull&#xff1a;拉模式&#xff0c;消费者自行拉取消息、上报消费结果Push&#x…

分布式(8)

目录 36.什么是TCC&#xff1f; 37.分布式系统中常用的缓存方案有哪些&#xff1f; 38.分布式系统缓存的更新模式&#xff1f; 39.分布式缓存的淘汰策略&#xff1f; 40.Java中定时任务有哪些&#xff1f;如何演化的&#xff1f; 36.什么是TCC&#xff1f; TCC&#xff08…

【算法挨揍日记】day41——【模板】01背包、416. 分割等和子集

【模板】01背包_牛客题霸_牛客网你有一个背包&#xff0c;最多能容纳的体积是V。 现在有n个物品&#xff0c;第i个物品的体积为 ,。题目来自【牛客题霸】https://www.nowcoder.com/practice/fd55637d3f24484e96dad9e992d3f62e?tpId230&tqId2032484&ru/exam/oj&qru…

HarmonyOS-ArkTS基本语法及声明式UI描述

初识ArkTS语言 ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript&#xff08;简称TS&#xff09;生态基础上做了进一步扩展&#xff0c;继承了TS的所有特性&#xff0c;是TS的超集。因此&#xff0c;在学习ArkTS语言之前&#xff0c;建议开发者具备TS语…

机器学习常用算法模型总结

文章目录 1.基础篇&#xff1a;了解机器学习1.1 什么是机器学习1.2 机器学习的场景1.2.1 模式识别1.2.2 数据挖掘1.2.3 统计学习1.2.4 自然语言处理1.2.5 计算机视觉1.2.6 语音识别 1.3 机器学习与深度学习1.4 机器学习和人工智能1.5 机器学习的数学基础特征值和特征向量的定义…

软件测试/测试开发丨Python 模块与包

python 模块与包 python 模块 项目目录结构 组成 package包module模块function方法 模块定义 定义 包含python定义和语句的文件.py文件作为脚本运行 导入模块 import 模块名from <模块名> import <方法 | 变量 | 类>from <模块名> import * 注意&a…

小红书如何高效引流?

近年来&#xff0c;公域流量价格不断上涨&#xff0c;私域流量的优势逐渐凸显。企业正花费大量资源和成本来获取新流量&#xff0c;但与其如此&#xff0c;不如将精力放在留存和复购上&#xff0c;从而实现业绩的新增长。其中关键在于如何有效地将公域流量转化为私域流量。 然而…

境内深度合成服务算法备案清单(2023年12月)

截止2024年1月3日&#xff0c;第三批深度合成服务算法备案信息的公告尚未发布&#xff0c;预计将会在2024-1-10左右发布&#xff0c;我公司已知晓部分公示名单&#xff0c;如中国电信数字人生成算法&#xff0c;详情联系WX号&#xff1a;SuanfabeiandayuAI生成合成类算法应办理…

ArkTS - @Prop、@Link

一、作用 Prop 装饰器 和Link装饰器都是父组件向子组件传递参数&#xff0c;子组件接收父组件参数的时候用的&#xff0c;变量前边需要加上Prop或者Link装饰器即可。&#xff08;跟前端vue中父组件向子组件传递参数类似&#xff09; // 子组件 Component struct SonCom {Prop…

管程-第三十三天

目录 为什么要引入管程 管程的定义和基本特征 用管程解决生产者消费者问题 结论 本节思维导图 为什么要引入管程 原因&#xff1a;在解决进程的同步与互斥问题时&#xff0c;信号量机制存在编写困难和易出错的问题 能不能设计一种机制&#xff0c;让程序员写程序时不再需…

openGauss学习笔记-184 openGauss 数据库运维-升级-升级验证

文章目录 openGauss学习笔记-184 openGauss 数据库运维-升级-升级验证184.1 验证项目的检查表184.2 升级版本查询184.2.1 验证步骤 184.3 检查升级数据库状态184.3.1 验证步骤 openGauss学习笔记-184 openGauss 数据库运维-升级-升级验证 本章介绍升级完成后的验证操作。给出验…

VINS-MONO拓展1----手写后端求解器,LM3种阻尼因子策略,DogLeg,构建Hessian矩阵

文章目录 0. 目标及思路1. 非线性优化求解器2. 基于VINS-MONO的Marginalization框架构建Hessian矩阵2.1 estimator.cpp移植2.2 solve.cpp/preMakeHessian()2.3 solve.cpp/makeHessian() 3. solve.cpp/solveLinearSystem()求解正规方程4. 更新状态5. 迭代求解6. EVO评估结果7. 待…

虹科方案丨从困境到突破:TigoLeap方案引领数据采集与优化变革

来源&#xff1a;虹科工业智能互联 虹科方案丨从困境到突破&#xff1a;TigoLeap方案引领数据采集与优化变革 原文链接&#xff1a;https://mp.weixin.qq.com/s/H3pd5G8coBvyTwASNS_CFA 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; 导读 在数字化工厂和智能制造时…

connection refused

nohup /home/bavon/miniconda3/envs/SLFCD/bin/python -m visdom.server -port 8098 >/home/bavon/logs/visdom.log 2>&1 &

8086CPU的寻址方式(7种)

基本概念 立即操作数&#xff1a;操作数包含在指令中寄存器操作数&#xff1a;操作数包含在CPU的某个内部寄存器中存储器操作数&#xff1a;约定操作数事先存放在存储器中存放数据的某个单元基本格式 MOV xx,yy xx&#xff1a;目的操作数字段 yy&#xff1a;源操作数字段 EA&a…

whl is not a supported wheel on this platform.解决办法

1.问题&#xff1a; 安装torch产生 2.解决办法&#xff1a; 使用pip debug --verbose查看 对应的torch版本号 Compatible tags字样&#xff0c;这些就是当前Python版本可以适配的标签。例如&#xff0c;我的Python版本是3.11&#xff0c;可以匹配下面这些文件名&#xff1a;…

Nginx多域名部署多站点

目录 1.修改配置文件nginx.conf 2. 修改hosts文件 1.修改配置文件nginx.conf 在配置文件的 server_name 处修改成自己需要的域名&#xff0c;然后保存退出 j 查看语法是否错误&#xff0c;然后重启nginx nginx -t # 查看语法是否正确 systemctl restart nginx # 重启nginx …

【面试】面向对象编程的三大概念(实例辅助记忆)

【面试】面向对象编程的三大概念&#xff08;实例辅助记忆&#xff09; 虑面向对象编程的三大特性&#xff0c;它们是&#xff1a; 封装&#xff08;Encapsulation&#xff09;&#xff1a; 将对象的状态和行为封装在一起&#xff0c;对外部隐藏对象的内部实现细节。这样可以防…

Power Automate删除SharePoint Online或OneDrive for Business文件版本历史

SharePoint Online和OneDrive for Business支持版本控制&#xff0c;可以保留文件的版本历史&#xff0c;方便用户随时查看和恢复以前的版本。但该功能也会占用大量SharePoint Online或OneDrive for Business存储空间。官方删除版本历史的方法无法批量操作&#xff0c;故今天提…

音效出众设计时尚,内置AI功能,sanag塞那Z50上手

现在蓝牙耳机已经成为人们生活中不可或缺的一部分了&#xff0c;像是在上班、坐车的时候&#xff0c;既可以享受自己的音乐空间&#xff0c;又不会吵到别人&#xff0c;看书、做题还是运动的时候&#xff0c;也可以保证长时间使用耳朵卫生、舒适度。正因为庞大的市场需求&#…