RabbitMQ -消息可靠 的实战示例

一、核心设计思路

先明确 RabbitMQ 保证消息安全的核心链路:

二、环境准备

1. 依赖引入(pom.xml)

java 复制代码
<dependencies>
    <!-- Spring Boot 基础 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- RabbitMQ 核心依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <!-- 工具类(JSON、UUID) -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.32</version>
    </dependency>
    <!-- 测试依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

2. 配置文件(application.yml)

java 复制代码
spring:
  # RabbitMQ 基础配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    # 连接池配置
    connection-timeout: 10000
    # 发送确认配置
    publisher-confirm-type: correlated # 开启Confirm确认(异步回调)
    publisher-returns: true # 开启Return回退(消息到交换机但未到队列时回调)
    template:
      mandatory: true # 强制触发Return回调
    # 消费端配置
    listener:
      simple:
        acknowledge-mode: manual # 手动ACK(关键:禁用自动确认)
        concurrency: 1 # 最小消费线程数
        max-concurrency: 5 # 最大消费线程数
        retry:
          enabled: true # 开启本地重试
          max-attempts: 3 # 本地最大重试次数(1次正常消费+3次重试)
          initial-interval: 1000ms # 首次重试间隔
          multiplier: 2 # 重试间隔倍数(1s→2s→4s)
          max-interval: 5000ms # 最大重试间隔
        default-requeue-rejected: false # 消费失败不重新入队(避免无限循环)

三、核心代码实现

1. 队列 / 交换机 / 死信配置(RabbitMQConfig.java)

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

@Configuration
public class RabbitMQConfig {
    // 核心队列/交换机名称
    public static final String BUSINESS_QUEUE = "business.queue";
    public static final String BUSINESS_EXCHANGE = "business.exchange";
    public static final String BUSINESS_ROUTING_KEY = "business.key";
    
    // 死信相关名称
    public static final String DEAD_LETTER_QUEUE = "dead.letter.queue";
    public static final String DEAD_LETTER_EXCHANGE = "dead.letter.exchange";
    public static final String DEAD_LETTER_ROUTING_KEY = "dead.letter.key";

    // 1. 声明死信交换机
    @Bean
    public DirectExchange deadLetterExchange() {
        return new DirectExchange(DEAD_LETTER_EXCHANGE, true, false);
    }

    // 2. 声明死信队列
    @Bean
    public Queue deadLetterQueue() {
        return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
    }

    // 3. 绑定死信队列到死信交换机
    @Bean
    public Binding deadLetterBinding() {
        return BindingBuilder.bind(deadLetterQueue())
                .to(deadLetterExchange())
                .with(DEAD_LETTER_ROUTING_KEY);
    }

    // 4. 声明业务队列(绑定死信交换机)
    @Bean
    public Queue businessQueue() {
        Map<String, Object> args = new HashMap<>();
        // 绑定死信交换机
        args.put("x-dead-letter-exchange", DEAD_LETTER_EXCHANGE);
        // 绑定死信路由键
        args.put("x-dead-letter-routing-key", DEAD_LETTER_ROUTING_KEY);
        // 队列消息过期时间(可选,此处不设置,由消费失败触发死信)
        // args.put("x-message-ttl", 60000);
        return QueueBuilder.durable(BUSINESS_QUEUE).withArguments(args).build();
    }

    // 5. 声明业务交换机
    @Bean
    public DirectExchange businessExchange() {
        return new DirectExchange(BUSINESS_EXCHANGE, true, false);
    }

    // 6. 绑定业务队列到业务交换机
    @Bean
    public Binding businessBinding() {
        return BindingBuilder.bind(businessQueue())
                .to(businessExchange())
                .with(BUSINESS_ROUTING_KEY);
    }
}

2. 生产者(消息发送 + 确认回调)(ProducerService.java)

java 复制代码
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.UUID;

@Service
public class ProducerService {
    private static final Logger log = LoggerFactory.getLogger(ProducerService.class);

    @Resource
    private RabbitTemplate rabbitTemplate;

    // 初始化:设置发送确认和回退回调
    @PostConstruct
    public void initRabbitTemplate() {
        // 1. Confirm回调:消息到达交换机触发(无论成功/失败)
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            String msgId = correlationData.getId();
            if (ack) {
                log.info("消息[{}]成功发送到交换机", msgId);
            } else {
                log.error("消息[{}]发送到交换机失败,原因:{}", msgId, cause);
                // 此处可添加失败重试/告警/入库补偿逻辑
            }
        });

        // 2. Return回调:消息到达交换机但未路由到队列触发
        rabbitTemplate.setReturnsCallback(returned -> {
            String msgId = returned.getMessage().getMessageProperties().getMessageId();
            log.error("消息[{}]路由到队列失败,路由键:{},回复码:{},原因:{}",
                    msgId, returned.getRoutingKey(), returned.getReplyCode(), returned.getReplyText());
            // 此处可添加重发/入库补偿逻辑
        });
    }

    /**
     * 发送业务消息
     * @param data 业务数据(示例:订单ID)
     */
    public void sendBusinessMessage(String data) {
        // 1. 生成唯一消息ID(用于幂等/追踪)
        String msgId = UUID.randomUUID().toString();
        // 2. 构建消息(转JSON)
        String msg = JSON.toJSONString(data);
        // 3. 构建CorrelationData(用于Confirm回调关联消息)
        CorrelationData correlationData = new CorrelationData(msgId);

        // 4. 发送消息
        rabbitTemplate.convertAndSend(
                RabbitMQConfig.BUSINESS_EXCHANGE,
                RabbitMQConfig.BUSINESS_ROUTING_KEY,
                msg,
                message -> {
                    // 设置消息ID(用于消费端幂等)
                    message.getMessageProperties().setMessageId(msgId);
                    return message;
                },
                correlationData
        );
        log.info("消息[{}]已发送(待确认),内容:{}", msgId, data);
    }
}

