【学习笔记】中间件-RabbitMQ

1、什么是消息队列(MQ)

MQ 的全称是 Message Queue(消息队列) ,它是一种在应用程序之间进行异步通信的机制,也是现代分布式系统中一个重要的中间件组件。

你可以把它想象成一个智能的"快递柜"或"邮局"

  • 生产者 (Producer) :就像寄快递的人,把包裹(消息 )放进快递柜(队列)。
  • 消费者 (Consumer) :就像取快递的人,在方便的时候从快递柜取出包裹进行处理。

通过这种方式,发送方和接收方不需要同时在线,也不需要知道对方的存在,实现了通信的"解耦"。

2、MQ有哪些协议及对应产品?

(1) AMQP (Advanced Message Queuing Protocol)

  • 特点:这是一种企业级的通用协议,功能非常强大。它支持复杂的路由规则(通过交换机 Exchange)、消息持久化、事务处理和可靠投递。它的设计目标是保证消息在不同厂商的系统间能可靠传输。

  • 对应产品

    • RabbitMQ:最典型的代表,它是AMQP协议的完整实现,以其灵活的路由功能著称。
    • ActiveMQ:老牌的消息中间件,对AMQP也有很好的支持。
    • Qpid:Apache下的一个项目,也是AMQP的早期实现。

(2) MQTT (Message Queuing Telemetry Transport)

  • 特点 :专为物联网 (IoT) 设计。它非常轻量级,占用带宽极低,基于"发布/订阅"模式。它非常适合网络不稳定或设备资源受限(如电池供电、低算力)的场景。

  • 对应产品

    • EMQX (原EMQ):高性能的开源MQTT Broker,广泛用于物联网。
    • Mosquitto:Eclipse旗下的轻量级开源MQTT Broker。
    • HiveMQ:基于Java的企业级MQTT平台。
    • 腾讯云 TDMQ MQTT版:云厂商提供的托管服务,常用于车联网、智能家居。

(3) JMS (Java Message Service)

  • 特点 :严格来说,JMS 是 Java 平台定义的一套 API 规范(接口),而不是具体的网络传输协议。但它定义了消息中间件在 Java 世界里的标准用法(如点对点、发布订阅)。

  • 对应产品

    • ActiveMQ:JMS 规范的经典实现。
    • IBM MQ:金融领域常用的商业软件,完美支持 JMS。
    • RocketMQ:虽然有自己的私有协议,但也提供了 JMS 的客户端支持,方便 Java 应用接入。

(4) STOMP (Simple Text Oriented Messaging Protocol)

  • 特点:一种基于文本的简单协议,类似 HTTP。它的设计初衷是让任何支持 TCP 的语言都能轻松与消息中间件交互,不需要复杂的二进制解析。

  • 对应产品

    • RabbitMQActiveMQ 都内置了对 STOMP 的支持,常用于简单的脚本语言或前端 WebSocket 直接连接 MQ 的场景。

(5) 📊 自定义/专用协议

  • 特点:为了追求极致的性能(高吞吐),很多现代 MQ 产品使用自定义的二进制协议,通常兼容 Kafka 协议。

  • 对应产品

    • Kafka:使用自定义的基于 TCP 的二进制协议,强调高吞吐和分区日志存储。
    • RocketMQ:使用自定义协议,但也兼容 OpenMessaging 标准。
    • Pulsar:支持多种协议,但核心使用其自定义的二进制协议(Pulsar Protocol)。

3、为什么要用消息队列(作用)?

消息队列中间件是分布式系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。目前使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ。

4、RabbitMQ

RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。

rabbitMQ是一款基于AMQP协议的消息中间件,它能够在应用之间提供可靠的消息传输。在易用性,扩展性,高可用性上表现优秀。使用消息中间件利于应用之间的解耦,生产者(客户端)无需知道消费者(服务端)的存在。而且两端可以使用不同的语言编写,大大提供了灵活性。

5、RabbitMQ的安装

  • 服务端:
bash 复制代码
安装配置epel源
   $ rpm -ivh http://dl.fedoraproject.org/pub/epel/6/i386/epel-release-6-8.noarch.rpm
 
