Kafka 如何实现顺序消息

news2024/11/19 14:49:11

版本说明

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

  • 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 官方文档

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

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

相关文章

【数据结构】用C语言实现链队列(附完整运行代码)

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:数据结构 ⚙️操作环境:Visual Studio 2022 一.了解项目功能 在本次项目中我们的目标是实现一个链队列: 该链队列使用动态内存分配空间,可以用来存储任意数量的同类型数据. 队列结点(QNode)需要包含两个要素:数据域data,…

2017年4月10日 Go生态洞察:开发者体验工作组介绍

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

我好像发现了车载测试面试成功的秘籍

在汽车行业中&#xff0c;车载测试工程师扮演着至关重要的角色。他们负责确保汽车的各种系统和功能在各种条件下都能正常运行&#xff0c;以确保车辆的安全性、可靠性和性能。如果你梦想成为一名车载测试工程师&#xff0c;那么你可能需要准备好回答一些关键的面试问题。在本文…

web:[ZJCTF 2019]NiZhuanSiWei1

题目 点进题目&#xff0c;网页显示如下&#xff0c;需要代码审计 $_GET["text"]和$_GET["file"]来获取传入的两个参数text和file。使用isset()函数来检查$text变量是否已设置并且不为null。如果设置了并且不为null&#xff0c;则执行下面的逻辑。在下面的…

C++实现十大排序算法

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…

Proteus仿真--基于DS1302与数码管设计的可调电子钟

本文主要介绍基于51单片机的DS1302的可调式电子钟实验&#xff08;完整仿真源文件及代码见文末链接&#xff09; 仿真图如下 其中数码管显示电子钟时间信息&#xff0c;按键用于调节时间&#xff0c;时间芯片选用DS1302芯片 仿真运行视频 Proteus仿真--基于DS1302与数码管设…

Debian 11.3 ARM64 安装中文语言包

文章目录 Debian 介绍1、执行命令2、语言选择3、修改设置 Debian 介绍 Debian是一种自由开源的操作系统&#xff0c;被广泛用于服务器、个人计算机和嵌入式设备。它是由全球志愿者组成的开发团队开发和维护的&#xff0c;以稳定性、安全性和自由性而闻名。 以下是一些关于Deb…

机器学习-线性模型·

线性模型是一类用于建模输入特征与输出之间线性关系的统计模型。这类模型的基本形式可以表示为&#xff1a; 其中&#xff1a; 是模型的输出&#xff08;目标变量&#xff09;。 是截距&#xff08;常数项&#xff0c;表示在所有输入特征都为零时的输出值&#xff09;。 是权重…

Yakit工具篇:WebFuzzer模块之热加载技术

简介 官方定义&#xff1a; 什么是热加载&#xff1f; 广义上来说&#xff0c;热加载是一种允许在不停止或重启应用程序的情况下&#xff0c;动态加载或更新特定组件或模块的功能。这种技术常用于开发过程中&#xff0c;提高开发效率和用户体验。 在Yakit 的Web Fuzzer中&…

PTA NeuDs_数据库题目

二.单选题 1.数据库应用程序的编写是基于数据库三级模式中的。 A.模式 B.外模式 C.内模式 D.逻辑模式 用户应用程序根据外模式进行数据操作&#xff0c;通过外模式一模式映射&#xff0c;定义和建立某个外模式与模式间的对应关系 2.对创建数据库模式一类的数据库对象的授权…

Python基础语法之判断语句

1.布尔类型和比较运算符 布尔类型&#xff1a;数字类型的一种。 比较运算符&#xff1a; > < > < ! 2.if语句基本格式 if 要判断的条件&#xff1a; 条件成立&#xff0c;即做~ 例子&#xff1a; 注意&#xff1a;格式上冒号和缩进 3.if else组合…

docker devicemapper: Error running DeleteDevice dm_task_run failed

docker 删除容器&#xff0c;遇到&#xff1a; devicemapper: Error running DeleteDevice dm_task_run failed 异常 [hadoophadoop02 ~]$ sudo docker rm 5ede1280f0bf Error response from daemon: container 5ede1280f0bf791e91d40038b15decd42e8923546ae578abd96e08114c76…

Linux 基础-常用的命令和搭建 Java 部署环境

文章目录 目录相关查看目录中的内容查看目录当前的完整路径切换目录 文件相关创建文件查看文件内容写文件vim 基础 创建删除创建目录 移动和复制移动(剪切粘贴)复制(复制粘贴) 搭建 Java 部署环境1. 安装 jdk2. 安装 tomcat1). 我们在自己电脑上下好 tomcat2). 从官网下载的 .z…

2023年【安全员-C证】考试试卷及安全员-C证试题及解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 安全员-C证考试试卷是安全生产模拟考试一点通生成的&#xff0c;安全员-C证证模拟考试题库是根据安全员-C证最新版教材汇编出安全员-C证仿真模拟考试。2023年【安全员-C证】考试试卷及安全员-C证试题及解析 1、【多选…

【Java SE】 带你走近Java的抽象类与接口

&#x1f339;&#x1f339;&#x1f339;【JavaSE】专栏&#x1f339;&#x1f339;&#x1f339; &#x1f339;&#x1f339;&#x1f339;个人主页&#x1f339;&#x1f339;&#x1f339; &#x1f339;&#x1f339;&#x1f339;上一篇文章&#x1f339;&#x1f339;&…

【小沐学写作】原型设计工具汇总(Axure RP)

文章目录 1、简介2、Axure RP2.1 工具简介2.2 工具特点2.2.1 互动事件2.2.2 条件逻辑2.2.4 工作表格2.2.5 多状态容器2.2.6 数据驱动接口2.2.7 自适应视图2.2.8 流程图 2.3 工具安装2.3.1 安装2.3.2 运行 2.4 使用费用2.5 工具体验2.5.1 登陆框制作 3、其他3.1 Figma3.2 Adobe …

如何避免死锁

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一波电子书籍资料&#xff0c;包含《Effective Java中文版 第2版》《深入JAVA虚拟机》&#xff0c;《重构改善既有代码设计》&#xff0c;《MySQL高性能-第3版》&…

【设计模式-2.1】创建型——单例模式

说明&#xff1a;设计模式根据用途分为创建型、结构性和行为型。创建型模式主要用于描述如何创建对象&#xff0c;本文介绍创建型中的单例模式。 饿汉式单例 单例模式是比较常见的一种设计模式&#xff0c;旨在确保对象的唯一性&#xff0c;什么时候去使用这个对象都是同一个…

Vue19 列表过滤

直接上代码 以下代码使用了两种实现方式&#xff0c;监视属性和计算属性 当能用计算属性实现时&#xff0c;推荐使用计算属性 <!DOCTYPE html> <html><head><meta charset"UTF-8" /><title>列表过滤</title><script type&q…

xadmin后台在每一行记录增加一个复制链接按钮

xadmin后台在每一行记录增加一个复制链接按钮 1、效果 点击复制后,自动把url链接复制到粘贴板,按Ctrl+v即可显示复制内容。 2、实现代码 adminx.py # 用户管理 class UserWhiteListAdmin(object):search_fields = [name, mobile] # 检索字段list_display