欢迎光临
我们一直在努力

从0-1一文详解RabbitMQ

前言

RabbitMQ 是微服务中常见的消息中间件,主要用来完成异步通信、服务解耦和流量削峰。本文从同步调用的问题开始,依次讲解 RabbitMQ 的核心对象、Spring Boot 集成、Work Queue、交换机、消息可靠性、业务幂等和延迟消息,并给出可以直接改造到 Spring AMQP 项目中的代码示例。

一、为什么需要消息队列

同步调用中,调用方必须等待被调用方返回结果。以支付为例,支付服务扣款后还要同步调用订单、通知、积分等服务。下游服务越多,调用链越长,支付请求的响应时间越难控制;其中一个服务变慢或故障,还可能占满上游线程,引发级联失败。

异步调用把“通知下游处理”变成一条消息。发送方把消息交给消息代理后即可继续执行,消费者在自己的节奏下处理消息。这样可以减少服务之间的直接依赖,并用队列暂存突发流量。

异步调用适合"后续动作依赖前置结果,但前置服务不需要等待后续结果"的场景,例如支付成功后通知订单服务更新状态、通知服务发送消息、积分服务增加积分。若下一步必须立即拿到上一步的返回值,仍应使用同步调用。

异步方案需要接受几个代价:调用方不能立即得到下游结果,消息代理或消费者故障需要补偿,消息可能重复投递。因此,可靠性和幂等性必须一起设计。

二、RabbitMQ基础概念与环境准备

1. RabbitMQ的核心对象

RabbitMQ中最重要的对象有五个:

  • Publisher:发送消息的生产者。
  • Exchange:交换机,接收消息并根据路由规则转发。
  • Queue:队列,保存等待消费的消息。
  • Consumer:消费者,从队列接收并处理消息。
  • VirtualHost:虚拟主机,用于隔离不同业务的交换机、队列、绑定和权限。

消息的典型路径是:

Publisher -> Exchange -> Binding + RoutingKey -> Queue -> Consumer

交换机只负责路由和转发,不负责长期存储消息。生产者直接向队列发送时,使用的是 RabbitMQ 的默认交换机;进入真实业务后,通常显式声明业务交换机和绑定关系。

2. Spring AMQP依赖和连接配置

Spring Boot 集成 RabbitMQ 时需要引入 spring-boot-starter-amqp:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

连接配置至少要保持 host、port、virtual-host、username 和 password 一致:

spring:
rabbitmq:
host: localhost
port: 5672
virtual-host: /hmall
username: ******
password: ******
connection-timeout: 1s

5672 是 AMQP 客户端端口,15672 是管理页面端口。连接到哪个 VirtualHost,决定了应用能看到哪一组交换机和队列。 请添加图片描述

三、Spring AMQP的基本收发

1. 生产者发送消息

Spring AMQP通过 RabbitTemplate 封装了连接、信道和消息转换。发送字符串到队列可以直接调用:

@Autowired
private RabbitTemplate rabbitTemplate;

@Test
void testSend() {
String queueName = "test.quene";
String message = "hello world";
rabbitTemplate.convertAndSend(queueName, message);
}

第一个参数是目标名称。这里传入队列名,消息会经由默认交换机按队列名路由;第二个参数是消息体。convertAndSend 会根据消息转换器把 Java 对象转换为 RabbitMQ 消息。

2. 消费者监听队列

消费者使用 @RabbitListener 声明监听目标:

@Component
public class MqListener {

@RabbitListener(queues = "test.quene")
public void listenSimpleQuene(String message) {
System.out.println("消费者收到了test.quene消息:" + message);
}
}

应用启动后,Spring AMQP会为监听方法创建消费者。方法参数接收消息体,方法正常返回时消息会进入确认流程;如果方法抛出异常,则由确认模式和重试配置决定消息是否重新投递。

3. 对象消息使用JSON转换

默认的消息转换器可能使用 JDK 序列化,消息在管理页面上可读性较差,也会带来更大的体积和反序列化风险。项目在 publisher 启动类中配置了 JSON 转换器:

