RocketMQ5.0顺序消息设计实现

news2024/7/6 19:31:24

前言

顺序消息是 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) {
            // 分片键 也就是MessageGroup
            shardingKey = 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: IPv4
  addresses {
    host: "127.0.0.1"
    port: 8081
  }
}
request_timeout {
  seconds: 3
}
subscription {
  group {
    name: "G_fifo"
  }
  subscriptions {
    topic {
      name: "fifo"
    }
    expression {
      type: TAG
      expression: "*"
    }
  }
}
user_agent {
  language: JAVA
  version: "5.0.4"
  platform: "Mac OS X 10.16"
  hostname: "localhost-5.local"
}

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

client_type: PUSH_CONSUMER
access_point {
  scheme: IPv4
  addresses {
    host: "127.0.0.1"
    port: 8081
  }
}
backoff_policy {
  max_attempts: 17
  customized_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: TAG
      expression: "*"
    }
  }
  fifo: true
  receive_batch_size: 32
  long_polling_timeout {
    seconds: 20
  }
}
user_agent {
  language: JAVA
  version: "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) {
        // 顺序消息 给拉取到的这一批消息记录OrderInfo
        int 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 {
        // 普通消息 追加CheckPoint
        appendCheckPoint(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;
    }
    // 距离最后一次消费时间是否小于不可见时间60s
    boolean 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位图 对应位设为1
    orderInfo.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;
        }
    }
    // 无需commit
    return -2;
}

尾巴

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

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

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

相关文章

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;…

YOLOv8改进 更换轻量化模型MobileNetV3

一、MobileNetV3论文 论文地址&#xff1a;1905.02244.pdf (arxiv.org) 二、 MobileNetV3网络结构 MobileNetV3引入了一种新的操作单元&#xff0c;称为"Mobile Inverted Residual Bottleneck"&#xff0c;它由一个1x1卷积层和一个3x3深度可分离卷积层组成。这个操…

简易电子琴

#include<reg51.h> //包含51单片机寄存器定义的头文件 sbit P14P1^4; //将P14位定义为P1.4引脚 sbit P15P1^5; //将P15位定义为P1.5引脚 sbit P16P1^6; //将P16位定义为P1.6引脚 sbit P17P1^7; //将P17位定义为P1.7引脚 unsigned char keyval; …

Sam Altman的一天被曝光!每天15小时禁食、服用小剂量安眠药,尽可能避免开会

Sam Altman在经历了几天混乱的管理重组后&#xff0c;重新回到了OpenAI的CEO位置。在日常生活中&#xff0c;奥特曼与许多科技行业高管一样&#xff0c;痴迷于延长自己的寿命。 据报道&#xff0c;他还为应对末日场景&#xff08;致命合成病毒的释放、核战争和人工智能攻击等&…

学习Go语言Web框架Gee总结--前缀树路由Router(三)

学习Go语言Web框架Gee总结--前缀树路由Router router/gee/trie.gorouter/gee/router.gorouter/gee/context.gorouter/main.go 学习网站来源&#xff1a;Gee 项目目录结构&#xff1a; router/gee/trie.go 实现动态路由最常用的数据结构&#xff0c;被称为前缀树(Trie树) 关…

用Audio2Face驱动UE - MetaHuman

新的一年咯&#xff0c;很久没发博客了&#xff0c;就发两篇最近的研究吧。 开始之前说句话&#xff0c;别轻易保存任何内容&#xff0c;尤其是程序员不要轻易Ctrl S 在UE中配置Audio2Face 先检查自身电脑配置看是否满足&#xff0c;按最小配置再带个UE可能会随时崩&#x…

类的生命周期/加载

理一下&#xff0c;java 编译后的字节码文件&#xff0c;我们已经熟悉了 字节码文件长什么样&#xff0c;字节码文件中有哪些内容&#xff0c;那么下一步就是使用类加载器 把字节码文件加载到JVM中 类的生命周期 类的生命周期是 JVM类加载的基础。 加载 所谓加载&#xff0c;…

贝叶斯推断:细谈贝叶斯变分和贝叶斯网络

1. 贝叶斯推断 统计推断这件事大家并不陌生&#xff0c;如果有一些采样数据&#xff0c;我们就可以去建立模型&#xff0c;建立模型之后&#xff0c;我们通过对这个模型的分析会得到一些结论&#xff0c;不管我们得到的结论是什么样的结论&#xff0c;我们都可以称之为是某种推…

【深度学习下载大型数据集】快速下载谷歌云盘数据集

个人博客:Sekyoro的博客小屋 个人网站:Proanimer的个人网站 跑深度学习的时候,一些数据集比较大,比如60多个G,而且只是训练集. 然后这些数据是由某些实验室组采集的,并不像一些大公司搞的,一般都直接方法一些网盘中. 如果是谷歌网盘,本身通过代理也不麻烦,但是发现即使通过代…

