Kafka 如何实现顺序消息

版本说明

本文所有的讨论均在如下版本进行,其他版本可能会有所不同。

  • Kafka: 3.6.0
  • Pulsar: 2.9.0
  • RabbitMQ 3.7.8
  • RocketMQ 5.0
  • Go1.21
  • github.com/segmentio/kafka-go v0.4.45

结论先行

Kafka 只能保证单一分区内的顺序消息,无法保证多分区间的顺序消息。具体来说,要在 Kafka 完全实现顺序消息,至少需要保证以下几个条件:

  1. 同一生产者生产消息;
  2. 同步发送消息到 Kafka broker;
  3. 所有消息发布到同一个分区;
  4. 同一消费者同步按照顺序消费消息。

而要满足第 3 点,常用的有 2 种思路:

  1. 固定消息的 key,生产端采用 key hash 的方式写入 broker;
  2. 自定义分区策略,要保证顺序的消息都写入到指定的分区。

消息队列中的顺序消息如何实现

顺序消息定义

生产端发送出来的消息的顺序和消费端接收到消息的顺序是一样的。

消息存储结构

一般来说,消息队列都是基于顺序存储结构来存储数据的,不需要 B 树、B+ 树等复杂数据结构,利用文件的顺序读写,性能也很高。所以理想情况下,生产者按顺序发送消息,broker 会按顺序存储消息,消费者再按顺序消费消息,那么天然就实现了我们要的顺序消息了,如下:

Kafka 消息存储结构

基本条件

但是一般情况下,消息队列为了支持更高的并发和吞吐,大多数都有分区(partition)和消费者组(consumer group)机制,而为了高可用,一般也会有副本(replica)机制,所以情况就复杂得多了,如下面几个例子,就会导致消息失序:

  1. 多个生产者同时发送消息,那么到达 broker 的时间也是不确定的,所以 broker 就无法保证落盘的顺序性了;
  2. 单个生产者,但是采用异步发送,因为异步线程是并发执行的,由 CPU 进行调度,且有可能会因为发送失败而重试,所以也无法保证消息可以按照顺序到达 broker,同理,消费者异步处理消息,也无法保证顺序性;
  3. 一个 topic 有多个分区,那么即使是同一个生产者,由于分区策略,消息可能会被分发到多个分区中,消费者也就无法保证顺序性了。

所以到这里,我们可以总结出实现顺序消息,至少需要满足以下 3 点:

  1. 单一生产者同步发送;
  2. 单一分区;
  3. 单一消费者同步消费;

第 1、3 点比较简单,Kafka 通过分区和 offset 的方式保证了消息的顺序。每个分区都是一个有序的、不可变的消息序列,每个消息在分区中都有一个唯一的序数标识,称为 offset。生产者在发送消息到分区时,Kafka 会自动为消息分配一个 offset。消费者在读取消息时,会按照 offset 的顺序来读取,从而保证了消息的顺序。

下面我们主要来谈一谈第 2 点。

Kafka 顺序消息的实现

写入消息的过程

  1. 配置生产者:首先,你需要配置 Kafka 生产者。这包括指定 Kafka 集群的地址和端口,以及其他相关配置项,如消息序列化器、分区策略等。
  2. 创建生产者实例:在应用程序中,你需要创建一个 Kafka 生产者的实例。这个实例将用于与 Kafka 集群进行通信。
  3. 序列化消息:在将消息发送到 Kafka 集群之前,你需要将消息进行序列化。Kafka 使用字节数组来表示消息的内容,因此你需要将消息对象序列化为字节数组。这通常涉及将消息对象转换为 JSON、Avro、Protobuf 等格式。
  4. 选择分区:Kafka 的主题(topic)被分为多个分区(partition),每个分区都是有序且持久化的消息日志。当你发送消息时,你可以选择将消息发送到特定的分区,或者让 Kafka 根据分区策略自动选择分区。
  5. 发送消息:一旦消息被序列化并选择了目标分区,你可以使用 Kafka 生产者的 send() 方法将消息发送到 Kafka 集群。发送消息时,生产者会将消息发送到对应分区的 leader 副本。
  6. 异步发送:Kafka 生产者通常使用异步方式发送消息,这样可以提高吞吐量。生产者将消息添加到一个发送缓冲区(send buffer)中,并在后台线程中批量发送消息到 Kafka 集群。
  7. 消息持久化:一旦消息被发送到 Kafka 集群的 leader 副本,它将被持久化并复制到其他副本,以确保数据的高可靠性和冗余性。只有当消息被成功写入到指定数量的副本后,生产者才会收到确认(acknowledgement)。
  8. 错误处理和重试:如果发送消息时发生错误,生产者可以根据配置进行错误处理和重试。你可以设置重试次数、重试间隔等参数来控制重试行为。