@Bean
public MessageConverter messageConverter() {
Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter();
converter.setCreateMessageIds(true);
return converter;
}

Jackson2JsonMessageConverter 负责把对象转换为 JSON;setCreateMessageIds(true) 会为消息生成 messageId,为后续去重提供消息级标识。发送 Map 时:

Map<String, Object> map = new HashMap<>(2);
map.put("name", "张三");
map.put("age", 18);
rabbitTemplate.convertAndSend("Object.queue", map);

消费者应使用与发送端匹配的类型接收消息,例如使用 Map<String, Object> 接收上述 JSON 对象。生产者和消费者的消息转换器必须保持兼容,不能让同一队列中同时混用无法互相解析的序列化格式。

四、Work Queue:多个消费者共同处理一个队列

当单个消费者处理速度跟不上生产速度时,消息会在队列中堆积。Work Queue 让多个消费者监听同一个队列,同一条消息只会交给其中一个消费者处理,适合把任务分摊给多个实例。

生产者可以在短时间内发送一批消息:

@Test
void testSimple() throws Exception {
String queueName = "simple.quene";
for (int i = 1; i <= 50; i++) {
String message = "hello simple:" + i;
rabbitTemplate.convertAndSend(queueName, message);
Thread.sleep(20);
}
}

消费者端配置两个监听方法:

@RabbitListener(queues = "simple.quene")
public void listenSimpleQuene1(String message) throws InterruptedException {
System.out.println("消费者收到了simple.quene消息:" + message);
Thread.sleep(20);
}

@RabbitListener(queues = "simple.quene")
public void listenSimpleQuene2(String message) throws InterruptedException {
System.out.println("消费者收到了simple.quene消息………………:" + message);
Thread.sleep(200);
}

默认分发方式接近轮询,消费者能力不同也可能拿到相近数量的消息。项目在 consumer 模块配置:

spring:
rabbitmq:
listener:
simple:
prefetch: 1

prefetch: 1 表示消费者处理完当前消息后再获取下一条。这样处理速度快的消费者可以继续领取任务,处理速度慢的消费者不会提前积压一批未完成消息。

五、交换机决定消息如何路由

请添加图片描述

1. Fanout:广播到所有绑定队列

Fanout 交换机忽略 routing key,把消息发送给所有绑定队列。一个事件需要被多个业务同时处理时,可以使用广播模式。

可以通过配置类声明交换机、队列和绑定:

@Configuration
public class FanoutConfiguration {

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

@Bean
public Queue fanoutQueue3() {
return new Queue("notification.queue", true);
}

@Bean
public Queue fanoutQueue4() {
return new Queue("points.queue", true);
}

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

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

这里两个队列都绑定到同一个 FanoutExchange,因此同一条消息会分别进入两个队列。队列名称必须稳定,生产者发送时使用交换机名:

rabbitTemplate.convertAndSend("app.fanout", "", "hello everyone");

2. Direct:按精确 routing key 路由

Direct 交换机要求 binding key 与消息的 routing key 精确匹配。下面两个监听器分别声明自己的队列和绑定键:

@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1", durable = "true"),
exchange = @Exchange(
name = "hmall.direct",
type = ExchangeTypes.DIRECT),
key = {"blue", "red"}
))
public void listenDirectQueue1(String message) {
System.out.println("消费者收到了direct.queue1消息:" + message);
}

@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2", durable = "true"),
exchange = @Exchange(
name = "hmall.direct",
type = ExchangeTypes.DIRECT),
key = {"yellow", "red"}
))
public void listenDirectQueue2(String message) {
System.out.println("消费者收到了direct.queue2消息………………:" + message);
}

发送 routing key 为 blue 时,只会匹配 direct.queue1;发送 red 时,两个队列都会匹配。Direct 适合路由条件明确、值域有限的场景。

3. Topic:用通配符匹配 routing key

Topic 交换机在 Direct 的精确匹配基础上增加通配符:* 匹配一个单词,# 匹配零个或多个单词。例如 japan.* 可以匹配 japan.news,#.news 可以匹配 japan.news 或 sports.japan.news。