为什么大学c语言课不顺便教一下Linux,Makefile

为什么大学c语言课不顺便教一下Linux&#xff0c;Makefile&#xff0c;git&#xff0c;gdb等配套工具链呢? 在开始前我有一些资料&#xff0c;是我根据自己从业十年经验&#xff0c;熬夜搞了几个通宵&#xff0c;精心整理了一份「Linux的资料从专业入门到高级教程工具包」&…

【2024.01.03】转行小白-刷css面试题01

总结 1.margin 负值问题 margin-top 和 margin-left 负值&#xff0c;元素向上、向左移动&#xff0c;自己动margin-right 负值&#xff0c;右侧元素左移&#xff0c;自身不受影响&#xff0c;别人动margin-bottom 负值&#xff0c;下方元素上移&#xff0c;自身不受影响 &am…

第十四章 :案例课:部暑KVM虚拟化平台

[rootLinux01 ~]# mount /dev/cdrom /mnt //挂载安装KVM需要的软件 [rootLinux01 ~]# yum -y install qemu-kvm-tools [rootLinux01 ~]# yum -y install qemu-kvm [rootLinux01 ~]# yum -y install virt-install [rootLinux01 ~]# yum -y install qemu-img [rootLinux01 ~]#…

求一个整数二进制中1的个数(三种方法详解)

越过寒冬 前言 今天复习了一些操作符的知识&#xff0c;看到了这道题&#xff0c;并且发先有三种解题思路&#xff0c;觉得有趣&#xff0c;据记下来与诸位分享一下。 题目 写一个函数&#xff0c;给定一个整数&#xff0c;求他的二进制位中1的个数 思路1 既然是二进制位那…

JOSEF约瑟 断电延时继电器 SRTD-220VDC-2H2D 导轨安装

系列型号&#xff1a; SRTD-24VDC-1H1D断电延时继电器&#xff1b;SRTD-110VDC-1H1D断电延时继电器&#xff1b; SRTD-220VDC-1H1D断电延时继电器&#xff1b;SRTD-110VAC-1H1D断电延时继电器&#xff1b; SRTD-220VAC-1H1D断电延时继电器&#xff1b;SRTD-24VDC-2H断电延时继电…

一文搞懂手机卡的定向流量到底是什么!

最近有一些小伙伴对于手机卡流量中包含的定向流量这个概念不是很明白&#xff0c;而且也不知道具体如何使用&#xff0c;今天这个视频&#xff0c;葫芦弟就仔细给大家讲解一下&#xff0c;希望能解开小伙伴们心中的疑惑。废话不多说&#xff0c;我们直接进入正题&#xff01; 首…

网络安全—模拟ARP欺骗

文章目录 网络拓扑安装使用编辑数据包客户机攻击机验证 仅做实验用途&#xff0c;禁止做违法犯罪的事情&#xff0c;后果自负。当然现在的计算机多无法被欺骗了&#xff0c;开了防火墙ARP欺骗根本无效。 网络拓扑 均使用Windows Server 2003系统 相关配置可以点击观看这篇文章…

git rebase(变基)应用场景

文章目录 git rebase(变基)应用场景1.git rebase -i HEAD~3 git rebase(变基)应用场景 使得提交记录变得简洁 现在我们模拟我们有多次提交记录&#xff0c;本地仓库有三条提交 整合成一条提交记录 1.git rebase -i HEAD~3 提交记录合并 HEAD~3合并三条记录 执行之后 然后把…

【Python机器学习】构建简单的k近邻算法模型

k近邻算法是一个很容易理解的算法&#xff0c;构建模型只需要保存训练数据集。要对一个新的数据点做出预测&#xff0c;算法会在训练集中寻找与这个新数据点距离最近的数据点&#xff0c;然后将找到的数据点的标签赋值给这个新数据点。 l近邻算法中k的含义是&#xff1a;我们可…

Ubuntu 常用命令之 locate 命令用法介绍

&#x1f525;Linux/Ubuntu 常用命令归类整理 locate命令是在Ubuntu系统下用于查找文件或目录的命令。它使用一个预先构建的数据库&#xff08;通常由updatedb命令创建&#xff09;来查找文件或目录&#xff0c;因此它的查找速度非常快。 plocate 安装 locate 不是 Ubuntu 系…

jdk动态代理中invoke的return返回的值有什么用?

目录 首先在接口中定义一个行为再定义一个目标角色实现接口&#xff0c;实现行为去代理角色类中解决一下报错&#xff0c;但是什么都不要写 invoke的return返回的值是调用方法中返回的值 下面我们来实例看一下 首先在接口中定义一个行为 public String toMarry02();再定义一个…