RabbitMQ

一、特点

可靠性

多种机制,允许在性能和可靠性之间进行权衡
例:消息持久化,开启持久化会将消息写入磁盘,而磁盘速度远慢于内存。这会导致消息吞吐量下降,性能受损。

灵活的路由

多种交换机类型,不同的路由匹配规则。
实现自己的交换机类型当作插件使用。

多协议,插件支持

STOMP 文本协议(类似HTTP)
常用,命令和头部是文本,消息体可以是二进制或文本,解析成本较高。
MQTT 二进制协议
适用于带宽低、延迟高、网络不稳定、电力/计算资源受限的远程设备(物联网传感器、移动设备)。
RabbitMQ Streams 二进制协议
高吞吐量、低延迟的流式数据,适用于处理日志、遥测数据等大规模实时数据流场景。
HTTP/WebSocket
允许浏览器等客户端通过 WebSocket 传输 STOMP 或 MQTT 等协议
AMQP 0-9-1
定义了强大的消息语义(如 Exchange、Queue、Binding)
AMQP 1.0
国际标准,适用于不同消息中间件之间互相操作

可视化工具

部署mq后,打开服务器ip+配置的端口号,就是可视化面板

mq集群

联合模型

二、作用

解耦
两个功能的通信通过mq传递,当两个功能不再关联,不需要删除代码
异步
用户支付成功后需要加积分、赠送权益、发送微信通知,正常流程需要三者顺序进行,消耗三个功能的响应时间,使用mq同时进行,只消耗一个功能的时间。
削峰
一段时间内请求暴增,所有sql都挤进mysql执行,mysql处理不过来就会崩溃,进而导致系统瘫痪。
mq短时间能积压一部分数据,Qos限流机制给消费者消费,
12306点击购买后提示“购票流程已进入队列”,mq就起到削峰的作用。

三、简单原理

消息流程

生产者连接到MQ的Broker,创建Connection,开启channel;生产者发送消息并指定消息的RoutingKey,Exchange接收到消息根据Binding的路由规则转发给Queue,若Queue内有消息Broker会根据设定Push给监听的消费者

组成

Virtual Host
虚拟容器,出于多用户和安全设计,划分出多个vhost,如果有多个用户连接mq,每个用户创建独自的exchange/queue等,起到隔离的作用
Broker
rabbitmq的服务器
Exchange
direct 直连/精确模式:RoutingKey与BindingKey完全匹配
topic 主题/模糊模式:模糊匹配,通配符符合就放入该队列,交给Costumer消费者消费
fanout 订阅发布/广播模式:忽略RoutingKey,进入Exchange绑定的所有Queue
Headers:忽略RoutingKey,根据消息Heade属性匹配,极少使用
Quene
Costumer
Pull:消费者定时去队列中拿,有延迟性
Push:队列有消息,Broker就推送给消费者

实现流程

定义名称并配置交换机new TopicExchange和队列new Queue,BindingBuilder.bind绑定队列到交换机,rabbitAdmin初始化所有exchange和queue,生产方rabbitTemplate.convertAndSend指定exchange和RoutingKey,消费方@RabbitListener指定监听Queue名

四、机制

消息持久化

交换机和队列持久化默认是true可以不设置
持久化交换机:声明交换机时设置durable=true
持久化队列:声明队列时设置durable=true
发送持久化消息:发送消息时,将delivery_mode属性设置为2

消息应答

自动应答:队列将消息发给消费者后立即删除,不考虑消费者是否真正处理成功。
简单但不可靠,适合允许丢消息、日志收集等场景

// autoAck = true,消费后自动确认
channel.basicConsume("queue_name", true, (consumerTag, delivery) -> {
    String message = new String(delivery.getBody());
    System.out.println("收到消息: " + message);
    // 方法执行完,RabbitMQ 自动认为消息已处理
}, consumerTag -> {});

手动应答:可靠但需要手动处理
三种应答方法
basicAck 确认成功 deliveryTag:消息标签 multiple:是否批量确认
basicNack 拒绝(可批量) deliveryTag:消息标签 multiple:是否批量 requeue:是否重新入队
basicReject 拒绝(单条) deliveryTag:消息标签 requeue:是否重新入队
使用手动应答时,如果忘记调用basicAck,消息会一直处于Unacked状态,积压过多会导致RabbitMQ内存爆满。

// autoAck = false,需要手动确认
channel.basicConsume("queue_name", false, (consumerTag, delivery) -> {
    try {
        String message = new String(delivery.getBody());
        System.out.println("处理消息: " + message);

        // 业务处理成功,手动确认
        channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
        //                                          multiple = false ↑ 只确认当前这条

    } catch (Exception e) {
        // 处理失败,拒绝并重新入队
        channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true);
        //                                                    multiple  requeue ↑
    }
}, consumerTag -> {});

消息分发机制

任务分配(Work Queue):多个消费者监听同一个队列
轮询分发:RabbitMQ默认,不考虑消费速度,按顺序平均分配。可能导致忙的忙死,闲的闲死。
公平分发:能者多劳,消费者处理并确认完一条后,才会收到下一条,处理快的能分到更多消息。通过配置 channel.basicQos(1) 开启,必须配合手动应答(autoAck=false)。