3. 消费者(手动 ACK + 重试 + 死信)(ConsumerService.java)

java 复制代码
import com.alibaba.fastjson.JSON;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

import java.io.IOException;

@Service
public class ConsumerService {
    private static final Logger log = LoggerFactory.getLogger(ConsumerService.class);

    /**
     * 消费业务消息(手动ACK+本地重试)
     */
    @RabbitListener(queues = RabbitMQConfig.BUSINESS_QUEUE)
    public void consumeBusinessMessage(Message message, Channel channel) throws IOException {
        String msgId = message.getMessageProperties().getMessageId();
        String msgContent = new String(message.getBody());
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            log.info("开始消费消息[{}],内容:{}", msgId, msgContent);
            
            // 模拟业务处理(此处可替换为真实业务逻辑)
            String orderId = JSON.parseObject(msgContent).toString();
            if ("error_order".equals(orderId)) {
                // 模拟业务处理失败(触发重试)
                throw new RuntimeException("订单处理失败:库存不足");
            }

            // 1. 业务处理成功:手动确认ACK(关键:仅成功时确认)
            channel.basicAck(deliveryTag, false);
            log.info("消息[{}]消费成功,已确认ACK", msgId);

        } catch (Exception e) {
            log.error("消息[{}]消费失败,准备投递死信队列,原因:{}", msgId, e.getMessage());
            // 2. 业务处理失败:拒绝消息并投递到死信队列(本地重试3次后执行此逻辑)
            // basicNack参数说明:
            // - deliveryTag:消息唯一标识
            // - multiple:是否批量拒绝
            // - requeue:是否重新入队(false=直接投递死信)
            channel.basicNack(deliveryTag, false, false);
        }
    }

    /**
     * 消费死信消息(归档/告警)
     */
    @RabbitListener(queues = RabbitMQConfig.DEAD_LETTER_QUEUE)
    public void consumeDeadLetterMessage(Message message, Channel channel) throws IOException {
        String msgId = message.getMessageProperties().getMessageId();
        String msgContent = new String(message.getBody());
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            log.warn("处理死信消息[{}],内容:{},开始归档/告警", msgId, msgContent);
            // 此处添加死信处理逻辑:入库归档、发送告警、人工介入等
            // 示例:插入死信日志表
            // deadLetterMapper.insert(msgId, msgContent, "消费重试3次失败");

            // 确认死信消费成功
            channel.basicAck(deliveryTag, false);
            log.info("死信消息[{}]归档成功", msgId);
        } catch (Exception e) {
            log.error("死信消息[{}]处理失败", msgId, e);
            // 死信处理失败:直接拒绝(避免死循环)
            channel.basicNack(deliveryTag, false, false);
        }
    }
}

4. 测试类(TestRabbitMQ.java)

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
public class TestRabbitMQ {
    @Resource
    private ProducerService producerService;

    // 测试正常消息发送
    @Test
    public void testSendNormalMessage() {
        producerService.sendBusinessMessage("normal_order_123456");
        // 休眠3秒,等待消费完成
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 测试失败消息(触发重试+死信)
    @Test
    public void testSendErrorMessage() {
        producerService.sendBusinessMessage("error_order");
        // 休眠10秒,等待重试+死信处理完成
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


相关推荐
Wang's Blog2 小时前
RabbitMQ:高效消息处理与资源管理实践
分布式·rabbitmq
脸大是真的好~16 小时前
分布式锁-基于redis实现分布式锁(不推荐)- 改进利用LUA脚本(不推荐)前面都是原理 - Redisson分布式锁
redis·分布式·lua
liuniansilence16 小时前
🚀 高并发场景下的救星:BullMQ如何实现智能流量削峰填谷
前端·分布式·消息队列
Wang's Blog20 小时前
RabbitMQ: 实现高效消息监听之从基础到自动配置
分布式·rabbitmq
Wang's Blog20 小时前
RabbitMQ: 高级特性详解之消息返回机制与消费端确认机制
分布式·rabbitmq
Wang's Blog21 小时前
RabbitMQ: 使用MessageConverter高效处理消息
分布式·rabbitmq
‘胶己人’1 天前
redis分布式锁
数据库·redis·分布式
山沐与山1 天前
【MQ】Kafka与RocketMQ深度对比
分布式·kafka·rocketmq