RabbitMQ:交换机、消息确认与生产问题

RabbitMQ:交换机、消息确认、死信队列与生产问题

一、RabbitMQ 整体架构

1.1 核心概念


Producer(生产者)-> 发送消息 -> Exchange(交换机)-> 路由规则 -> Queue(队列)-> Consumer(消费者)

Key 组件:
  Exchange:接收消息,按规则路由到队列
  Binding:交换机和队列之间的绑定规则
  RoutingKey:路由键,决定消息去哪
  VirtualHost:虚拟主机,隔离不同环境的队列(dev/test/prod)

1.2 为什么用 MQ

| 场景 | 说明 | 示例 |

|------|------|------|

| 异步解耦 | 生产者不需要等消费者处理完 | 下单后发短信,不用等短信发完 |

| 流量削峰 | 缓冲瞬时高并发 | 秒杀场景把请求先塞入 MQ |

| 数据同步 | 变更事件广播给多个消费者 | 订单状态变更通知仓储/财务/物流 |

二、交换机类型(重点)

2.1 四种交换机对比

| 类型 | 路由规则 | 适用场景 | 示例 |

|------|----------|----------|------|

| Direct | RoutingKey 精确匹配 | 单播、点对点 | 日志级别 routing: error -> error-queue |

| Fanout | 广播,忽略 RoutingKey | 一个消息多个消费者 | 订单创建后同时通知短信/邮件/积分 |

| Topic | RoutingKey 模式匹配(* 和 #) | 灵活路由 | user.# 匹配 user.create、user.delete |

| Headers | 根据消息 Header 匹配 | 少用 | 根据自定义头字段路由 |

2.2 Direct Exchange(直连交换机)


Producer -> Exchange(direct) + routingKey="order.create"
  -> Binding: queue_order 绑定 routingKey="order.create"
  -> 消息只到 queue_order

// 声明交换机
@Bean
public DirectExchange directExchange() {
    return new DirectExchange("order.direct");
}

// 声明队列
@Bean
public Queue orderQueue() {
    return new Queue("queue.order");
}

// 绑定:routingKey = "order.create"
@Bean
public Binding orderBinding(Queue orderQueue, DirectExchange directExchange) {
    return BindingBuilder.bind(orderQueue)
        .to(directExchange)
        .with("order.create");
}

2.3 Fanout Exchange(扇出交换机)


Producer -> Exchange(fanout)
  -> Binding: queue_sms, queue_email, queue_points 全部绑定
  -> 消息同时到三个队列(广播)

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

// 不需要 routingKey,绑定的队列都会收到
@Bean
public Binding smsBinding(Queue smsQueue, FanoutExchange fanoutExchange) {
    return BindingBuilder.bind(smsQueue).to(fanoutExchange);
}

2.4 Topic Exchange(主题交换机)

通配符规则

  • *:匹配一个单词(以 . 分隔)
  • #:匹配零个或多个单词

routingKey 格式:模块.操作.类型

绑定规则:
  user.#    -> 匹配 user.create、user.delete、user.create.vip
  user.*    -> 匹配 user.create,但不匹配 user.create.vip
  *.create  -> 匹配 user.create、order.create
  #         -> 匹配所有

示例:
  routingKey = "order.create.success"
  绑定了 "order.#" 的队列 -> 收到
  绑定了 "order.create.*" 的队列 -> 收到
  绑定了 "*.create" 的队列 -> 收不到(* 只匹配一个词)

@Bean
public TopicExchange topicExchange() {
    return new TopicExchange("order.topic");
}

@Bean
public Binding topicBinding1(Queue queue1, TopicExchange topicExchange) {
    return BindingBuilder.bind(queue1).to(topicExchange).with("order.#");
}

@Bean
public Binding topicBinding2(Queue queue2, TopicExchange topicExchange) {
    return BindingBuilder.bind(queue2).to(topicExchange).with("*.success");
}

三、消息确认机制

3.1 生产者确认(Publisher Confirm)

问题:生产者把消息发出去后,怎么确认 MQ 收到了?


流程:
  Producer -> 发送消息 -> RabbitMQ 收到 -> 返回 ACK(成功)或 NACK(失败)

// 配置开启
spring.rabbitmq.publisher-confirm-type=correlated

// 回调处理
@Configuration
public class RabbitConfig implements RabbitTemplate.ConfirmCallback {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @PostConstruct
    public void init() {
        rabbitTemplate.setConfirmCallback(this);
    }
    
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (ack) {
            log.info("消息发送成功, id={}", correlationData.getId());
        } else {
            log.error("消息发送失败, id={}, cause={}", correlationData.getId(), cause);
            // 重试或记录数据库
        }
    }
}

