黑马 RabbitMq 基础篇 学习记录

​​​​

微服务拆分之后,服务间通信是个绕不开的坑。同步调用写起来简单,但一上线问题就来了:下游慢了你也慢,下游挂了你也挂,想加个新服务还得改老代码。消息队列(MQ)就是用来解这个耦的。在众多 MQ 中,RabbitMQ 算不上吞吐量最高的,但胜在稳定、好上手、社区成熟,特别适合中小团队和对可靠性要求高的业务场景。

这篇文章从我自己的使用经验出发,把 RabbitMQ 的核心概念、安装部署、SpringAMQP 开发实战串一遍,附带踩过的坑和避坑建议。

一、同步 vs 异步:为什么需要消息队列?

1.1 同步调用的问题

同步调用就是"调用方等着被调用方返回结果,然后继续往下走"。这没什么问题------前提是你的调用链很短,而且每个环节都稳定。但实际情况往往是:

一个下单接口要调库存、调优惠券、调风控、调物流......串行下来,响应时间是累加的。更要命的是,如果风控服务挂了,整个下单链路就都失败了,哪怕库存和优惠券完全正常。

这就是同步调用的三个核心问题:

  • 耦合高:加一个下游,上游就得改代码发通知;
  • 响应慢:串行调用,延迟逐级叠加;
  • 级联故障:一个服务挂了,整个链路跟着崩。

1.2 异步怎么解决

异步调用的思路是:发完消息就走,不等人处理完。就像你给同事发了个消息就去忙别的,而不是站他工位旁边等回复。

MQ 在这个模型里扮演"中间人":

  • 生产者发消息到 MQ;
  • MQ 暂存并转发消息;
  • 消费者从 MQ 拿消息并处理。

带来的好处很直接:

  • 解耦:上下游只跟 MQ 打交道,互相不知道对方存在;
  • :发完消息就返回,不用等下游处理完;
  • 故障隔离:下游挂了不影响上游,消息在 MQ 里排队等着,恢复后继续消费;
  • 削峰:瞬时高并发来了,MQ 先扛着,下游按自己的节奏慢慢消化。

当然也有代价:消息处理变成异步了,你没法立刻知道下游处理成功没有;整个链路多了一个 MQ 组件要维护;消息丢了、重复了怎么办------这些都需要额外设计。

1.3 选型:为什么是 RabbitMQ?

市面上主流的几款 MQ 各有侧重,没有银弹:

特性 Kafka RabbitMQ RocketMQ ActiveMQ
协议 自定义 AMQP, MQTT, STOMP 自研 JMS, AMQP
吞吐量 极高(百万级 TPS) 中等(万级 TPS) 高(十万级 TPS) 低(万级 TPS)
延迟 毫秒~秒级 极低(毫秒级) 低(毫秒级) 毫秒级
可靠性 高(多副本) 高(ACK 机制) 极高(金融级)
事务消息 不支持 插件支持 原生支持 支持
顺序消息 分区内有序 单队列有序 分区内严格有序 单队列有序
扩展性 水平扩展极佳 集群扩展较复杂 水平扩展好 以垂直扩展为主
学习成本 中(文档全、社区好)

简单粗暴的选型建议:

  • 日志收集、大数据管道 → Kafka,吞吐量是它最大的优势;
  • 金融级可靠性、事务消息 → RocketMQ;
  • 中小系统、业务异步解耦、延迟敏感 → RabbitMQ,稳定够用,关键是出问题了好排查;
  • ActiveMQ 基本可以跳过,社区活跃度和性能都不如前三个。

如果你的场景是"订单支付后通知各个业务方""用户注册后发欢迎邮件、初始化账户"这类业务解耦需求,RabbitMQ 非常合适。

二、RabbitMQ 核心概念与安装

RabbitMQ 是 Erlang 写的,所以传统方式安装需要先装 Erlang 环境,比较麻烦。生产环境推荐 Docker 部署,一行命令搞定。

2.1 六个核心概念

理解这六个概念就理解了 RabbitMQ 的工作原理:

  • Producer(生产者):发送消息的一方,把消息发到交换机;
  • Consumer(消费者):接收消息的一方,监听队列并处理;
  • Queue(队列):消息的存储容器,RabbitMQ 里消息最终是存在队列里的;
  • Exchange(交换机):消息的路由器,它不存消息,只负责把消息转发到对应的队列;
  • Binding(绑定):交换机和队列之间的"连线",同时指定绑定键(BindingKey);
  • RoutingKey(路由键):生产者发消息时指定的标识,交换机根据它和 BindingKey 的匹配规则来决定消息去哪。

