基于 Spring Boot 与 RabbitMQ 的分布式消息通信机制设计与实现

1. rabbit MQ 消息分发模式

模式 Exchange 类型 特点 常见应用场景
1. 简单队列模式(Simple Queue) 默认(无 Exchange) 一个生产者 → 一个队列 → 一个消费者 测试或点对点任务
2. 工作队列模式(Work Queues) direct 多个消费者共同竞争同一队列中的消息,实现负载均衡 异步任务处理、后台批处理
3. 发布/订阅模式(Publish/Subscribe) fanout 生产者将消息广播到多个队列,所有消费者都会收到 日志广播、系统通知、推送消息
4. 路由模式(Routing) direct 消息根据 routing key 精确匹配路由到队列 不同类型日志分发(info、error、warn)
5. 通配符模式(Topics) topic 支持模糊匹配,如 log.*log.# 复杂的主题订阅系统
6. RPC 模式 临时 direct Exchange 模拟远程调用机制,通过请求队列和回调队列 分布式任务同步请求场景

1.1 工作队列模式

1.2 发布订阅模式

1.3 路由模式

1.4 主题模式

2. 消息可靠性投递

阶段 可能失败原因 对应机制 解决方案
生产者 → Exchange 网络异常、Exchange 不存在 Publisher Confirm 回调确认+重发
Exchange → Queue 路由键错误、队列不存在 Return Callback 记录日志/死信转发
Queue 存储 非持久化、Broker 崩溃 Message Persistence 持久化配置
Queue → Consumer 消费异常、程序崩溃 ACK/NACK 手动确认 重试、DLX
全流程异常处理 各阶段累积问题 死信队列 DLX 后续分析/人工干预

2.1 消息没有发送到消息队列

解决思路A:在生产者端进行确认,具体操作中我们会分别针对交换机和队列来确认,如果没有成功发送到消息队列服务器上,那就可以尝试重新发送

解决思路B:为目标交换机指定备份交换机,但目标交换机投递失败时,把消息投递至备份交换机

2.2 消息队列服务器宕机导致内存中消息丢失

解决思路:消息持久化到硬盘,哪怕服务器重启也不会导致消息丢失。默认配置是支持持久化

2.3 消费端宕机或抛异常导致消息没有成功被消费

消费端消费消息成功,给服务器返回ACK信息,然后消息队列删除该消息

消费端消费消息失败,给服务器端返回NACK消息,同时把消息恢复为带消费端状态,这样就可以再次取回消息,重试一次。

3 死信

  • 概念:当一个消息无法被消费,它就变成了死信。
  • 死信产生的原因大致有下面三种
    • 拒绝 :消费者拒接消息,使用basicNack()/basicReject(),并且不把消息重新放入原目标队列,即requeue=false
    • 溢出:队列中消息数量到达限制。比如队列最大只能存储10条消息,且现在已经存储了10条,此时如果再发送一条消息进来,根据先进先出原则,队列中最早的消息会变成死信。
    • 超时:消息到达超时时间未被消费。
  • 死信的处理方式大致有下面三种
    • 丢弃:对不重要的消息直接丢弃,不做处理。
    • 入库:把死信写入数据库,日后处理。
    • 监听:消息变成死信后进入死信队列,专门设置消费端监听死信队列,做后续处理(通常采用)。

3.1 死信的绑定

正常链路

生产者 → 正常交换机(Normal Exchange)→ 正常队列(Normal Queue)→ 消费者

死信链路

Normal Queue(死信触发)→ 死信交换机(Dead Exchange)→ 死信队列(Dead Queue)

3.1.1 创建两套完整的路由体系

需要先创建:

  • 一个普通交换机(如 tcp.data.exchange);

  • 一个普通队列(如 tcp.data.queue);

  • 一个死信交换机(如 dead.exchange);

  • 一个死信队列(如 dead.queue)。

3.1.2 在普通队列上绑定死信交换机

args.put("x-dead-letter-exchange", "dead.exchange"); // 指定死信交换机

args.put("x-dead-letter-routing-key", "dead.key"); // 指定死信路由键

这样,当普通队列中的消息变成"死信"时,RabbitMQ 会自动把它重新路由到 dead.exchange,再由死信交换机转发到 dead.queue

java 复制代码
package com.wulang.tcp.config;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration // ioc容器初始化时加载这个配置类进行对象创建
@Slf4j
public class RabbitConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback {

    public static final String NORMAL_EXCHANGE = "tcp.data.exchange";
    public static final String NORMAL_QUEUE = "tcp.data.queue";
    public static final String ROUTING_KEY = "tcp.data.key";

    public static final String DEAD_EXCHANGE = "dead.exchange";
    public static final String DEAD_QUEUE = "dead.queue";

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 对rabbitTemplate的功能进行增强,用在发送端rabbit配置中
     */
    @PostConstruct // 对象创建之后立即执行的注解
    public void initRabbitTemplate() {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnsCallback(this);
    }

    /**
     * 声明回调函数来接收RabbitMQ服务器返回的确认信息
     * 确认消息是否发送到交换机
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        // 消息发送到交换机成功或者失败时调用这个方法
        log.debug("发送交换机确认 confirm() 回调函数 CorrelationData: {}", correlationData);
        log.debug("发送交换机确认 confirm() 回调函数 ack: {}", ack);
        log.debug("发送交换机确认 confirm() 回调函数 cause: {}", cause);
    }

    /**
     * 声明回调函数来接收RabbitMQ服务器返回的确认信息
     * 确认消息是否发送到队列
     */
    @Override
    public void returnedMessage(@NotNull ReturnedMessage returnedMessage) {
        // 发送到队列失败时才调用这个方法
        log.debug("发送队列失败 消息主体:{}", new String(returnedMessage.getMessage().getBody()));
        log.debug("发送队列失败 应答码:{}", returnedMessage.getReplyCode());
        log.debug("发送队列失败 描述:{}", returnedMessage.getReplyText());
        log.debug("发送队列失败 消息使用的交换机 exchange:{}", returnedMessage.getExchange());
        log.debug("发送队列失败 消息使用的路由键 routing key:{}", returnedMessage.getRoutingKey());
    }