3.2 生产者 Return 回调(消息未路由到队列)

问题:消息到了 Exchange,但没有匹配的队列(routingKey 不匹配),消息会丢失。


// 配置
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true

// 回调
@Configuration
public class RabbitConfig implements RabbitTemplate.ReturnCallback {
    
    @PostConstruct
    public void init() {
        rabbitTemplate.setReturnCallback(this);
    }
    
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText,
                                 String exchange, String routingKey) {
        log.error("消息未路由到队列: exchange={}, routingKey={}, body={}",
            exchange, routingKey, new String(message.getBody()));
        // 记录日志/告警/重试
    }
}

3.3 消费者确认(Manual Ack)

三种确认模式

| 模式 | 说明 | 风险 |

|------|------|------|

| auto(自动确认) | 消息一到消费者就自动 ACK | 消费者处理失败消息会丢失 |

| manual(手动确认) | 消费者处理完后手动 ACK | 安全,推荐生产使用 |

| none | 不确认 | 不用 |


// 配置开启手动确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual

// 消费者手动 ACK
@RabbitListener(queues = "queue.order")
public void handleOrder(Message message, Channel channel) throws IOException {
    try {
        String body = new String(message.getBody());
        // 处理业务
        processOrder(body);
        
        // 业务成功 -> 手动 ACK
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        
    } catch (Exception e) {
        log.error("消息处理失败", e);
        // 业务失败 -> NACK,requeue=false 不重回队列(走死信)
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
    }
}

basicReject vs basicNack

  • basicReject:只能拒绝单条消息
  • basicNack:可以批量拒绝(multiple=true)

3.4 完整的消息可靠性保障链路


Producer:
  1. Publisher Confirm 确认消息到达 Exchange
  2. Return Callback 确认消息路由到 Queue
  3. 消息落数据库(消息表),定时任务补偿

Broker:
  1. 队列持久化(durable=true)
  2. 消息持久化(deliveryMode=2)

Consumer:
  1. 手动 ACK
  2. 处理失败走死信队列
  3. 幂等消费(防重复)

四、死信队列(DLQ)

4.1 消息什么时候变成死信

| 条件 | 说明 |

|------|------|

| 消息被拒绝(basicNack/basicReject)且 requeue=false | 消费者处理失败 |

| 消息过期(TTL) | 设置了过期时间但没被消费 |

| 队列满了 | 队列达到最大长度 |

4.2 死信队列配置


正常队列 -> 消息变成死信 -> 转发到死信交换机 -> 路由到死信队列 -> 死信消费者处理

// 1. 声明死信交换机和死信队列
@Bean
public DirectExchange dlxExchange() {
    return new DirectExchange("dlx.exchange");
}

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

@Bean
public Binding dlxBinding(Queue dlxQueue, DirectExchange dlxExchange) {
    return BindingBuilder.bind(dlxQueue).to(dlxExchange).with("dlx");
}

// 2. 正常队列绑定死信交换机
@Bean
public Queue normalQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-dead-letter-exchange", "dlx.exchange");  // 死信交换机
    args.put("x-dead-letter-routing-key", "dlx");        // 死信 routingKey
    return new Queue("queue.normal", true, false, false, args);
}

死信队列的用途

  • 记录失败消息,后续人工排查
  • 配合定时任务重试(如延迟 5 分钟后重新发送)
  • 告警通知(死信数量超过阈值发邮件/短信)

五、延迟队列

5.1 场景

| 场景 | 说明 |

|------|------|

| 订单超时取消 | 下单 30 分钟后如果未支付自动取消 |