Kafka 生产者组件图 -《Kafka 权威指南第2版》

实现单一分区

再 Kafka 中,我们要实现将消息写入到同一个分区,有 3 种思路:

  • 配置 num.partitions=1 或者创建 topic 的时候指定只有 1 个分区,但这会显著降低 Kafka 的吞吐量。
  • 固定消息的 key,然后采用 key hash 的分区策略,这样就可以让所有消息都被分到同一个分区中。
  • 实现并指定自定义分区策略,可以根据业务需求,将需要顺序消费的消息都分到固定一个分区中。
// 如下例子,所有使用"same-key"作为key的消息都会被发送到同一个Partition
ProducerRecord<String, String> record = new ProducerRecord<String, String>("topic", "same-key", "message");
producer.send(record);

重平衡带来的问题

如果采用上述的第 2 种思路:固定消息 key,依靠 key hash 分区策略,实现单一分区。在我们只有 1 个消费者的情况下是没有问题的,但是如果我们使用的是消费者组,那么,在发生重平衡操作的时候,就可能会有问题了。

Kafka 的重平衡(Rebalance)是指 Kafka 消费者组(Consumer Group)中的消费者实例对分区的重新分配。这个过程主要发生在以下几种情况:

  1. 消费者组中新的消费者加入。
  2. 消费者组中的消费者离开或者挂掉。
  3. 订阅的 Topic 的分区数发生变化。
  4. 消费者调用了 #unsubscribe() 或者 #subscribe() 方法。

重平衡的过程主要包括以下几个步骤:

  1. Revoke:首先,Kafka 会撤销消费者组中所有消费者当前持有的分区。
  2. Assignment:然后,Kafka 会重新计算分区的分配情况,然后将分区分配给消费者。
  3. Resume:最后,消费者会开始消费新分配到的分区。

重平衡的目的是为了保证消费者组中的消费者能够公平地消费 Topic 的分区。通过重平衡,Kafka 可以在消费者的数量发生变化时,动态地调整消费者对分区的分配,从而实现负载均衡。

然而,当发生重平衡时,分区可能会被重新分配给不同的消费者,这可能会影响消息的消费顺序。

举个例子:

  1. 假设消费者 A 正在消费分区 P 的消息,它已经消费了消息 1,消息 2,正在处理消息 3。
  2. 此时,发生了重平衡,分区 P 被重新分配给了消费者 B。
  3. 消费者 B 开始消费分区 P,它会从上一次提交的偏移量(offset)开始消费。假设消费者 A 在处理消息 3 时发生了故障,没有提交偏移量,那么消费者 B 会从消息 3 开始消费。
  4. 这样,消息 3 可能会被消费两次,而且如果消费者 B 处理消息 3 的速度快于消费者A,那么消息 3 可能会在消息 2 之后被处理,这就打破了消息的顺序性。

Kafka 重平衡导致消息失序

再举个例子:

  1. topic-A 本来只有 3 个分区,按照 key hash,key 为 same-key 的消息应该都发到 第 2 个分区;
  2. 但是后来 topic-A 变成了 4 个分区,按照 key hash,key 为 same-key 的消息可能就被发到第 3 个分区了;
  3. 这就无法做到单一分区,可能会导致消息失序。

当然这个例子不是由重平衡直接引起的,但是这种情况也是有可能导致消息失序的。