安装erlang
   $ yum -y install erlang
 
安装RabbitMQ
   $ yum -y install rabbitmq-server
   
 启动(无用户密码)
     service rabbitmq-server start/stop
     
 设置用户密码
     sudo rabbitmqctl add_user username password
     # 设置用户为administrator角色
     sudo rabbitmqctl set_user_tags username administrator
     # 设置权限
     sudo rabbitmqtcl set_permissions -p "/" root ".*" ".*" ".*"
     
     service rabbitmq-server start/stop
  
  • 客户端:

    pip3 install pika

6、使用

6.1 简单模式

ini 复制代码
# ================ 生产者==================
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello')

channel.basic_publish(exchange='',
                      routing_key='hello',
                      body='Hello World!')

print(" [x] Sent 'Hello World!'")

# =====================消费者===============
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()

channel.queue_declare(queue='hello')

def callback(ch, method, properties, body):
    print(" [x] Received %r" % body)


channel.basic_consume(queue='hello',
                      auto_ack=True,
                      on_message_callback=callback)


print(' [*] Waiting for messages. To exit press CTRL+C')
channel.start_consuming()

6.2 参数

- 应答参数ack(确保消费方接收安全)

ini 复制代码
auto_ack=False
ch.basic_ack(delivery_tag=method.delivery_tag)

- 持久化参数durable(确保生产者数据安全)

ini 复制代码
#声明queue
channel.queue_declare(queue='hello2', durable=True)  # 若声明过,则换一个名字
 
channel.basic_publish(exchange='',
                      routing_key='hello2',
                      body='Hello World!',
                      properties=pika.BasicProperties(
                          delivery_mode=2,  # make message persistent
                          )
                      )

- 分发参数prefetch_count

有两个消费者同时监听一个的队列。其中一个线程sleep2秒,另一个消费者线程sleep1秒,但是处理的消息是一样多。这种方式叫轮询分发(round-robin)不管谁忙,都不会多给消息,总是你一个我一个。想要做到公平分发(fair dispatch),必须关闭自动应答ack,改成手动应答。使用basicQos(perfetch=1)限制每次只发送不超过1条消息到同一个消费者,消费者必须手动反馈告知队列,才会发送下一个。

ini 复制代码
channel.basic_qos(prefetch_count=1)

6.3 交换机模式

交换机之发布订阅

发布订阅和简单的消息队列区别在于,发布订阅会将消息发送给所有的订阅者,而消息队列中的数据被消费一次便消失。所以,RabbitMQ实现发布和订阅时,会为每一个订阅者创建一个队列,而发布者发布消息时,会将消息放置在所有相关队列中。

ini 复制代码
# 生产者
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs',
                         exchange_type='fanout')

message = "info: Hello World!"
channel.basic_publish(exchange='logs',
                      routing_key='',
                      body=message)
print(" [x] Sent %r" % message)
connection.close()

# 消费者
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters(host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs',
                         exchange_type='fanout')

result = channel.queue_declare("",exclusive=True)
queue_name = result.method.queue

channel.queue_bind(exchange='logs',
                   queue=queue_name)

print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)


channel.basic_consume(queue=queue_name,
                      auto_ack=True,
                      on_message_callback=callback)

channel.start_consuming()

交换机之关键字

ini 复制代码
# 生产者
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs2',
                         exchange_type='direct')

message = "info: Hello Yuan!"
channel.basic_publish(exchange='logs2',
                      routing_key='info',
                      body=message)
print(" [x] Sent %r" % message)
connection.close()

# 消费者

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs2',
                         exchange_type='direct')

result = channel.queue_declare("",exclusive=True)
queue_name = result.method.queue


severities = sys.argv[1:]
if not severities:
    sys.stderr.write("Usage: %s [info] [warning] [error]\n" % sys.argv[0])
    sys.exit(1)

for severity in severities:
    channel.queue_bind(exchange='logs2',
                       queue=queue_name,
                       routing_key=severity)

print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)


channel.basic_consume(queue=queue_name,
                      auto_ack=True,
                      on_message_callback=callback)

channel.start_consuming()

交换机之通配符

