【学习笔记】中间件-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
相关推荐
三千星1 小时前
Java开发者转型AI工程化Week 3:从LangChain4j到AI Agent
后端·langchain
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第45题】【JVM篇】第5题:JVM中,对象何时会进入老年代?
java·开发语言·jvm·后端·面试
空空潍2 小时前
MySQL存储引擎与索引深度解析
后端·sql·mysql·innodb
程序员三明治2 小时前
【AI】一文讲清 RAG:从大模型局限到企业级知识库落地流程
java·人工智能·后端·ai·大模型·llm·rag
l软件定制开发工作室2 小时前
Spring开发系列教程(37)——使用Conditional
java·后端·spring
yangminlei2 小时前
Spring Boot Starter自定义开发 构建企业级组件库
java·spring boot·后端
RemainderTime2 小时前
基于Spring AI + 阿里百炼 DashScope:构建 AI Agent RAG 企业级知识助手
人工智能·后端·spring·ai·es
接着奏乐接着舞2 小时前
springboot 常用注解
spring boot·后端·python
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第44题】【JVM篇】第4题:什么时候会触发 Young GC?什么时候会触发 Full GC?
java·开发语言·jvm·后端·面试