缓解重平衡的问题

  • 避免动态改变分区数:在需要严格保持消息顺序的场景下,应避免动态地改变分区数。这意味着在设计 Kafka 主题时,应提前规划好所需的分区数,以避免日后需要进行更改。
  • 使用单个分区:对于严格顺序要求的场景,可以考虑使用单分区主题。虽然这会限制吞吐量和并发性,但可以保证消息的全局顺序。
  • 使用其他策略保持顺序:在某些情况下,可以通过在应用层实现逻辑来保持顺序,比如在消息中包含顺序号或时间戳,并在消费时根据这些信息重建正确的顺序。
  • 使用静态成员功能:它允许消费者在断开和重新连接时保持其消费者组内的身份,这可以减少因短暂的网络问题或消费者重启导致的不必要的重平衡。

上面这些措施,只能减少重平衡带来的问题,并无法根除,如果非要实现严格意义上的顺序消息,要么在消息中加入时间戳等标记,在业务层保证顺序消费,要么就只能采用 单一生产者同步发送 + 单一分区 +单一消费者同步消费 这种模式了。

静态成员功能

Kafka 2.3.0 版本引入了一项新功能:静态成员(Static Membership)。这个功能主要是为了减少由于消费者重平衡(rebalance)引起的开销和延迟。在传统的 Kafka 消费者组中,当新的消费者加入或离开消费者组时,会触发重平衡。这个过程可能会导致消息的处理延迟,并且在高吞吐量的场景下可能会对性能造成影响。静态成员功能旨在缓解这些问题。以下是它的一些关键点:

静态成员的工作原理:

  1. 静态成员标识:消费者在加入消费者组时可以提供一个静态成员标识(Static Member ID)。这允许 Kafka Broker 识别特定的消费者实例,而不是仅仅依赖于消费者组内的动态分配。

  2. 重平衡优化:当使用静态成员功能时,如果一个已知的消费者由于某种原因(如网络问题)短暂断开后重新连接,Kafka 不会立即触发重平衡。相反,Kafka 会等待一个预设的超时期限(session.timeout.ms),在此期间如果消费者重新连接,它将保留原来的分区分配。

  3. 减少重平衡次数:这大大减少了由于消费者崩溃和恢复、网络问题或维护操作引起的不必要的重平衡次数。

使用静态成员的优点:

  1. 提高稳定性:减少重平衡可以提高消费者组的整体稳定性,尤其是在大型消费者组和高吞吐量的情况下。

  2. 减少延迟:由于减少了重平衡的次数,可以减少因重平衡导致的消息处理延迟。

  3. 持久的消费者分区分配:这使得消费者在分区分配上更加持久,有助于更好地管理和优化消息的消费。

如何使用:

  • 要使用静态成员功能,需要在 Kafka 消费者的配置中设置 group.instance.id。这个 ID 应该是唯一的,并且在消费者重启或重新连接时保持不变。同时,还需要配置 session.timeout.ms,以决定在触发重平衡之前消费者可以离线多长时间。

注意事项:

  • 虽然静态成员功能可以减少重平衡的发生,但它不会完全消除重平衡。在消费者组成员的长期变化(如新消费者的加入或永久离开)时,仍然会发生重平衡。
  • 需要合理设置 session.timeout.ms,以避免消费者由于短暂的网络问题或其他原因的断开而过早触发重平衡。

静态成员功能在处理大规模 Kafka 应用时尤其有用,它提供了一种机制来优化消费者组的性能和稳定性。

幂等性

Kafka 0.11 版本后提供了幂等性生产者,这意味着即使生产者因为某些错误重试发送相同的消息,这些消息也只会被记录一次。这是通过给每一批发送到 Kafka 的消息分配一个序列号实现的,broker 使用这个序列号来删除重复发送的消息。使用幂等性生产者,可以减少重复消息的风险,这意味着即使在网络重试等情况下,消息的顺序也能得到更好的保证。因为重复消息不会被多次记录,所以不会破坏已有消息的顺序。

其他常见消息队列顺序消息的实现

Pulsar

Pulsar 和 Kafka 一样,都是通过生产端按 Key Hash 的方案将数据写入到同一个分区。

RabbitMQ

