MQ 笔记

什么是消息队列?

消息队列(Message Queue, MQ)是一种用于在分布式系统中传递消息的中间件技术。

它允许应用程序通过发送和接收消息进行异步通信。

消息队列的核心思想是解耦生产者和消费者,生产者将消息发送到队列中,消费者从队列中获取消息并进行处理。

  • 生产者(Producer):负责生成消息并发送到队列。
  • 消费者(Consumer):负责从队列中获取消息并进行处理。
  • 队列(Queue):存储消息的缓冲区,确保消息在传递过程中不会丢失。

消息队列可以是内存中的数据结构,也可以是独立的中间件服务(如 Kafka、RabbitMQ、RocketMQ 等)。


消息队列的使用场景?

消息队列在分布式系统高并发场景 中扮演着重要角色,其主要作用包括:

  • 异步通信

    • 生产者和消费者不需要同时在线,生产者发送消息后可以立即返回,消费者可以在稍后处理消息。
    • 例如:用户注册后,发送欢迎邮件的操作可以通过消息队列异步处理。
  • 解耦系统

    • 生产者和消费者之间没有直接依赖,通过消息队列进行通信。
    • 例如:订单系统和库存系统通过消息队列解耦,订单系统生成订单后,通过消息队列通知库存系统扣减库存。
  • 流量削峰

    • 在流量突增时,消息队列可以缓冲请求,避免系统过载。
    • 例如:电商大促期间,订单系统将订单消息放入队列,由后端服务逐步处理。
  • 可靠性

    • 消息队列可以确保消息不丢失,即使消费者暂时不可用,消息也会存储在队列中,直到被成功处理。
    • 例如:支付系统通过消息队列处理支付请求,即使支付服务暂时不可用,支付请求也不会丢失。
  • 顺序性

    • 消息队列可以保证消息的顺序性,确保消息按照发送的顺序被处理。
    • 例如:日志系统通过消息队列保证日志的顺序性。
  • 分布式事务

    • 通过消息队列实现最终一致性,解决分布式系统中的事务问题。
    • 例如:订单系统生成订单后,通过消息队列通知支付系统处理支付。
  • 扩展性

    • 通过消息队列,可以轻松扩展系统的处理能力,增加更多的消费者来处理消息。
    • 例如:图片处理服务通过消息队列分发任务,增加更多的工作节点来提高处理能力。
  • 广播消息

    • 消息队列支持发布/订阅模式,可以将消息广播给多个消费者。
    • 例如:配置中心将配置变更消息广播给所有服务。

消息队列的核心概念

  • 生产者(Producer):消息的发送方。
  • 消费者(Consumer):消息的接收方。
  • 消息(Message):传递的基本单位,包含消息体和元数据。
  • 队列(Queue):消息的存储容器,具有 FIFO 特性。
  • 主题(Topic):消息的分类标识,用于发布/订阅模式。
  • 订阅(Subscription):消费者与主题之间的绑定关系。

消息队列的通信模式

消息队列的通信模式主要分为两种:点对点(Point-to-Point)模式发布/订阅(Publish/Subscribe)模式

1. 点对点(Point-to-Point)模式

定义

  • 点对点模式是一种一对一的通信模式,生产者将消息发送到队列中,消费者从队列中获取消息并进行处理。
  • 每条消息只能被一个消费者处理,处理完成后消息从队列中移除。

特点

  • 一对一通信:每条消息只有一个消费者。
  • 消息持久化:消息存储在队列中,直到被消费者处理。
  • 顺序性:消息按照发送顺序被处理(FIFO)。
  • 可靠性:消息被消费者确认(ACK)后才会从队列中移除,确保消息不丢失。

适用场景

  • 任务分发:将任务分配给多个工作节点处理。
  • 异步处理:生产者不需要等待消费者处理完成。
  • 分布式事务:通过消息队列实现最终一致性。

示例

  • 订单系统将订单消息发送到队列,库存系统从队列中获取消息并扣减库存。

2. 发布/订阅(Publish/Subscribe)模式

定义

  • 发布/订阅模式是一种一对多的通信模式,生产者将消息发布到主题(Topic),所有订阅该主题的消费者都会收到消息。
  • 每条消息可以被多个消费者处理。

特点

  • 一对多通信:每条消息可以被多个消费者处理。
  • 主题和订阅:消息通过主题进行分类,消费者通过订阅主题接收消息。
  • 灵活性:可以动态添加或移除消费者,不影响生产者。
  • 广播机制:消息被广播给所有订阅者。