| 延迟重试 | 消息处理失败,延迟 5 分钟重试 |

| 定时任务 | 每天固定时间执行 |

5.2 实现方式一:TTL + 死信队列


消息设置 TTL -> 正常队列(不消费)-> TTL 到期 -> 死信交换机 -> 死信队列(消费者处理)

// 延迟队列(绑死信交换机,但正常消费者不消费,等 TTL 后转死信)
@Bean
public Queue delayQueue() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-dead-letter-exchange", "dlx.exchange");
    args.put("x-dead-letter-routing-key", "dlx");
    args.put("x-message-ttl", 30 * 60 * 1000);  // 30 分钟过期
    return new Queue("queue.delay", true, false, false, args);
}

问题:同一个队列只能设置一个 TTL,不同延迟时间需要不同队列。

5.3 实现方式二:延迟交换机插件(推荐)


// 安装 rabbitmq-delayed-message-exchange 插件后
@Bean
public CustomExchange delayedExchange() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-delayed-type", "direct");
    return new CustomExchange("delayed.exchange", "x-delayed-message", true, false, args);
}

// 发送延迟消息
rabbitTemplate.convertAndSend("delayed.exchange", "routing.key", message, msg -> {
    msg.getMessageProperties().setDelay(30 * 60 * 1000);  // 延迟 30 分钟
    return msg;
});

六、生产三大问题

6.1 消息丢失

可能丢失的环节

| 环节 | 原因 | 解决方案 |

|------|------|----------|

| 生产者 -> MQ | 网络异常、MQ 宕机 | Publisher Confirm + 消息落库 |

| MQ 内部 | 队列未持久化、MQ 重启 | 队列持久化 + 消息持久化 + 集群 |

| MQ -> 消费者 | 自动 ACK 后消费者宕机 | 手动 ACK |

完整保障


1. 生产者:
   - 开启 Publisher Confirm
   - 消息写本地消息表,定时任务扫描未确认消息重发

2. MQ Broker:
   - 队列 durable=true
   - 消息 deliveryMode=2(持久化)
   - 集群部署 + 镜像队列

3. 消费者:
   - 手动 ACK
   - 先处理业务,再 ACK
   - 失败走死信队列

6.2 消息重复

原因

  • 生产者重发(网络超时导致 Producer 以为没发送成功)
  • 消费者 ACK 丢失(MQ 没收到 ACK,重新投递)

解决方案:消费者幂等


// 方式一:消息 ID 去重(Redis)
@RabbitListener(queues = "queue.order")
public void handleOrder(Message message, Channel channel) throws IOException {
    String msgId = message.getMessageProperties().getMessageId();
    String key = "mq:dedup:" + msgId;
    
    Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", 24, TimeUnit.HOURS);
    if (Boolean.FALSE.equals(success)) {
        log.info("重复消息,跳过, msgId={}", msgId);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        return;
    }
    
    try {
        processOrder(message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    } catch (Exception e) {
        channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        throw e;
    }
}

// 方式二:业务唯一键去重(数据库唯一索引)
// 比如订单号作为唯一索引,重复插入会失败

6.3 消息积压

现象:队列中消息越来越多,消费者处理不过来。

排查思路


1. 看消费者是否在线(可能挂了)
2. 看消费者处理速度(是不是变慢了)
3. 查消费者日志(是不是有异常一直在重试)
4. 查下游依赖(DB 慢、下游服务不可用)

应急处理


方案一:临时扩容消费者
  启动更多消费者实例,加速消费

方案二:紧急降级
  消费者只做核心逻辑,非核心操作(日志、通知)先跳过

方案三:消息丢弃(极端情况)
  如果积压消息已经没用了,可以清空队列

方案四:转移积压消息
  新建一个消费者,把积压消息转到新队列,慢慢处理

预防

  • 监控队列长度(超过阈值告警)
  • 消费者设置合理的并发数(spring.rabbitmq.listener.simple.concurrency
  • 消费者处理逻辑尽量异步化
posted @ 2026-05-07 16:56  xzlrf  阅读(23)  评论(0)    收藏  举报