RabbitMQ 在生产时没有生产分区分配的过程。它是通过 ExchangeRoute Key 机制来实现顺序消息的。Exchange 会根据设置好的 Route Key 将数据路由到不同的 Queue 中存储。此时 Route Key 的作用和 Kafka 的消息的 Key 是一样的。

RocketMQ

RocektMQ 支持消息组(MessageGroup)的概念。在生产端指定消息组,则同一个消息组的消息就会被发送到同一个分区中。此时这个消息组起到的作用和 Kakfa 的消息的 Key 是一样的。

实战 Kafka 实现顺序消息

代码仓库:https://github.com/hedon954/kafka-go-examples/tree/master/orderedmsg

下面我们来写一写实战用例,更加直观地感受一下 Kafka 顺序消息的实现细节。

首先我们在集群上创建一个 topic ordered-msg-topic,分区为 3 个,运行以下命令:

/opt/kafka-3.6.0/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic ordered-msg-topic --partitions 3 --replication-factor 1

搭建 Kafka 集群可以看这两篇:Kafka集群搭建(Zookeeper)、Kafka集群搭建(KRaft)。

单生产者单消费者

正常情况下,使用单一生产者同步发送和单一消费者同步发送,只要我们保证 key 是固定的,则所有消息都会写到同一个分区,是可以实现顺序消息的。

代码目录如下:

├─config
│      config.go		# 常量定义
├─consumer
│      consumer.go		# 消费者
└─producer
        producer.go		# 生产者

首先我们先定义一些常量:

import "github.com/segmentio/kafka-go"

var (
	Topic      = "ordered-msg-topic"
	Brokers    = []string{"kafka1.com:9092", "kafka2.com:9092", "kafka3.com:9092"}
	Addr       = kafka.TCP(Brokers...)
	GroupId    = "ordered-msg-group"
	MessageKey = []byte("message-key")
)

我们先实现生产者端,主要是不断往 ordered-msg-topic 中写入数据:

package main

import (
	"context"
	"fmt"
	"time"

	"kafka-go-examples/orderedmsg/config"

	"github.com/segmentio/kafka-go"
)

func NewProducer() *kafka.Writer {
	return &kafka.Writer{
		Addr:     config.Addr,
		Topic:    config.Topic,
		Balancer: &kafka.Hash{}, // 哈希分区
	}
}

func NewMessages(count int) []kafka.Message {
	res := make([]kafka.Message, count)
	for i := 0; i < count; i++ {
		res[i] = kafka.Message{
			Key:   config.MessageKey,
			Value: []byte(fmt.Sprintf("msg-%d", i+1)),
		}
	}
	return res
}

func main() {
	producer := NewProducer()
	messages := NewMessages(100)
	if err := producer.WriteMessages(context.Background(), messages...); err != nil {
		panic(err)
	}
	_ = producer.Close()
}

我们再来实现消费者,目前我们就启动 1 个消费者:

package main

import (
	"context"
	"fmt"
	"time"

	"kafka-go-examples/orderedmsg/config"

	"github.com/segmentio/kafka-go"
)

type Consumer struct {
	Id string
	*kafka.Reader
}

// NewConsumer 创建一个消费者,它属于 config.GroupId 这个消费者组
func NewConsumer(id string) *Consumer {
	c := &Consumer{
		Id: id,
		Reader: kafka.NewReader(kafka.ReaderConfig{
			Brokers: config.Brokers,
			GroupID: config.GroupId,
			Topic:   config.Topic,
			Dialer: &kafka.Dialer{
				ClientID: id,
			},
		}),
	}
	return c
}

// Read 读取消息,intervalMs 用来控制消费者的消费速度
func (c *Consumer) Read(intervalMs int) {
	fmt.Printf("%s start read\n", c.Id)
	for {
		msg, err := c.ReadMessage(context.Background())
		if err != nil {
			fmt.Printf("%s read msg err: %v\n", c.Id, err)
			return
		}
		// 模拟消费速度
		time.Sleep(time.Millisecond * time.Duration(intervalMs))
		fmt.Printf("%s read msg: %s, time: %s\n", c.Id, string(msg.Value), time.Now().Format("03-04-05"))
	}
}

