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		# 消费者
└─producerproducer.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 mainimport ("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 mainimport ("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() {// 先启动 c1c1 := NewConsumer("consumer-1")go func() {c1.Read(500)}()// 5 秒后启动 c2time.Sleep(5 * time.Second)go func() {c2 := NewConsumer("consumer-2")c2.Read(300)}()// 再 10 秒后启动 c3 和 c4time.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 官方文档

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

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

相关文章

uniapp挽留提示2.0

项目需求&#xff1a;有时候挽留的ui是全屏的&#xff0c;用page-container也可以。后来产品提了个问题&#xff0c;手机侧滑的时候没那么顺畅&#xff08;就是一用侧滑&#xff0c;就显示出来&#xff0c;产品要的方案是如下图&#xff0c;emmm大概是这个意思&#xff09; 后面…

Python入门04字符串

目录 1 字符串的定义2 转义字符3 字符串的常见方法4 分割字符串5 字符串反转6 字符串的链式调用7 格式化字符串8 多行字符串总结 1 字符串的定义 在Python中&#xff0c;字符串表示一个字符的序列&#xff0c;比如 str "hello,world"这里我们定义了一个字符串&…

java选择排序和冒泡排序

1.区别 选择排序和冒泡排序的区别主要在于算法逻辑、稳定性和交换成本。 算法逻辑&#xff1a;选择排序和冒泡排序都属于比较排序&#xff0c;但在具体算法逻辑上有所不同。冒泡排序是通过相邻元素之间的比较和交换&#xff0c;将较大&#xff08;或较小&#xff09;的元素逐…

一文从Vue2过渡到Vue3

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

Pytorch中的gather的理解和用法

Pytorch中的gather的理解和用法 这个Gather的用法花费了点时间&#xff0c;我相信很多人一开始不太懂。 跟着我简单理解。 首先样例是&#xff1a; tensor([[ 3, 4, 5],[ 6, 7, 8],[ 9, 10, 11]])然后index: [[2, 1, 0]]然后执行的代码&#xff1a; tensor_0.gather(0…

享受更健康的睡眠,dido P1S智能手环体验

如今很多人都有严重的失眠困扰&#xff0c;像是因为平时工作非常繁忙&#xff0c;每天回到家后已经很晚了&#xff0c;或者是睡前长时间刷手机&#xff0c;躺在床上辗转反侧&#xff0c;晚上就很难入睡&#xff0c;脑海中一片混乱&#xff0c;休息的质量就很差。我最近尝试了一…

人工智能关键技术决定机器人产业的前途

人工智能&#xff08;Artificial Intelligence&#xff0c;AI&#xff09;是指让计算机或机器具有类似于人类的智能和学习能力的技术。人工智能技术与机器人技术的结合将改变传统的机器人行业格局&#xff0c;就像智能手机对传统手机的颠覆一样。本文从人工智能技术的发展趋势、…

ASIWebPage数据库抓取代码示例

objective-c #import "ASIWebPageRequest.h" int main(int argc, const char * argv[]) { autoreleasepool { // 创建并设置 NSURL *代理URL [NSURL URLWithString:""]; ASIHTTPRequest *request [ASIHTTPRequest requestW…

jsoup登录日志平台后调企业微信机器人自动发送错误日志告警

一、需求&#xff1a;错误日志Top10告警发送 二、需求分解 jsoup实现登录&#xff0c;获取到cookie和token等用户鉴权信息获取接口相应的key值调用日志平台错误日志Top榜接口&#xff0c;查询到结果集调用企业微信机器人发送消息接口加上定时任务&#xff0c;可以实现定时发送…

java多线程-扩展知识二:线程的生命周期

1、生命周期 生命周期有广义与狭义之分&#xff0c;狭义为生命科学术语&#xff0c;指包括人类在内的一切动物由出生到死亡经历的生命全程。广义的生命周期泛指自然界与人类社会各种客观事物的阶段性变化及规律&#xff0c;如家庭生命周期、产品生命周期等。本义即狭义的生命周…

任务管理器怎么打开?4个方法快速打开!

“我想进入电脑任务管理器中对某些应用进行设置&#xff0c;但是我不知道应该怎么进入任务管理器中。有什么方法可以快速进入任务管理器吗&#xff1f;” 任务管理器是Windows操作系统中一个强大的工具&#xff0c;可以让你监控和管理计算机上运行的进程、应用程序和性能。当我…

FLASK博客系列5——模板之从天而降

我们啰啰嗦嗦讲了4篇&#xff0c;都是在调接口&#xff0c;啥时候能看到漂亮的页面呢&#xff1f;别急&#xff0c;今天我们就来实现。 来我们先来实现一个简单的页面。不多说&#xff0c;上代码。 app.route(/) def index():user {username: clannadhh}return <html>&…

解决方案:新版WPS-右键粘贴值到可见单元格没有了

旧版WPS&#xff0c;右键就能出现 但是新版WPS不是在这里&#xff08;方法1&#xff09; 新版WPS&#xff08;方法2&#xff09; 视频详细教程链接&#xff1a;解决方案&#xff1a;新版WPS-右键粘贴值到可见单元格没有了 -- 筛选后复制公式粘贴为数值_哔哩哔哩_bilibili

智能优化算法应用:基于飞蛾扑火算法无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于飞蛾扑火算法无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于飞蛾扑火算法无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.飞蛾扑火算法4.实验参数设定5.算法结果6.参考…

数智融合 开启金融数据治理新时代

11月24日&#xff0c;由上海罗盘信息科技有限公司&#xff08;罗盘科技&#xff09;主办&#xff0c;北京酷克数据科技有限公司&#xff08;酷克数据&#xff09;支持协办的“博学近思 切问治理”数据治理分享会在上海成功举行。 本次会议深度聚焦金融行业数智化转型&#xff…

FPGA模块——DA转换模块(AD9708类)

FPGA模块——DA转换模块&#xff08;AD9708类&#xff09; AD9708/3PD9708代码 AD9708/3PD9708 由于电路接了反相器&#xff0c;所以对应就不一样了。 电路图&#xff1a; 代码 在ROM中存入要输出的波形数据&#xff1a; 用软件生成各个对应的点。 给DA转换器一个时钟&…

网络篇---第四篇

系列文章目录 文章目录 系列文章目录前言一、TCP 如何保证可靠性二、OSI 的七层模型都有哪些?三、浏览器中输入:“www.woaijava.com”之后都发生了什么?请详细阐述前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站…

关于Unity中字典在Inspector的显示

字典在Inspector的显示 方法一&#xff1a;实现ISerializationCallbackReceiver接口 《unity3D游戏开发第二版》记录 在编辑面板中可以利用序列化监听接口特性对字典进行序列化。 主要继承ISerializationCallbackReceiver接口 实现OnAfterDeserialize() OnBeforeSerialize() …

C# WPF上位机开发(第一个应用)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 万事开头难&#xff0c;很多事情都是难在第一步。走出了这第一步&#xff0c;回过头看以前走的每一步&#xff0c;发现其实也不难。用c# wpf编写界…

VSD Viewer for Mac(Visio绘图文件阅读器)

VSD Viewer for Mac版是mac上一款非常强大的Visio绘图文件阅读器&#xff0c;它为打开和打印Visio文件提供了简单的解决方案。可以显示隐藏的图层&#xff0c;查看对象的形状数据&#xff0c;预览超链接。还可以将Visio转换为包含图层&#xff0c;形状数据和超链接的PDF文档。 …