发送 Topic 消息时,可以把业务领域和事件类型写入 routing key:

rabbitTemplate.convertAndSend("hmall.topic", "japan.news", "日本爆炸了");

Topic 适合把业务领域、事件类型等信息编码进 routing key,再由多个队列按模式订阅。选择交换机时,可以按"全部接收、精确匹配、模式匹配"分别对应 Fanout、Direct、Topic。

六、从发送到入队:生产者可靠性

消息可能在生产者到 RabbitMQ 的连接阶段失败,也可能到达交换机后没有匹配到队列。生产者可靠性通常包括 RabbitTemplate 操作重试、Publisher Confirm 和 Publisher Return:

spring:
rabbitmq:
connection-timeout: 1s
template:
retry:
enabled: true
initial-interval: 200ms
multiplier: 2
max-attempts: 3
mandatory: true
publisher-confirm-type: correlated
publisher-returns: true

connection-timeout 只控制建立连接的超时时间,template.retry 控制 RabbitTemplate 操作失败后的重试,两者作用不同。重试可以应对短暂网络抖动,但同步重试会占用发送线程,重试次数和等待时间需要结合接口响应要求设置。

Publisher Confirm 用来确认 RabbitMQ 是否接收了发布请求;correlated 模式通过 CorrelationData 异步关联每条消息的回执。Confirm不代表消息已经被消费者处理。Publisher Return 用来处理"消息已到交换机,但无法路由到队列"的情况,需要开启 mandatory,并且每个 RabbitTemplate 只能设置一个 ReturnCallback:

@Configuration
@Slf4j
public class MqConfirmConfig implements ApplicationContextAware {

@Override
public void setApplicationContext(ApplicationContext applicationContext) {
RabbitTemplate rabbitTemplate =
applicationContext.getBean(RabbitTemplate.class);
rabbitTemplate.setReturnsCallback(returned -> log.debug(
"收到消息的return callback, exchange:{}, key:{}, msg:{}, code:{}, text:{}",
returned.getExchange(),
returned.getRoutingKey(),
returned.getMessage(),
returned.getReplyCode(),
returned.getReplyText()));
}
}

发送端可以使用 CorrelationData 处理确认:

CorrelationData correlationData =
new CorrelationData(UUID.randomUUID().toString());
correlationData.getFuture().addCallback(
new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
log.error("消息发送失败", ex);
}

@Override
public void onSuccess(@Nullable CorrelationData.Confirm result) {
if (result != null && result.isAck()) {
log.info("消息发送成功");
} else {
log.error("消息发送失败,原因:{}", result.getReason());
}
}
});

rabbitTemplate.convertAndSend("hmall.direct", "red", "hello", correlationData);

确认成功只能说明发布链路达到了对应阶段,业务仍要记录失败消息、处理无法路由的消息,并决定是否重试。错误的交换机名、routing key 或绑定关系,应优先修正配置,而不是无限重发。

请添加图片描述

七、RabbitMQ消息持久化