适用场景

  • 事件通知:将事件通知给多个订阅者。
  • 日志收集:将日志消息广播给多个日志处理服务。
  • 配置更新:将配置变更消息广播给所有服务。

示例

  • 配置中心将配置变更消息发布到配置主题,所有订阅该主题的服务都会收到配置变更通知。

3. 两种模式的对比

特性 点对点(Point-to-Point)模式 发布/订阅(Publish/Subscribe)模式
通信方式 一对一 一对多
消息消费 每条消息只能被一个消费者处理 每条消息可以被多个消费者处理
消息存储 消息存储在队列中 消息存储在主题中
顺序性 消息按照发送顺序被处理(FIFO) 消息可能被多个消费者并行处理
适用场景 任务分发、异步处理、分布式事务 事件通知、日志收集、配置更新

消息队列如何保证消息不丢失?

1. 生产者端保证消息不丢失

1.1 消息确认机制(ACK)

  • 生产者发送消息后,消息队列会返回一个确认(ACK)信号,表示消息已成功接收。
  • 如果生产者未收到 ACK,可以重试发送消息。
  • 示例:RabbitMQ 的 Publisher Confirms 机制。

1.2 持久化消息

  • 生产者可以将消息标记为持久化,确保消息在队列中存储到磁盘,即使消息队列服务重启也不会丢失。
  • 示例:Kafka 和 RabbitMQ 都支持消息持久化。

1.3 事务机制

  • 生产者可以使用事务机制,确保消息发送和业务逻辑的原子性。
  • 示例:RabbitMQ 的事务机制。

1.4 重试机制

  • 生产者在发送失败时,可以通过重试机制重新发送消息。
  • 示例:Kafka 的 Producer 重试机制。

2. 消息队列端保证消息不丢失

2.1 消息持久化

  • 消息队列将消息持久化到磁盘,确保即使服务重启,消息也不会丢失。
  • 示例:Kafka 将消息存储到日志文件(Log Segment),RabbitMQ 将消息存储到磁盘。

2.2 副本机制

  • 消息队列通过多副本机制(Replication)保证消息的高可用性。
  • 即使某个节点故障,其他副本节点仍可以提供服务。
  • 示例:Kafka 的多副本机制。

2.3 高可用性

  • 消息队列通过集群部署,确保在单点故障时仍能正常服务。
  • 示例:RabbitMQ 的镜像队列,Kafka 的集群部署。

2.4 消息确认机制

  • 消息队列在消费者成功处理消息后,会返回一个确认(ACK)信号,确保消息被成功消费。
  • 如果消费者未发送 ACK,消息队列会重新投递消息。
  • 示例:RabbitMQ 的 Consumer ACK 机制。

3. 消费者端保证消息不丢失

3.1 手动确认机制

  • 消费者在处理完消息后,手动发送 ACK 确认消息已处理。
  • 如果消费者未发送 ACK,消息队列会重新投递消息。
  • 示例:RabbitMQ 的 Manual ACK 机制。

3.2 幂等性设计

  • 消费者需要设计幂等性逻辑,确保即使消息被重复消费,也不会对业务造成影响。
  • 示例:通过唯一 ID 判断消息是否已处理。

3.3 重试机制

  • 消费者在处理失败时,可以通过重试机制重新处理消息。
  • 示例:Kafka 的 Consumer 重试机制。

3.4 死信队列(DLQ)

  • 如果消息多次处理失败,可以将其转移到死信队列,避免消息丢失。
  • 示例:RabbitMQ 的死信队列机制。

4. Kafka 和 RabbitMQ 具体实现

Kafka 的保证机制

  • 生产者端:通过 ACKS 参数控制消息确认级别(如 acks=all 确保所有副本确认)。
  • 消息队列端:通过多副本和 ISR(In-Sync Replicas)机制保证消息不丢失。
  • 消费者端:通过 Offset 提交机制和幂等性设计保证消息不丢失。

RabbitMQ 的保证机制

  • 生产者端:通过 Publisher Confirms 和持久化消息保证消息不丢失。
  • 消息队列端:通过持久化队列和镜像队列保证消息不丢失。
  • 消费者端:通过 Manual ACK 和死信队列保证消息不丢失。

如何处理消息重复消费的问题?

重复消费可能导致数据不一致、业务逻辑错误等问题。为了解决这个问题,可以从 消息队列本身业务逻辑设计 两个方面入手,采取多种措施来避免或处理重复消费。

1. 消息队列本身的机制