消息流向:Producer → Exchange → [根据 RoutingKey + Binding 规则] → Queue → Consumer。

2.2 Docker 安装

临时体验
bash 复制代码
docker run -d --name rabbitmq \
  -p 5672:5672 -p 15672:15672 \
  --restart=always \
  rabbitmq:management
生产环境(推荐挂载数据卷)
bash 复制代码
docker run -d \
  -e RABBITMQ_DEFAULT_USER=fei \
  -e RABBITMQ_DEFAULT_PASS=fei \
  -v mq-plugins:/plugins \
  --name mq \
  --hostname mq \
  -p 5672:5672 \
  -p 15672:15672 \
  --network hmall \
  --restart=always \
  rabbitmq:management

端口说明:

  • 5672:AMQP 协议端口,Java 客户端连接用;
  • 15672 :管理后台端口,浏览器访问 http://你的IP:15672

容器启动后,访问管理后台用你设的用户名密码登录。默认的 guest/guest 只能本地访问,远程会被拒绝------这是 RabbitMQ 的安全策略,不是 bug。

常用 Docker 数据卷操作
操作 命令 说明
创建卷 docker volume create [卷名] 创建命名卷,方便管理
查看卷 docker volume ls / docker volume inspect [卷名] 列出所有卷或查详情(能看到宿主机路径)
删除卷 docker volume rm [卷名] 删除指定卷
清理 docker volume prune 删除所有未被容器引用的卷

2.3 Virtual Host:数据隔离

RabbitMQ 用 vhost 来做多租户隔离。每个 vhost 有独立的队列、交换机、用户权限,互相不可见。多个项目共用一个 RabbitMQ 实例时,给每个项目建一个 vhost 是常规操作。

操作步骤(管理后台):

  1. 进入 Admin → Users,新建用户(比如 hmall);
  2. 进入 Admin → Virtual Hosts,新建 vhost(比如 /hmall);
  3. 在用户权限里,把该用户绑定到对应 vhost,授予 configure、read、write 权限。

常见坑 :新建的用户默认没有 vhost 的 configure 权限,导致 Java 客户端无法声明队列和交换机。如果代码里报 access to vhost '/' refused 之类的错,先去管理后台检查权限。

三、SpringAMQP 实战

SpringAMQP 是 Spring 对 AMQP 协议的封装,省去了手动管理 Connection、Channel 的麻烦,通过注解和配置就能快速上手。

3.1 快速入门

场景:创建队列 simple.queue,生产者发消息,消费者收消息。

1. 引入依赖(生产者和消费者都要加):

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2. 配置连接信息(application.yml):

yaml 复制代码
spring:
  rabbitmq:
    host: 192.168.56.2
    port: 5672
    virtual-host: "/"
    username: guest
    password: guest

3. 生产者发消息

java 复制代码
@Autowired
private RabbitTemplate rabbitTemplate;

@Test
void contextLoads() {
    rabbitTemplate.convertAndSend("hello.queue1", "hello from java");
}

4. 消费者收消息

java 复制代码
@Component
@Slf4j
public class SpringRabbitMQListener {

    @RabbitListener(queues = {"hello.queue1"})
    public void receive(String message) {
        log.info("receive message: {}", message);
    }
}

就这样,一个最简单的收发就通了。下面来看生产环境中真正会遇到的问题和解决方案。

3.2 WorkQueue:消息堆积怎么办

如果生产者发消息的速度远超消费者的处理速度,消息就会在队列里越堆越多。WorkQueue 的思路是:一个队列绑多个消费者,一起干活。

但这里有一个容易被忽视的细节。RabbitMQ 默认是轮询分发------不管你处理完没处理完,轮流给每个消费者发一条。这就导致:处理快的消费者闲着,处理慢的消费者面前堆了一堆消息。

比如两个消费者,消费者1每秒处理 50 条,消费者2每秒只能处理 5 条。轮询分发的结果是消费者2被塞了大量消息,处理不过来,消费者1反而没事干。

解决方案:设置 prefetch = 1,告诉 RabbitMQ"每次只给这个消费者一条消息,处理完了再给下一条"。这样消息就自然流向处理快的消费者,实现"能者多劳"。

yaml 复制代码
spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1