func main() {
	c1 := NewConsumer("consumer-1")
	c1.Read(500)
}

启动生产者生产消息,然后启动消费者,观察控制台,不难看出这种情况下就是顺序消费:

consumer-1 read msg: msg-10, time: 04:29:10
consumer-1 read msg: msg-11, time: 04:29:11
consumer-1 read msg: msg-12, time: 04:29:12
consumer-1 read msg: msg-13, time: 04:29:13
consumer-1 read msg: msg-14, time: 04:29:14
consumer-1 read msg: msg-15, time: 04:29:15
consumer-1 read msg: msg-16, time: 04:29:16

重平衡带来的问题

我们先重建 topic,清楚掉之前的数据:

/opt/kafka-3.6.0/bin/kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic ordered-msg-topic
/opt/kafka-3.6.0/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic ordered-msg-topic --partitions 3 --replication-factor 1

下面我们来采用消费者组的形式消费消息,在这期间,我们不断往消费者组中新增消费者,使其发生重平衡,我们来观察下消息的消费情况。

修改消费者端的 main():

func main() {
	// 先启动 c1
	c1 := NewConsumer("consumer-1")
	go func() {
		c1.Read(500)
	}()

	// 5 秒后启动 c2
	time.Sleep(5 * time.Second)
	go func() {
		c2 := NewConsumer("consumer-2")
		c2.Read(300)
	}()

	// 再 10 秒后启动 c3 和 c4
	time.Sleep(10 * time.Second)
	go func() {
		c3 := NewConsumer("consumer-3")
		c3.Read(100)
	}()
	go func() {
		c4 := NewConsumer("consumer-4")
		c4.Read(100)
	}()

	select {}
}

先启动生产者重新生产数据,然后再启动消费者消费数据,观察控制台:

consumer-1 start read
consumer-1 read msg: msg-1, time: 04:44:28
consumer-1 read msg: msg-2, time: 04:44:28
consumer-1 read msg: msg-3, time: 04:44:29		# consumer-1 按顺序消费
consumer-2 start read						  # consumer-2 进来
consumer-1 read msg: msg-4, time: 04:44:30
consumer-1 read msg: msg-5, time: 04:44:30
consumer-1 read msg: msg-6, time: 04:44:31      # 这里相差了 6s,就是在进行重平衡
consumer-2 read msg: msg-7, time: 04:44:37      # 重平衡后发现原来的分区给 consumer-2 消费了
consumer-1 read msg: msg-7, time: 04:44:37	    # 这里发生了重复消费
consumer-2 read msg: msg-8, time: 04:44:37
consumer-2 read msg: msg-9, time: 04:44:37
consumer-2 read msg: msg-10, time: 04:44:38
consumer-2 read msg: msg-11, time: 04:44:38
consumer-2 read msg: msg-12, time: 04:44:38
consumer-2 read msg: msg-13, time: 04:44:39
consumer-2 read msg: msg-14, time: 04:44:39
consumer-2 read msg: msg-15, time: 04:44:39      # consumer-2 按顺序消息
consumer-4 start read						   # consumer-3 和 consumer-4 进来
consumer-3 start read
consumer-2 read msg: msg-16, time: 04:44:40	   
consumer-4 read msg: msg-17, time: 04:44:46      # 这里发生重平衡
consumer-4 read msg: msg-18, time: 04:44:46      # 重平衡后由 consumer-4 负责该分区
consumer-2 read msg: msg-17, time: 04:44:46      # 这里由于 2 的速度比 4 慢很多,所以就乱序了,还重复消费
consumer-4 read msg: msg-19, time: 04:44:46
consumer-4 read msg: msg-20, time: 04:44:46
# ...

总结

当我们采用消费者组的时候,由于重平衡机制的存在,单纯从 Kafka 的角度来说是无法完全实现顺序消息的,只能通过静态成员功能、避免分区数量变化和减少消费者组成员数量变化等方式来尽可能减少重平衡的发生,进而尽可能维持消息的顺序性。