1.1 消息确认机制(ACK)

  • 问题:如果消费者未正确发送 ACK,消息队列可能会重新投递消息,导致重复消费。
  • 解决方案
    • 使用手动确认机制,确保消费者在处理完消息后发送 ACK。
    • 在 RabbitMQ 中,使用 basic.ack 手动确认消息。
    • 在 Kafka 中,手动提交 Offset,确保消息已处理。

1.2 消息幂等性设计

消息队列支持幂等性投递,确保同一条消息不会被重复投递。

例如,Kafka 通过 enable.idempotence=true 开启生产者幂等性。

1.3 消息去重

消息队列支持消息去重,避免重复存储。

例如,RocketMQ 支持消息去重机制。


2. 业务逻辑设计

2.1 幂等性设计

在消费者端设计幂等性逻辑,确保即使消息被重复消费,也不会对业务造成影响。

比如,为每条消息分配 唯一 ID,消费者在处理消息前检查该 ID 是否已处理。

2.2 消息去重表

使用消息去重表,记录已处理的消息 ID。
在处理消息前,检查消息 ID 是否已存在于去重表中。

示例

CREATE TABLE message_dedup (
    message_id VARCHAR(64) PRIMARY KEY,
    processed_at TIMESTAMP
);

2.3 分布式锁

使用分布式锁(如 Redis 或 ZooKeeper)确保同一消息不会被多个消费者同时处理。

示例

 lockKey := "message_lock_" + messageID
 success, err := redis.SetNX(lockKey, 1, time.Minute).Result()
 if success {
   
     // 处理消息
 }

SetNX"Set if Not Exists" 的缩写,表示

  • 当键 lockKey 不存在时,才会设置它的值为 1,并返回 true;
  • 如果键已经存在,则不会设置值,并返回 false。

time.Minute 是锁的过期时间,表示这个键值对会在 1 分钟后自动过期(删除)。

2.4 消息状态标记

在数据库中为消息添加状态字段(如 status),标记消息是否已处理。

在处理消息前,检查消息状态,避免重复处理。

示例

UPDATE messages SET status = 'processed' WHERE id = ? AND status = 'pending';

消息队列如何保证消息的顺序性?

在分布式系统中,消息顺序性 是一个重要的需求,尤其是在某些业务场景中(如订单处理、日志记录等),消息的处理顺序必须与发送顺序一致。

1. 消息队列本身的顺序性保证

1.1 单分区/单队列顺序

  • 实现方式:将消息发送到同一个分区(Partition)或队列(Queue),确保消息按照发送顺序被处理。
  • 示例
    • Kafka:将消息发送到同一个分区。
    • RabbitMQ:将消息发送到同一个队列。
  • 适用场景:适用于消息量较小的场景。

1.2 全局顺序

  • 实现方式:在整个消息队列中保证消息的全局顺序。
  • 示例
    • RocketMQ:通过全局顺序消息(Global Ordered Message)实现。
  • 适用场景:适用于严格要求全局顺序的场景。

1.3 分区/队列顺序

  • 实现方式:将消息按某种规则(如业务键)分配到不同的分区或队列,确保每个分区或队列内的消息顺序性。
  • 示例
    • Kafka:通过消息的 Key 进行分区,确保同一 Key 的消息发送到同一分区。
    • RabbitMQ:通过路由键(Routing Key)将消息发送到不同的队列。
  • 适用场景:适用于消息量较大且需要局部顺序的场景。

2. 生产者端的顺序性保证

2.1 同步发送

  • 实现方式:生产者按顺序发送消息,并等待消息队列返回确认(ACK)后再发送下一条消息。
  • 示例
    • Kafka:通过同步发送(acks=all)确保消息顺序。
  • 适用场景:适用于对顺序性要求较高的场景。

2.2 消息编号

  • 实现方式:为每条消息添加序号(Sequence Number),消费者根据序号处理消息。
  • 示例
    • RocketMQ:通过消息序号保证顺序性。
  • 适用场景:适用于需要严格顺序的场景。

3. 消费者端的顺序性保证

3.1 单线程消费

  • 实现方式:消费者使用单线程处理消息,确保消息按顺序处理。
  • 示例
    • Kafka:使用单线程消费同一个分区。
  • 适用场景:适用于消息量较小的场景。

3.2 消息缓冲

  • 实现方式:消费者将消息缓存到本地队列,按顺序处理。
  • 示例
    • RabbitMQ:使用本地队列缓存消息。
  • 适用场景:适用于需要批量处理的场景。