通配符交换机"与之前的路由模式相比,它将信息的传输类型的key更加细化,以"key1.key2.keyN...."的模式来指定信息传输的key的大类型和大类型下面的小类型,让消费者可以更加精细的确认自己想要获取的信息类型。而在消费者一段,不用精确的指定具体到哪一个大类型下的小类型的key,而是可以使用类似正则表达式(但与正则表达式规则完全不同)的通配符在指定一定范围或符合某一个字符串匹配规则的key,来获取想要的信息。

"通配符交换机"(Topic Exchange)将路由键和某模式进行匹配。此时队列需要绑定在一个模式上。符号"#"匹配一个或多个词,符号""仅匹配一个词。因此"audit.#"能够匹配到"audit.irs.corporate",但是"audit."只会匹配到"audit.irs"。(这里与我们一般的正则表达式的"*"和"#"刚好相反,这里我们需要注意一下。)

ini 复制代码
# 生产者
import pika

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs3',
                         exchange_type='topic')

message = "info: Hello ERU!"
channel.basic_publish(exchange='logs3',
                      routing_key='europe.weather',
                      body=message)
print(" [x] Sent %r" % message)
connection.close()

# 消费者

import pika
import sys

connection = pika.BlockingConnection(pika.ConnectionParameters(
        host='localhost'))
channel = connection.channel()

channel.exchange_declare(exchange='logs3',
                         exchange_type='topic')

result = channel.queue_declare("",exclusive=True)
queue_name = result.method.queue



channel.queue_bind(exchange='logs3',
                   queue=queue_name,
                   routing_key="#.news")

print(' [*] Waiting for logs. To exit press CTRL+C')

def callback(ch, method, properties, body):
    print(" [x] %r" % body)


channel.basic_consume(queue=queue_name,
                      auto_ack=True,
                      on_message_callback=callback)

channel.start_consuming()

7、常见面试题

基础概念篇

1. 什么是消息中间件(MQ)?为什么要使用它?

消息中间件(Message Queue, MQ)是分布式系统中用于应用间异步通信的中间件。它通过"消息存储+转发"的模式,让服务之间不再直接强依赖调用。

引入MQ主要为了解决三大核心问题:

  • 异步处理 (Asynchronous) :将非核心、耗时的操作(如发送邮件、记录日志)异步化,让主流程快速响应,提升用户体验。
  • 应用解耦 (Decoupling) :服务A只需将消息发送到MQ,无需关心谁消费、如何消费。下游服务B、C、D可以独立地订阅和处理消息,系统扩展和维护更加灵活。
  • 流量削峰 (Traffic Shaping) :在秒杀、大促等流量洪峰场景下,MQ可以作为缓冲区,将所有请求先接收并排队,后端服务再按照自己的处理能力匀速消费,避免系统被瞬间击垮。

2. 消息队列有哪些核心模型?

主要有两种核心通信模型:

  • 点对点模型 (P2P / Queue) :一条消息只能被一个消费者消费。消息被消费后就会从队列中移除。适用于任务分发场景。
  • 发布/订阅模型 (Pub/Sub / Topic) :一条消息可以被多个订阅了该主题的消费者同时消费。适用于广播通知场景。

核心挑战与解决方案篇

这部分是面试的重中之重,考察你对MQ深层次问题的理解和解决能力。

1. 如何保证消息不丢失?(可靠性)

要保证消息的可靠性,需要从生产者、MQ Broker、消费者三个环节入手,形成闭环。

  • 生产者端 :开启消息确认机制 (如RabbitMQ的publisher confirm,Kafka的acks=all)。确保消息已成功发送到MQ Broker,如果发送失败则进行重试。
  • MQ Broker端 :开启消息持久化。将消息写入磁盘,防止MQ服务重启或宕机导致内存中的消息丢失。同时,可以采用主从复制(如RocketMQ、Kafka)来保证高可用。
  • 消费者端 :关闭自动确认,采用手动ACK(Acknowledgment)。只有在业务逻辑成功处理完毕后,再向MQ发送确认信号。如果处理失败,可以返回NACK让消息重新入队或进入死信队列。

2. 如何保证消息不被重复消费?(幂等性)