    // ========================= 以下为新增部分:设置超时、容量限制、死信队列 ========================= //
    @Bean
    public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory) {
        RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory);
        rabbitAdmin.setAutoStartup(true); // ✅ 应用启动时自动声明交换机、队列
        return rabbitAdmin;
    }

    /**
     * 声明普通交换机
     */
    @Bean
    public DirectExchange normalExchange() {
        return new DirectExchange(NORMAL_EXCHANGE);
    }

    /**
     * 声明死信交换机
     */
    @Bean
    public DirectExchange deadExchange() {
        return new DirectExchange(DEAD_EXCHANGE);
    }

    /**
     * 声明普通队列,设置:
     * 1. 消息TTL超时时间(例如10秒)
     * 2. 最大队列长度(超过自动丢弃最早的消息或转入死信)
     * 3. 死信交换机绑定,用于接收过期或失败消息
     */
    @Bean
    public Queue normalQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-message-ttl", 10000); // 消息10秒未被消费则过期
        args.put("x-max-length", 100); // 队列最多容纳100条消息
        args.put("x-dead-letter-exchange", DEAD_EXCHANGE); // 设置死信交换机
        args.put("x-dead-letter-routing-key", "dead.key"); // 设置死信路由键
        return new Queue(NORMAL_QUEUE, true, false, false, args);
    }

    /**
     * 声明死信队列,用于接收过期、被拒绝或队列满的消息
     */
    @Bean
    public Queue deadQueue() {
        return new Queue(DEAD_QUEUE);
    }

    /**
     * 普通队列绑定普通交换机
     */
    @Bean
    public Binding normalBinding() {
        return BindingBuilder.bind(normalQueue()).to(normalExchange()).with(ROUTING_KEY);
    }

    /**
     * 死信队列绑定死信交换机
     */
    @Bean
    public Binding deadBinding() {
        return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead.key");
    }
}

3.2 消费端拒绝消息

3.2.1 配置手动ACK

spring:

rabbitmq:

listener:

simple:

acknowledge-mode: manual # 开启手动确认模式

也可以直接在方法上进行注解

@RabbitListener(queues = {QUEUE_NAME}, ackMode = "MANUAL")

3.2.2 手动拒绝

操作场景 处理方式 RabbitMQ行为
业务正常完成 basicAck() 消息被确认并移除
业务异常但希望重试 basicReject(deliveryTag, true) 消息重新入队等待下一次消费
业务异常且不希望重试 basicReject(deliveryTag, false) 消息拒绝并转入死信队列
批量拒绝 basicNack(deliveryTag, true, false) 多条消息批量拒绝并进入死信队列

在手动模式下,开发者可根据业务处理结果显式调用:

  • channel.basicAck():确认消息已成功消费;

  • channel.basicReject()channel.basicNack():拒绝消费该消息。

方法名 是否支持批量 是否能重新入队 常见用途
basicReject(long deliveryTag, boolean requeue) ❌ 否 ✅ 可控制 单条消息拒绝处理
basicNack(long deliveryTag, boolean multiple, boolean requeue) ✅ 可 ✅ 可控制 批量拒绝消息
java 复制代码
package com.wulang.device.rabbitmq.listener;

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@Slf4j
public class TCPMessageListener {
    public static final String QUEUE_NAME = "tcp.data.queue";
    @RabbitListener(queues = {QUEUE_NAME}, ackMode = "MANUAL")
    public void processMessage(String dataString, Message message, Channel channel) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            log.debug("消费端接收到消息: {}", dataString);

            // ======== 模拟业务处理 ========
//            Integer a = 1/0;

            // 业务处理成功 -> 手动确认
            channel.basicAck(deliveryTag, false); //告诉队列可以删除 true 确认当前 deliveryTag 之前的所有未确认消息 false:只确认当前这一条消息(最常用)
            log.debug("消息处理成功,已ACK");

        } catch (Exception e) {
            log.error("消息处理异常:{}", e.getMessage(), e);
            // 拒绝并不重新入队(false),进入死信队列
            channel.basicReject(deliveryTag, false); //true:重新入队,false:丢弃或进入死信队列
            log.warn("消息已拒绝并进入死信队列");
        }
    }
}
相关推荐
7***684312 分钟前
Spring Boot 热部署
java·spring boot·后端
k***459913 分钟前
Spring Boot实时推送技术详解:三个经典案例
spring boot·后端·状态模式
n***632732 分钟前
DeepSeek API 调用 - Spring Boot 实现
windows·spring boot·后端
C***u1761 小时前
【springboot】Spring 官方抛弃了 Java 8!新idea如何创建java8项目
java·spring boot·spring
S***q3772 小时前
Java进阶-在Ubuntu上部署SpringBoot应用
java·spring boot·ubuntu
棋啊_Rachel2 小时前
Spring Boot深度解析:从零开始构建企业级应用
java·spring boot·后端
苏州知芯传感2 小时前
环境智能的触角:MEMS微振镜赋能分布式光纤传感网络
网络·分布式·3d·mems·激光·微振镜
草原印象2 小时前
Spring Boot Spring MVC MyBatis MyBatis Plus框架编写项目实战案例
spring boot·spring·mybatis·springmvc·mybatisplus
p***s912 小时前
Spring Boot 整合 Redis 步骤详解
spring boot·redis·bootstrap