3.3 状态机

  • 实现方式:通过状态机控制消息的处理顺序,确保业务逻辑的顺序性。
  • 示例
    • 订单处理:根据订单状态(如创建、支付、发货)顺序处理消息。
  • 适用场景:适用于复杂业务逻辑的场景。

4. Kafka、RabbitMQ、RocketMQ 具体实现

4.1 Kafka 的顺序性保证

  • 分区顺序:将消息发送到同一个分区,确保分区内的消息顺序性。
  • 生产者同步发送:使用同步发送(acks=all)确保消息顺序。
  • 消费者单线程消费:使用单线程消费同一个分区。

4.2 RabbitMQ 的顺序性保证

  • 单队列顺序:将消息发送到同一个队列,确保队列内的消息顺序性。
  • 消费者单线程消费:使用单线程消费同一个队列。

4.3 RocketMQ 的顺序性保证

  • 全局顺序消息:通过全局顺序消息实现全局顺序性。
  • 分区顺序消息:通过分区顺序消息实现局部顺序性。

5. 其他技术

5.1 分布式锁

  • 实现方式:使用分布式锁(如 Redis 或 ZooKeeper)确保同一资源的消息按顺序处理。
  • 示例
    • 订单处理:对同一订单 ID 加锁,确保订单消息按顺序处理。
  • 适用场景:适用于资源竞争的场景。

5.2 消息编号和状态

  • 实现方式:为每条消息添加序号和状态,消费者根据序号和状态处理消息。
  • 示例
    • 日志处理:根据日志序号和状态顺序处理日志消息。
  • 适用场景:适用于需要严格顺序的场景。

在分布式场景下,如何保证消息的顺序性?

  1. 单分区/单队列顺序性
    • 原理:将需要保证顺序的消息发送到同一个分区(如 Kafka 的 Partition)或同一个队列中,确保这些消息由同一个消费者按顺序处理。
    • 实现
      • 在 Kafka 中,可以通过指定相同的消息键(Key)将消息路由到同一个 Partition(分区)。
      • 在 RabbitMQ 中,可以将消息发送到同一个队列,并由单个消费者处理。
    • 优点:简单易实现。
    • 缺点:限制了系统的扩展性,无法充分利用分布式系统的并行处理能力。

  1. 消息键(Message Key)路由
    • 原理:使用消息键将相关消息路由到同一个分区或队列中,确保这些消息按顺序处理。
    • 实现
      • 在 Kafka 中,可以为同一组相关的消息指定相同的消息键(如用户 ID、订单 ID 等),确保它们被路由到同一个 Partition(分区)。
      • 在 RocketMQ 中,可以使用消息的 MessageQueue 来实现类似的功能。
    • 优点:在保证顺序性的同时,可以支持一定程度的并行处理。
    • 缺点:如果消息键分布不均匀,可能导致某些分区或队列负载过高。

  1. 消费者顺序处理
    • 原理:在消费者端保证消息的顺序处理,即使消息可能来自多个分区或队列。
    • 实现
      • 使用单线程处理消息,避免并行消费。
      • 使用本地队列或缓存,将消息按顺序排列后再处理。
    • 优点:实现简单。
    • 缺点:处理效率较低,无法充分利用多核 CPU 和分布式系统的优势。

  1. 分布式锁或顺序标记
    • 原理:使用分布式锁或顺序标记来确保消息的全局顺序性。
    • 实现
      • 使用分布式锁(如 Redis 或 Zookeeper)确保同一组相关消息按顺序处理。
      • 为消息添加顺序标记(如时间戳或序列号),消费者根据标记顺序处理消息。
    • 优点:可以支持全局顺序性。
    • 缺点:引入分布式锁会增加系统复杂性和性能开销。

消息队列如何实现消息的持久化?

RabbitMQ

  • 持久化队列:在声明队列时设置 durable=true,这样队列的元数据会被持久化到磁盘。
    channel.queue_declare(queue='my_queue', durable=True)
    
  • 持久化消息:在发送消息时设置 delivery_mode=2,表示消息会被持久化到磁盘。
    channel.basic_publish(
        exchange='',
        routing_key='my_queue',
        body='Hello World!',
        properties=pika.BasicProperties(delivery_mode=2)
    )
    

Kafka

  • 日志持久化:Kafka 将所有消息以日志文件的形式持久化到磁盘,并支持多副本机制(Replication)来保证高可用性。
  • 消息保留策略:可以配置消息的保留时间(retention.ms)或大小(retention.bytes),确保消息在指定时间内不会被删除。