生产者测试代码:

java 复制代码
@Test
void testWQ() {
    for (int i = 0; i < 50; i++) {
        rabbitTemplate.convertAndSend("work.queue", "hello from java = " + i);
        try {
            TimeUnit.MILLISECONDS.sleep(20); // 1秒50条
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

消费者代码:

java 复制代码
// 消费者1:处理快
@RabbitListener(queues = {"work.queue"})
public void receiveWQ1(String message) {
    System.out.println("消费者1 收到 work.queue = " + message);
}

// 消费者2:模拟慢处理
@RabbitListener(queues = {"work.queue"})
public void receiveWQ2(String message) throws InterruptedException {
    System.err.println("消费者2 收到 work.queue = " + message);
    TimeUnit.MILLISECONDS.sleep(200); // 每秒5条
}

3.3 交换机:消息怎么路由

实际的业务开发中,消息不会直接发到队列,而是发到交换机,由交换机决定路由到哪个队列。RabbitMQ 有三种核心交换机类型,分别对应不同的路由场景。

3.3.1 Fanout(广播)

Fanout 最粗暴:收到消息后,直接广播到所有绑定的队列,不看 RoutingKey。

适合场景:一条消息要通知多个业务方,比如"用户注册成功"要同时触发发邮件、发优惠券、初始化账户。

测试方法:创建两个队列 fanout.queue1 和 fanout.queue2,都绑定到同一个 Fanout 交换机,生产者发消息后两个队列都会收到。

3.3.2 Direct(定向)

Direct 按精确匹配路由:消息的 RoutingKey 必须和队列绑定的 BindingKey 完全一致才转发。

适合场景:按业务类型分流,比如订单相关的消息路由到订单队列,支付相关的路由到支付队列。

java 复制代码
@Test
void testDirect() {
    // RoutingKey = "dq1",只有 BindingKey 也是 "dq1" 的队列能收到
    rabbitTemplate.convertAndSend("hmall.direct", "dq1", "direct test");
}
3.3.3 Topic(话题)

Topic 最灵活,支持通配符匹配。RoutingKey 是多个单词用 . 分隔(比如 order.pay.success),BindingKey 用通配符来匹配:

  • #:匹配 0 个或多个单词(china.# 能匹配 china.scchina.sc.bz);
  • *:匹配恰好 1 个单词(china.* 能匹配 china.sc,但不能匹配 china.sc.bz)。

适合场景:按模块+事件类型做精细路由。比如订单模块的所有消息(order.#)都进订单队列,但支付成功(order.pay.success)额外多投一份到通知队列。

3.4 用代码声明队列和交换机

除了在管理后台手动创建,SpringAMQP 支持在代码里声明,开发阶段能省不少事。

方式一:配置类(推荐)
java 复制代码
@Configuration
public class RabbitMQConfig {

    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("mall.fanout");
    }

    @Bean
    public Queue queue1() {
        return new Queue("mall.queue1");
    }

    @Bean
    public Binding bindingQueue(Queue queue1, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(queue1).to(fanoutExchange);
    }
}

配置类的好处是集中管理,一眼能看清所有队列和交换机的关系。

方式二:注解声明

直接在 @RabbitListener 上用注解声明,简单场景够用:

java 复制代码
@RabbitListener(
    bindings = @QueueBinding(
        value = @Queue(name = "inject.queue1"),
        exchange = @Exchange(name = "inject.direct", type = ExchangeTypes.DIRECT),
        key = {"inject.d", "d.i"}
    )
)
public void receiveDirect(String message) {
    System.out.println("收到 Direct 消息:" + message);
}

不过注解方式有个缺点:队列和交换机的声明散落在各个消费者里,项目大了不好找。建议小项目用注解,大项目用配置类。

3.5 消息序列化:换成 JSON

SpringAMQP 默认用 JDK 序列化(SimpleMessageConverter),这个默认值很坑:

  • 消息体是二进制,在管理后台看到的是乱码,排查问题不方便;
  • 体积大;
  • 发送方和接收方必须用同一个 Java 类,限制了跨语言消费。

换成 JSON,两步搞定:

步骤一:引入 Jackson

xml 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

步骤二:配置 MessageConverter

java 复制代码
@Bean
public MessageConverter messageConverter() {
    return new Jackson2JsonMessageConverter();
}

之后发送 Map、对象都可以,消息在管理后台也能直接看到 JSON 文本了。

java 复制代码
// 生产者
@Test
void testMap() {
    HashMap<String, String> map = new HashMap<>();
    map.put("name", "jack");
    rabbitTemplate.convertAndSend("hmall.topic", "china", map);
}

// 消费者
@RabbitListener(queues = "topic.queue1")
public void receiveTopic(Map<String, String> message) {
    System.out.println("消费者 map 绑定 = " + message);
}

注意 :生产者和消费者的消息类型(以及泛型里的类型)必须一致,否则反序列化会报错。比如发送方发的是 Map<String, String>,接收方也必须是 Map<String, String>,不能是 MapHashMap

3.6 实战:支付成功后异步通知

这是一个真实的业务场景:用户支付成功后,原本是用 OpenFeign 同步调用订单服务来更新订单状态。改造成异步通知,支付服务发一条消息就走,不用等订单服务处理完。

支付服务(生产者)

java 复制代码
@Test
void testPay() {
    // 支付成功后,发订单ID通知订单服务
    rabbitTemplate.convertAndSend("pay.topic", "pay.success", 910101010L);
}

订单服务(消费者)

java 复制代码
@Component
@RequiredArgsConstructor
public class PayStatusListener {

    private final OrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
        value = @Queue(name = "mark.pay.status.queue", durable = "true"),
        exchange = @Exchange(name = "pay.topic", type = ExchangeTypes.TOPIC),
        key = "pay.success"
    ))
    public void listen(Long orderId) {
        orderService.markOrderPaySuccess(orderId);
    }
}

这里 durable = "true" 表示队列持久化,即使 RabbitMQ 重启队列也不会丢。生产环境中,交换机、队列、消息都应该持久化,这是保证消息不丢失的第一道防线。

四、总结与避坑清单

写了这么多,核心其实就一条:理解"生产者 → 交换机 → 队列 → 消费者"这条链路,以及交换机根据什么规则把消息路由到哪个队列。剩下的都是在不同场景下的组合应用。

几个刚上手容易踩的坑:

  1. 权限问题 :新建的用户要显式授予 vhost 的 configure 权限,不然代码里声明不了交换机和队列。表现就是启动报 access refused,去管理后台检查一下权限配置往往能搞定。

  2. 消息丢了:开发环境无所谓,上生产一定要开持久化(队列持久化 + 消息持久化 + 交换机持久化),并配置消费者手动 ACK。只靠默认配置上线,消息丢失是迟早的事。

  3. 消息堆积 :WorkQueue 模式记得设 prefetch,不然轮询分发会让慢消费者拖垮整体吞吐。另外,如果消费速度长期跟不上生产速度,加消费者只是缓兵之计,根本上还是要优化消费逻辑或者加机器。

  4. 序列化问题:一上来就把默认的 JDK 序列化换成 JSON,别等项目跑起来了再改,那时候消息格式不兼容就很头疼。

  5. 消息类型要一致 :发送端和接收端的消息类型必须严格匹配,泛型也要一致。生产上建议定义统一的 DTO 类来传消息,不要用 Map 这样的松散结构,方便维护也减少序列化问题。

  6. 交换机选型:Fanout(广播)、Direct(精确匹配)、Topic(通配符匹配),根据实际需求选,别所有场景都用 Topic------规则越复杂,排查问题时越痛苦。

RabbitMQ 入门不难,但它有不少高级特性值得深入:死信队列、延迟队列、消息确认机制、集群部署等等。先把基础打牢,后续再按需深入,足够应对大多数业务场景了。

相关推荐
南子北游1 小时前
计算机视觉学习(三)全连接神经网络
神经网络·学习·计算机视觉
Titan20241 小时前
C++特殊类设计
c++·学习
再玩一会儿看代码1 小时前
Token 统计中的“命中缓存”和“未命中缓存”是什么意思?
经验分享·学习·缓存·电脑
守护安静星空1 小时前
交流桩学习-控制导引
学习
晓梦林2 小时前
Fuzzz靶场学习笔记
笔记·学习·安全·web安全
网安Ruler2 小时前
安卓逆向入门到入狱学习2
android·学习
小麦大叔2 小时前
给嵌入式工程师推荐一个 FOC 学习项目
学习·fpga开发
小新同学^O^2 小时前
简单学习 --> Spring统一处理
java·学习·spring·统一功能处理
小新同学^O^2 小时前
简单学习 --> 数据加密
java·数据库·学习·数据加密