参考

  • 极客时间 - 深入拆解消息队列 47 讲(许文强)
  • 《Kafka 权威指南(第 2 版)》
  • Pulsar 官方文档-分区topic-顺序保证
  • RocketMQ 官方文档-功能特性-顺序消息
  • RabbitMQ 官方文档

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【咕咕送书 | 第六期】深入浅出阐述嵌入式虚拟机原理,实现“小而能”嵌入式虚拟机!

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《粉丝福利》 《linux深造日志》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 ⛳️ 写在前面参与规则引言一、为什么嵌入式系统需要虚拟化技术&#xff1f;1.1 专家推荐 二、本书适合谁&#x…

二级分类菜单及三级分类菜单的层级结构返回

前言 在开发投诉分类功能模块时&#xff0c;遇到过这样一个业务场景&#xff1a;后端需要按层级结构返回二级分类菜单所需数据&#xff0c;换言之&#xff0c;将具有父子关系的List结果集数据转为树状结构数据来返回 二级分类菜单 前期准备 这里简单复刻下真实场景中 出现的…

一文从Vue2过渡到Vue3

文章目录 Vue3简介创建Vue3.0工程使用 vue-cli 创建使用 vite 创建Vue3工程结构变化 常用 Composition API拉开序幕的setupref函数reactive函数Vue3.0中的响应式原理vue2.x的响应式Vue3.0的响应式 reactive对比refsetup的两个注意点计算属性与监视computed函数watch函数watchEf…

【限时免费】20天拿下华为OD笔试之【DP/贪心】2023B-观看文艺汇演-200分【欧弟算法】全网注释最详细分类最全的华为OD真题题解

【DP/贪心】2023B-观看文艺汇演 题目描述与示例 某公园将举行多场文艺表演&#xff0c;很多演出都是同时进行&#xff0c;一个人只能同时观看一场演出&#xff0c;且不能迟到早退&#xff0c;由于演出分布在不同的演出场地&#xff0c;所以连续观看的演出最少有 15 分钟的时间…

Docker搭建个人网盘NextCloud并接入雨云对象存储的教程

雨云服务器使用Docker搭建私有云盘NextCloud并接入雨云对象存储ROS的教程。 NextCloud简介 NextCloud由原ownCloud联合创始人Frank Karlitschek创建的&#xff0c;继承原ownCloud的核心技术又有不少的创新。在功能上NextCloud和ownCloud差不多&#xff0c;甚至还要丰富一些&a…

WebSocket了解

一.什么是WebSocket WebSocket是HTML5下一种新的协议&#xff08;websocket协议本质上是一个基于tcp的协议&#xff09;它实现了浏览器与服务器全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通讯的目的Websocket是一个持久化的协议 二.websocket的原理 web…

【MATLAB源码-第90期】基于matlab的OQPSKsimulink仿真,对比初始信号和解调信号输出星座图。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 正交偏移二进制相移键控&#xff08;OQPSK, Orthogonal Quadrature Phase Shift Keying&#xff09;是一种数字调制技术&#xff0c;主要用于高效无线数据传输。它是传统二进制相移键控&#xff08;BPSK&#xff09;的一个变…

redis实现消息延迟队列

业务场景 在很多软件系统功能中都会出现定时任务的业务场景,比如提前点单,比如定时发布动态,文章等而出现这样的的定时的任务为延迟队任务 代码模块 任务的持久化一般都需要建立一个任务表和任务日志表,避免宕机导致任务失效,先新建立一个数据库,创建基本的任务表和任务日志表…

【刷题笔记】加油站||符合思维方式

加油站 文章目录 加油站1 题目描述2 思路3 解题方法 1 题目描述 https://leetcode.cn/problems/gas-station/ 在一条环路上有 n 个加油站&#xff0c;其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车&#xff0c;从第 i 个加油站开往第 i1 个加油站需要消…

每日一题--相交链表

离思五首-元稹 曾经沧海难为水&#xff0c;除却巫山不是云。 取次花丛懒回顾&#xff0c;半缘修道半缘君。 目录 题目描述&#xff1a; 思路分析&#xff1a; 方法及时间复杂度&#xff1a; 法一 计算链表长度(暴力解法) 法二 栈 法三 哈希集合 法四 map或unordered_map…