RabbitMQ的持久化需要同时考虑三个对象:

  • 交换机持久化,保证交换机重启后仍存在。
  • 队列持久化,保证队列重启后仍存在。
  • 消息持久化,保证消息可以被写入磁盘。
  • 只持久化队列而不持久化消息,仍然可能在 RabbitMQ 重启时丢失消息;只持久化消息而没有持久化队列,也无法恢复完整路由链路。持久化交换机、队列和持久化消息是三个独立设置,生产环境应在控制台或代码中明确检查 durable 和 delivery mode。

    1. 交换机和队列持久化

    编程式声明交换机和队列时,可以明确指定持久化:

    @Bean
    public DirectExchange orderExchange() {
    return new DirectExchange("order.direct", true, false);
    }

    @Bean
    public Queue orderQueue() {
    return new Queue("order.queue", true);
    }

    第二个参数 true 表示交换机或队列持久化。RabbitMQ重启后,持久化对象可以重新加载;临时交换机或临时队列则可能随着连接或服务结束而消失。

    2. 消息持久化

    发送消息时,可以通过消息属性指定持久化投递模式:

    Message message = MessageBuilder
    .withBody("订单支付成功".getBytes(StandardCharsets.UTF_8))
    .setDeliveryMode(MessageDeliveryMode.PERSISTENT)
    .build();

    rabbitTemplate.convertAndSend(
    "order.direct", "order.paid", message);

    只有交换机、队列和消息都满足持久化要求,RabbitMQ重启后才有机会恢复完整消息链路。持久化会增加磁盘写入开销,发布确认也应与持久化策略一起考虑。

    3. Lazy Queue和消息堆积

    消费者处理速度过慢时,消息会在队列中积压。旧版本 RabbitMQ中可以使用 x-queue-mode=lazy 让消息更早写入磁盘,降低内存占用。

    @Bean
    public Queue lazyQueue() {
    return QueueBuilder
    .durable("lazy.queue")
    .withArgument("x-queue-mode", "lazy")
    .build();
    }

    RabbitMQ 3.12及之后,经典队列的实际行为已经接近历史上的惰性队列,旧的 x-queue-mode=lazy 配置可能被忽略。使用时应以实际 RabbitMQ 版本的官方文档和管理页面为准,不要把旧版本配置直接当成所有版本都适用的结论。

    消息持久化会带来磁盘写入开销,发布确认在持久化完成后才有意义。消息大量堆积时要关注内存、磁盘和消费者处理速度,避免把持久化当成无限扩容手段。

    八、消费者确认、重试和失败消息

    消费者确认用于告诉 RabbitMQ 消息处理结果:

    • ack:处理成功,消息从队列中删除。
    • nack:处理失败,可以要求消息重新入队。
    • reject:拒绝消息并丢弃,或交给死信流程。

    Spring AMQP的 acknowledge-mode 常见有三种:

    • none:消息投递给消费者后立即确认,业务失败也可能丢消息。
    • manual:业务代码显式调用确认 API,控制最细,但会增加业务代码复杂度。
    • auto:Spring 根据监听方法是否正常返回来确认或拒绝消息,适合大多数普通消费场景。

    普通队列监听器可以配置为 auto 模式:

    spring:
    rabbitmq:
    listener:
    simple:
    acknowledge-mode: auto

    对于简单业务,auto 模式可以让消息处理成功时自动 ack,抛出异常时进入失败处理流程。关键业务要结合重试、幂等和异常队列一起使用。

    如果异常消息一直重新入队,会形成"消费失败 -> requeue -> 再次消费"的无限循环。Spring Retry 可以把有限次数的重试放在消费者本地:

    spring:
    rabbitmq:
    listener:
    simple:
    acknowledge-mode: auto
    retry:
    enabled: true
    initial-interval: 1000ms
    multiplier: 1
    max-attempts: 3
    stateless: true

    max-attempts 是总尝试次数,initial-interval 是第一次失败后的等待时间,multiplier 控制后续等待时间的增长。是否使用 stateless,要根据业务是否依赖事务上下文判断。

    重试耗尽后,MessageRecoverer 决定消息去向:

    • RejectAndDontRequeueRecoverer:拒绝并丢弃,默认策略。
    • ImmediateRequeueMessageRecoverer:重新入队,适合短暂故障,但要防止循环。
    • RepublishMessageRecoverer:把失败消息重新发布到异常交换机,保留失败原因,便于人工处理和补偿。

    可以声明异常交换机、异常队列和恢复器,核心配置如下:

    @Bean
    public MessageRecoverer messageRecoverer(RabbitTemplate rabbitTemplate) {
    return new RepublishMessageRecoverer(
    rabbitTemplate, "error.direct", "error");
    }

    交换机名称、队列名称和恢复器目标必须保持一致,否则重试耗尽后消息无法按预期进入异常队列。异常队列不应被当成普通业务队列长期堆积,需要配套人工处理、补偿或告警机制。

    请添加图片描述

    九、业务幂等:允许重复投递但不能重复生效

    消息系统为了可靠性可能重复投递,消费者重试也可能让同一业务被再次执行。幂等的目标是:同一业务执行一次或多次,对最终业务状态的影响相同。

    一种通用做法是为每条消息设置唯一 ID:

  • 生产者生成业务唯一 ID,并和消息一起发送。
  • 消费者处理业务成功后,把 ID 写入去重表或业务记录。
  • 再次收到相同 ID 时,先查询处理记录,已处理则直接确认并跳过。
  • 项目启用了 JSON 消息 ID:

    converter.setCreateMessageIds(true);

    消息ID只是消息属性,不能自动完成去重。消费者仍需要把已经处理过的消息ID保存到数据库或其他可靠存储中:

    @RabbitListener(queues = "order.queue")
    public void listenOrder(OrderMessage message) {
    if (messageLogService.exists(message.getMessageId())) {
    return;
    }

    orderService.updateOrderStatus(message.getOrderId(), "PAID");
    messageLogService.save(message.getMessageId());
    }

    实际项目中,业务处理和去重记录最好放在同一个事务中,或者通过数据库唯一索引保证同一个消息ID只能成功写入一次。

    更稳妥的做法是使用业务本身的唯一约束。例如支付成功后更新订单状态,更新前先查询订单当前状态,只有"未支付"状态才执行更新;已经支付的订单再次收到消息时直接返回。这样做比单纯依赖消息 ID 更贴近业务,也便于处理消息重发。

    支付服务、交易服务之间的最终一致性可以按三层保障:生产者确认保证消息尽量到达,RabbitMQ持久化减少服务重启造成的丢失,消费者确认和幂等更新保证重复或失败处理不会破坏业务状态。必要时再使用定时任务定期对账,补偿长时间未同步的订单。

    请添加图片描述

    十、延迟消息与延迟消息插件

    延迟消息在发送时指定等待时间,消费者在延迟时间到达后才收到消息。它适合订单超时关闭、定时检查支付状态、延时通知等场景。

    1. 死信交换机

    消息出现以下情况之一时,会成为死信:

    • 消费者使用 reject 或 nack 拒绝消息,并且不重新入队。
    • 消息达到队列或消息本身的过期时间。
    • 队列达到长度上限,旧消息被淘汰。

    队列通过 dead-letter-exchange 指定交换机后,死信会被重新投递到该交换机,再由交换机路由到死信队列。利用"过期后进入死信队列"的过程,可以间接实现延迟消费;不过 TTL 消息通常要等到期消息到达队首后才会被处理,因此它不是精确的定时器。

    下面的配置让消息先进入等待队列,30秒后过期,再通过死信交换机进入真正的业务队列:

    @Bean
    public DirectExchange delaySourceExchange() {
    return new DirectExchange("delay.source", true, false);
    }

    @Bean
    public DirectExchange deadLetterExchange() {
    return new DirectExchange("dlx.direct", true, false);
    }

    @Bean
    public Queue delaySourceQueue() {
    return QueueBuilder.durable("delay.source.queue")
    .ttl(30000)
    .deadLetterExchange("dlx.direct")
    .deadLetterRoutingKey("expired")
    .build();
    }

    @Bean
    public Queue deadLetterQueue() {
    return new Queue("dead-letter.queue", true);
    }

    @Bean
    public Binding sourceBinding(Queue delaySourceQueue,
    DirectExchange delaySourceExchange) {
    return BindingBuilder.bind(delaySourceQueue)
    .to(delaySourceExchange)
    .with("delay");
    }

    @Bean
    public Binding deadLetterBinding(Queue deadLetterQueue,
    DirectExchange deadLetterExchange) {
    return BindingBuilder.bind(deadLetterQueue)
    .to(deadLetterExchange)
    .with("expired");
    }

    ttl(30000) 设置队列中消息的存活时间,deadLetterExchange 指定过期消息的目标交换机,deadLetterRoutingKey 指定死信路由键。生产者把消息发到 delay.source 后,消息会先进入等待队列,过期后再被转发到 dead-letter.queue。

    请添加图片描述

    2. 安装rabbitmq-delayed-message-exchange插件

    RabbitMQ提供了 rabbitmq-delayed-message-exchange 插件,可以让交换机本身暂存延迟消息。插件版本要尽量匹配当前 RabbitMQ 版本,插件文件不是 Maven 依赖,需要安装到 RabbitMQ 服务端。

    # 查看RabbitMQ容器的插件目录
    docker exec rabbitmq rabbitmq-plugins directories -s

    # 将下载的插件文件复制到插件目录,实际目录以命令输出为准
    docker cp rabbitmq_delayed_message_exchange-<version>.ez \\
    rabbitmq:/opt/rabbitmq/plugins/

    # 启用延迟消息插件
    docker exec rabbitmq \\
    rabbitmq-plugins enable rabbitmq_delayed_message_exchange

    # 重启RabbitMQ容器
    docker restart rabbitmq

    启用插件后,RabbitMQ才能识别 x-delayed-message 类型的交换机。插件缺失时,声明 delayed = "true" 的交换机会失败。

    3. 声明延迟交换机和队列

    Spring AMQP可以通过 @Exchange 声明延迟交换机:

    @RabbitListener(bindings = @QueueBinding(
    value = @Queue(name = "delay.queue", durable = "true"),
    exchange = @Exchange(
    name = "delay.direct",
    delayed = "true",
    type = ExchangeTypes.DIRECT),
    key = "delay"
    ))
    public void listenDelayQueue(String message) {
    System.out.println("消费者收到了delay.queue消息:" + message);
    }

    delayed = "true" 表示这个交换机依赖延迟消息插件,key 是消息发送时使用的 routing key。声明队列和交换机后,生产者还必须在消息属性中设置延迟时间。

    4. 发送延迟消息

    发送时通过 MessagePostProcessor 设置延迟毫秒数:

    rabbitTemplate.convertAndSend(
    "delay.direct",
    "delay",
    "Hello, DelayQueue!",
    message -> {
    message.getMessageProperties().setDelay(10000);
    return message;
    });

    setDelay(10000) 表示延迟10秒。延迟时间越长、消息量越大,代理需要维护的延迟消息越多,CPU 和存储压力也会增加。订单超时场景还要考虑消息堆积:很多订单可能在等待期内已经完成支付,消费者收到检测消息后仍需再次查询订单状态,而不能直接执行取消操作。

    如果多个业务都需要发送延迟消息,可以把设置延迟时间的逻辑封装成可复用的消息处理器:

    public class DelayMessagePostProcessor
    implements MessagePostProcessor {

    private final int delay;

    public DelayMessagePostProcessor(int delay) {
    this.delay = delay;
    }

    @Override
    public Message postProcessMessage(Message message) {
    message.getMessageProperties().setDelay(delay);
    return message;
    }
    }

    调用时只需要传入延迟时间:

    rabbitTemplate.convertAndSend(
    "delay.direct",
    "delay",
    "订单超时检测",
    new DelayMessagePostProcessor(30 * 60 * 1000));

    延迟插件适合延迟时间较短、消息量可控的场景。延迟消息过多时,RabbitMQ需要维护更多定时任务,会增加 CPU、内存和磁盘压力。消费者收到订单检测消息后,仍然要查询订单当前状态,只有订单确实未支付时才能执行取消操作。

    请添加图片描述

    总结

    RabbitMQ的基础使用可以归纳为一条链路:生产者通过 RabbitTemplate 发送消息,交换机按类型和 routing key 路由到队列,消费者通过 @RabbitListener 接收并处理。Work Queue解决消费能力不足,Fanout、Direct、Topic解决不同的分发需求。

    可靠性要同时覆盖生产者、RabbitMQ和消费者:发布确认与返回处理发送结果,持久化减少服务重启丢失,消费者确认和有限重试处理异常,异常交换机保留失败消息,幂等设计避免重复消费造成业务错误。延迟消息适合订单超时和定时检查,但消费者最终仍应以数据库中的业务状态为准。

    请添加图片描述

    参考资料

    • RabbitMQ基础篇
    • RabbitMQ高级篇
    • Spring AMQP Reference
    • RabbitMQ官方文档
    赞(0)
    未经允许不得转载:171主机测评 » 从0-1一文详解RabbitMQ
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址