RocketMQ

  • CommitLog:RocketMQ 将所有消息写入一个统一的 CommitLog 文件,并异步刷盘到磁盘。
  • 消息索引:通过索引文件(ConsumeQueue)快速定位消息,同时支持多副本机制。

如何实现消息的延迟发送?

许多消息队列系统(如 RabbitMQ、RocketMQ、Kafka)提供了内置的延迟消息功能,可以直接使用。

RabbitMQ

RabbitMQ 通过 延迟消息插件rabbitmq-delayed-message-exchange)支持延迟消息。

  1. 安装插件:
    rabbitmq-plugins enable rabbitmq_delayed_message_exchange
    
  2. 声明延迟交换机:
    args = {
         'x-delayed-type': 'direct'}
    channel.exchange_declare(exchange='delayed_exchange', exchange_type='x-delayed-message', arguments=args)
    
  3. 发送延迟消息:
    headers = {
         'x-delay': 5000}  # 延迟 5 秒
    channel.basic_publish(
        exchange='delayed_exchange',
        routing_key='my_queue',
        body='Hello World!',
        properties=pika.BasicProperties(headers=headers)
    )
    

RocketMQ

RocketMQ 支持延迟消息,提供多个固定的延迟级别(如 1s、5s、10s 等)。

  1. 发送延迟消息:
    Message message = new Message("my_topic", "Hello World!".getBytes());
    message.setDelayTimeLevel(3);  // 延迟 10 秒
    producer.send(message);
    

Kafka

Kafka 本身不支持延迟消息,但可以通过自定义实现(如使用时间戳和消费者轮询)来实现。


基于数据库的延迟消息

如果消息队列不支持延迟消息,可以使用数据库来实现。

实现步骤

  1. 创建消息表,包含消息内容、状态、发送时间等字段。
    CREATE TABLE delayed_messages (
        id INT AUTO_INCREMENT PRIMARY KEY,
        content TEXT,
        status ENUM('pending', 'sent') DEFAULT 'pending',
        send_time DATETIME
    );
    
  2. 插入延迟消息:
    INSERT INTO delayed_messages (content, send_time)
    VALUES ('Hello World!', NOW() + INTERVAL 5 MINUTE);
    
  3. 定时任务扫描:
    使用定时任务(如 Cron Job)定期扫描表,将到期的消息发送到消息队列。
    SELECT * FROM delayed_messages
    WHERE status = 'pending' AND send_time <= NOW();
    
  4. 更新消息状态:
    发送成功后,更新消息状态为 sent

基于定时任务的延迟消息

通过 linux 系统定时任务(如 Cron )实现延迟消息。

实现步骤

  1. 将延迟消息存储到数据库或缓存中。
  2. 使用定时任务定期扫描未发送的消息。
  3. 将到期的消息发送到消息队列。

具体实现示例(RabbitMQ)

以下是基于 RabbitMQ 和延迟插件的完整示例:

安装插件

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

Python 代码

import pika

# 连接 RabbitMQ
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

# 声明延迟交换机
args = {
   'x-delayed-type': 'direct'}
channel.exchange_declare(exchange='delayed_exchange', exchange_type='x-delayed-message', arguments=args)

# 声明队列
channel.queue_declare(queue='my_queue', durable=True)
channel.queue_bind(exchange='delayed_exchange', queue='my_queue')

# 发送延迟消息
headers = {
   'x-delay': 5000}  # 延迟 5 秒
channel.basic_publish(
    exchange='delayed_exchange',
    routing_key='my_queue',
    body='Hello World!',
    properties=pika.BasicProperties(headers=headers)
)

print("Sent delayed message")
connection.close()

Kafka、RabbitMQ、RocketMQ 的区别?

特性/消息队列 Kafka RabbitMQ RocketMQ
设计目标 高吞吐量、分布式日志系统 通用的消息队列,支持多种消息模式 高吞吐量、低延迟、分布式消息队列
消息模型 发布/订阅模型 支持多种模型(点对点、发布/订阅) 发布/订阅模型
消息存储 持久化到磁盘,支持长时间存储 内存或磁盘,取决于配置 持久化到磁盘,支持长时间存储
吞吐量 非常高(适合大数据场景) 中等(适合中小规模场景) 高(适合大规模场景)
延迟 较高(适合批处理场景) 低(适合实时场景) 低(适合实时场景)
消息顺序 保证分区内消息顺序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值