YOLOv5算法进阶改进(4)— 引入解耦合头部 | 助力提高检测准确率

前言:Hello大家好,我是小哥谈。解耦头是目标检测中的一种头部设计,用于从检测网络的特征图中提取目标位置和类别信息。具体来说,解耦头部将目标检测任务分解为两个子任务:分类和回归。分类任务用于预测目标的类别,回归任务用于预测目标的位置。这种设计可以提高目标检测的…

粒子群算法Particle Swarm Optimization (PSO)的定义,应用优点和缺点的总结!!

文章目录 前言一、粒子群算法的定义二、粒子群算法的应用三、粒子群算法的优点四、粒子群算法的缺点&#xff1a;粒子群算法的总结 前言 粒子群算法是一种基于群体协作的随机搜索算法&#xff0c;通过模拟鸟群觅食行为而发展起来。该算法最初是由Eberhart博士和Kennedy博士于1…

PyQt6实战开发之旅-代码均可运行

学习感悟 由于官方文档是英文的&#xff0c;所以学习起来不是很直观。网上的中文教程也都有点偏重就轻&#xff0c;去从头学习细枝末节不是很必要。假如每个控件组件讲十分钟&#xff0c;几百个控件可想而知。最关键的是有python基础&#xff0c;能理解类与继承&#xff0c;函…

【漏洞复现】OpenTSDB 2.4.0 命令注入(CVE-2020-35476)漏洞复现

漏洞描述 官方文档这样描述:OpenTSDB is a distributed, scalable Time Series Database (TSDB) written ontop of HBase; 翻译过来就是,基于Hbase的分布式的,可伸缩的时间序列数据库。 主要用途,就是做监控系统;譬如收集大规模集群(包括网络设备、操作系统、应用程序…

官网IDM下载和安装的详细步骤

目录 一、IDM是什么 二、下载安装 三、解决下载超时的问题 四、谷歌浏览器打开IDM插件 谷歌浏览器下载官网&#x1f447; 五、测试 六、资源包获取 一、IDM是什么 IDM&#xff08;internet download manager&#xff09;是一个互联网下载工具插件&#xff0c;常见于用…

C语言数据类型和变量

# C语言数据类型和变量 # 数据类型介绍 C语⾔提供了丰富的数据类型来描述⽣活中的各种数据。使⽤整型类型来描述整数&#xff0c;使⽤字符类型来描述字符&#xff0c;使⽤浮点型类型来描述⼩数。所谓“类型”&#xff0c;就是相似的数据所拥有的共同特征&#xff0c;编译器只有…

可以在Playgrounds或Xcode Command Line Tool开始学习Swift

一、用Playgrounds 1. App Store搜索并安装Swift Playgrounds 2. 打开Playgrounds&#xff0c;点击 文件-新建图书。然后就可以编程了&#xff0c;如下&#xff1a; 二、用Xcode 1. 安装Xcode 2. 打开Xcode&#xff0c;选择Creat New Project 3. 选择macOS 4. 选择Comman…

使用Rust开发小游戏

本文是对 使用 Rust 开发一个微型游戏【已完结】[1]的学习与记录. cargo new flappy 在Cargo.toml的[dependencies]下方增加: bracket-lib "~0.8.7" main.rs中: use bracket_lib::prelude::*;struct State {}impl GameState for State { fn tick(&mut self,…

高校大学校园后勤移动报修系统 微信小程序uniapp+vue

本文主要是针对线下校园后勤移动报修传统管理方式中管理不便与效率低的缺点&#xff0c;将电子商务和计算机技术结合起来&#xff0c;开发出管理便捷&#xff0c;效率高的基于app的大学校园后勤移动报修app。该系统、操作简单、界面友好、易于管理和维护&#xff1b;而且对后勤…

微机11111

一、填空题&#xff08;共15分&#xff0c;每空1分&#xff09; 1、十六进制数30A.5转换为二进制是__________&#xff0c;转换为十进制是_________ 001100001010.0101B 778.3125 十六进制转换二进制 将一位十六进制分解成四位二进制 十六进制转换十进制 3X1620X16110X1605X1…