网络抖动等原因可能导致同一条消息被投递多次。解决这个问题的核心是保证消费者接口的幂等性,即无论一个操作被执行多少次,其结果都是一致的。

常见的幂等性方案有:

  • 数据库唯一约束:将消息的业务唯一ID(如订单号)作为数据库表的主键或唯一索引。重复插入时会因违反唯一约束而失败,直接忽略即可。
  • Redis分布式锁 :在处理消息前,先以消息ID为Key在Redis中尝试获取锁(SETNX命令)。如果获取成功则处理业务,处理完再释放锁;如果获取失败,说明该消息正在被处理或已处理过,直接跳过。
  • 状态机判断 :在更新数据时带上状态条件。例如,更新订单状态时,SQL写成 UPDATE order SET status = 'PAID' WHERE id = ? AND status = 'UNPAID',这样即使重复执行,也只会成功一次。

3. 如何保证消息的顺序性?

在某些场景下(如订单的创建->支付->发货),消息必须按顺序处理。保证顺序的核心思路是将需要保证顺序的消息路由到同一个队列/分区,并保证单线程消费

  • 生产者端 :发送消息时,指定一个分区键(Partition Key) ,例如订单ID。MQ会根据这个Key进行哈希,确保同一个订单的所有消息都进入同一个队列/分区。
  • 消费者端 :对于同一个队列/分区,必须使用单线程进行消费。如果开启多线程并发消费,就无法保证消息的处理顺序。

4. 消息大量堆积了怎么办?

消息堆积意味着消费者的处理速度跟不上生产者的生产速度,是一个需要紧急处理的线上问题。

  • 紧急扩容

    1. 增加消费者实例:快速部署更多的消费者服务,提升整体消费能力。
    2. 增加队列/分区数:如果当前队列数少于消费者数,可以先临时增加队列/分区,再增加消费者,实现水平扩展。
  • 优化消费逻辑:排查消费者代码,优化耗时的操作,如慢SQL查询、外部接口调用等,提升单个消费者的处理效率。

  • 降级处理:如果消息不是特别重要,可以考虑暂时丢弃或写入一个临时的存储(如数据库),待高峰期过后再进行异步处理。

主流产品选型篇

Kafka、RocketMQ、RabbitMQ 该如何选择?

这三款是目前最主流的MQ产品,它们的侧重点不同,适用于不同的业务场景。

产品 核心特点 典型应用场景
RabbitMQ 功能全面,路由灵活,社区成熟,可靠性高。 中小规模应用,对消息路由、延迟队列、可靠性要求高的场景。
RocketMQ 阿里开源,高吞吐,支持事务消息、顺序消息,金融级可靠。 电商交易、金融支付、物流等核心业务链路,需要高可靠和海量堆积能力。
Kafka 极高的吞吐量,性能卓越,生态完善,持久化能力强。 大数据日志收集、用户行为追踪、实时流计算(如Flink/Spark)。

一句话选型建议:

  • 大数据、日志处理 ,首选 Kafka
  • 电商、金融等核心业务 ,追求高可靠和功能丰富,首选 RocketMQ
  • 中小型项目 ,需要灵活的路由和快速上手,首选 RabbitMQ
相关推荐
卷毛的技术笔记3 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
会编程的土豆3 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
喵个咪4 小时前
GoWind Toolkit Go后端代码生成 完整全流程实战
后端·go·orm
basketball6164 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
qq_2518364574 小时前
SpringBoot+Vue 共享电池柜管理系统 完整实现 前后端分离项目实战 完整代码
vue.js·spring boot·后端
zhangxingchao5 小时前
AI 大模型核心六:量化、Workflow 与 Agent、多轮 RAG
前端·人工智能·后端
IT_陈寒6 小时前
Vite打包时遇到的坑,原来问题出在这里
前端·人工智能·后端
ayqy贾杰7 小时前
基层管理的三板斧,在AI时代行不通了
前端·后端·团队管理
Apifox7 小时前
Apifox 5 月更新|Postman 导入优化、Runner 支持非 root 运行、请求代码自动带鉴权
前端·后端·安全
JohnYan7 小时前
工作笔记 - PG分组极值
数据库·后端·postgresql