消息路由:Exchange四种路由方式

发布确认机制

confirm机制

生产者发送到Exchange的消息,如果顺利抵达会触发Exchange的confirm()回调。
Exchange的消息如果没有匹配到绑定的Queue会回调returnedMessage(),能路由到无回调。

ACK事务机制

application.yml中配置rabbitmq:
publisher-confirms: correlated
publisher-returns: true
template:
mandatory: true

死信队列

死信:无法被消费的消息
消费者处理异常且没有后续处理的消息会进入死信队列,成为死信 → 死信交换机 → 死信队列 → 监控/处理
死信的来源:
1.消息被拒绝(basic.reject/basic.nack),且 requeue = false(代表不重新回到队列)
2.消息超过TTL过期
3.积压的消息超过队列最大长度,先入队的消息会被丢弃变为死信

@Configuration
public class DeadLetterConfig {

    // 死信交换机
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("dlx_exchange");
    }

    // 死信队列
    @Bean
    public Queue dlxQueue() {
        return new Queue("dlx_queue");
    }

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dead_routing_key");
    }

    // 声明正常队列时绑定死信交换机
    @Bean
    public Queue normalQueue() {
        return QueueBuilder.durable("normal_queue")
                .deadLetterExchange("dlx_exchange")
                .deadLetterRoutingKey("dead_routing_key")
                .ttl(30000)                    // 消息 TTL
                .maxLength(1000)               // 队列最大长度
                .build();
    }
}

延迟队列

延迟队列:到达设置的延迟时间再推给消费者处理
利用 TTL + 死信实现延迟消费的核心思路:消息先在一个没有消费者的队列中过期,过期后自动进入死信队列,再由真正的消费者从死信队列消费,从而实现延迟执行的效果。
如果一条消息设置了TTL或者进入了设置TTL属性的队列,这条消息在TTL内没有被消费会成为死信。如果同时配置了队列的TTL和消息的TTL,使用较小的值。在队列上设置TTL,消息一过期就被队列丢弃,针对每条消息设置TTL,消息是否过期是在投递到消费者前判定的,积压在队列中的消息即使过期还能存活一阵。

@Configuration
public class DelayQueueSpringConfig {

    @Bean
    public DirectExchange delayExchange() {
        return new DirectExchange("order.delay.exchange");
    }

    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange("order.dlx.exchange");
    }

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

    @Bean
    public Binding dlxBinding() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("order.timeout");
    }

    @Bean
    public Queue delayQueue() {
        return QueueBuilder.durable("order.delay.queue")
                .deadLetterExchange("order.dlx.exchange")
                .deadLetterRoutingKey("order.timeout")
                .ttl(30 * 60 * 1000)  // 30分钟
                .build();
    }

    @Bean
    public Binding delayBinding() {
        return BindingBuilder.bind(delayQueue()).to(delayExchange()).with("order.delay");
    }
}

发送消息

@Service
public class DelayMessageService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    public void sendDelayMessage(String orderId) {
        rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", orderId);
    }
}

消费消息

@Component
public class TimeoutOrderListener {

    @RabbitListener(queues = "order.dlx.queue")
    public void processTimeoutOrder(String orderId) {
        System.out.println("订单超时检查: " + orderId);
        // 检查并处理...
    }
}

四、实践问题

消息丢失

消息从生产者到消费者,经过两次网络传输,所以以下三种情况会出现消息丢失
1、生产者向mq服务器(Broker)发送消息,mq服务器宕机或重启会丢失数据
解决方法:confirm机制
2、消息存储在队列中,如果队列没有对消息持久化,mq服务器宕机或重启会丢失数据
解决方法:消息持久化
3、消费者从MQ服务器获取队列中存储的数据消费,消费者程序出错或者宕机,消息没有被成功处理就从队列删除,数据丢失。
解决方法:ACK事务机制

消息积压

队列长度限制
队列溢出,造成内存泄漏,当前进程会被杀死

消息处理失败

消费失败的消息进入死信队列,被存入数据库,人为干预

channel.basicConsume("dlx_queue", false, (consumerTag, delivery) -> {
    String deadMsg = new String(delivery.getBody());
    Map<String, Object> headers = delivery.getProperties().getHeaders();

    // 获取死信原因信息(RabbitMQ 自动添加的头信息)
    String reason = (String) headers.get("x-death");
    Long originalExpiration = (Long) headers.get("x-first-death-exchange");

    System.err.println("收到死信消息: " + deadMsg);
    System.err.println("死信原因: " + reason);

    // 发送警告/存入数据库待人工处理
    sendAlert(deadMsg);

    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}, consumerTag -> {});

参考:
https://rabbitmq.mr-ping.com/
https://developer.aliyun.com/article/769883
https://developer.aliyun.com/article/769882
https://www.cnblogs.com/antLaddie/p/15958830.html
https://www.cnblogs.com/antLaddie/p/17318213.html

posted @ 2026-04-23 16:50  4加1等于9  阅读(22)  评论(0)    收藏  举报