rabbitmq之高级特性

本文详细解析了消息队列中的消息可靠性投递策略,包括消息落库、延时投递、确认机制及消费端限流等。探讨了如何通过多种机制保障消息的正确性和处理效率,避免重复消费与数据丢失。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.消息的可靠性投递(生产者)

什么是生产端消息的可靠性投递

①:保障消息成功发送出去
②:保障mq节点成功接收消息
③:消息发送端需要收到mq服务的确认应答
④:完善的消息补偿机制

保障消息可靠性投递的方案1(消息落库)

消息入库打标解决思路 (Order_Server调用物流服务举列子)
在消息生产者端(也就是订单服务)
正常链路流程:
第一步(该环节调用操作了二次数据库):在创建订单的操作的时候,把数据插入到订单相关的表中,并且构造调
用物流模块的数据消息,把消息插入到消息表中,初始状态为0。
第二步: 把物流消息投递到消息队列中。
第三步;消息队列访问一个确认消息,并且由,订单服务来监控mq server的确认消息。
第四步:根据收到的确认消息来更新数据库中的消息记录的状态。
异常链路流程:
第一步(该环节调用操作了二次数据库):在创建订单的操作的时候,把数据插入到订单相关的表中,并且构造调
用物流模块的数据消息,把消息插入到消息表中,初始状态为0。
第二步: 把物流消息投递到消息队列中。
第三步:由于网络闪断,导致消费端监控mq服务访问的确认消息 没有收到,那么在msg_db中的那条消息的
状态永远就是0状态。这个时候,我们需要对这种情况下做出补偿。
补偿机制:
启动一个分布式的定时任务,不定时的去扫描msg_db的这个表,状态为0的消息记录,在这里我们可以根据
业务来设置扫描重发规则规则1:插入msg_db 表中5Min后状态还是为0的记录,进行消息重试。
规则2:若重试的次数超过五次状态还是为0的话,我们就把消息状态改为2,此时我们需要人工的去确认状态
为2的消息是什么原因导致没有成功的。
消息入库打标的缺点:
在第一步的过程中,既插入了业务数据表,也同时插入了消息记录表,进行了二次db操作,在高并发的环
境下,这个环境就会造成性能瓶颈。

 

保障消息可靠性投递的方案2(延时投递,做二次确认检测,回调检测)

 

2.消息的确认(confirm)机制(生产者)

rabbitmq的消息确认(confirm)机制

1:消息的确认:指的是生产者将消息投递后,如果mq-server接受到消息,就会给生产者一个应答.
2:生产者接受到应答,来确保该条消息是否成功发送到了mq-server
3:confirm机制是消息可靠性投递的核心保障

confirm机制核心流程图:

confirm机制的实现步骤

第一步:在生产端的channel 上开启确认模式 channel.confirmSelect();
第二步:在生产端的channel上添加监听,用来监听mq-server返回的应答;
第三步:创建confirm消息监听器,实现ConfirmListener接口;

生产端代码

//生产端 开启confirm 确认机制
channel.confirmSelect();
//生产端 设置confirm 监听
channel.addConfirmListener(new SimpleConfirmListerner());

监听器代码

//实现ConfirmListener接口 实现handleAck和handleNack接口
public class AngleConfirmListerner implements ConfirmListener {
    
    /**
     *
     * @param deliveryTag 唯一消息Id
     * @param multiple:是否批量
     * @throws IOException
     */
    @Override
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息deliveryTag"+deliveryTag+"被正常签收");
    }

    /**
     * 处理异常
     * @param deliveryTag
     * @param multiple
     * @throws IOException
     */
    @Override
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("消息deliveryTag"+deliveryTag+"没被签收");
    }
}

 

3.return listener 消息处理机制(生产者)

Return Listener是用来处理一些不可路由的消息

rabbitmq消息生产者,通过把消息投递到exchange上,然后通过routingkey 把消息路由到某一个队列上,然后
消费者通过队列消息侦听,然后进行消息消费处理。
以上过程中会出现的情况:
情况一: broker中根本没有对应的exchange交换机来接受该消息;
情况二:消息能够投递到broker的交换机上,但是交换机根据routingKey 路由不到某一个队列上;

针对上述二种情况,rabbitmq提供了return listener来处理这种不可达的消息:
处理一:若在消息生产端发送消息时将参数mandatory设置为true,那么就会调用生产端ReturnListener 来处理;
处理二:若在消息生产端发送消息时将参数mandatory设置为false(默认值也是false) 那么mq-broker就会自动删除消息;

return listener消息处理机制流程:

生产者核心代码:

//channel设置监听不可达消息
channel.addReturnListener(new SimpleRetrunListener());
/**
 * 发送消息
 * mandatory:该属性设置为false,那么不可达消息就会被mq broker给删除掉;
 * 该属性设置为true,那么mq会调用retrunListener来通知业务系统该消息不能成功发送.
 */
//正确的消息发送
String msgContext = "你好 zyf...."+System.currentTimeMillis();        channel.basicPublish(exchangeName,okRoutingKey,true,basicProperties,msgContext.getBytes());
String errorMsg1 = "你好 zyf mandotory为false...."+System.currentTimeMillis();
//错误发送   mandotory为false                 channel.basicPublish(exchangeName,errorRoutingKey,false,basicProperties,errorMsg1.getBytes());
String errorMsg2 = "你好 zyf mandotory为true...."+System.currentTimeMillis();
//错误发送 mandotory 为true      channel.basicPublish(exchangeName,errorRoutingKey,true,basicProperties,errorMsg2.getBytes());

return监听器代码:

//实现ReturnListener接口 用于监听不可达的消息
public class SimpleRetrunListener implements ReturnListener {
    @Override
    public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
        System.out.println("replyCode:"+replyCode);
        System.out.println("replyText:"+replyText);
        System.out.println("exchange:"+exchange);
        System.out.println("routingKey:"+routingKey);
        System.out.println("properties:"+properties);
        System.out.println("msg body:"+new String(body));

    }
}

4.消息的幂等性(生产者和消费者)

一、什么是接口的幂等性?

接口的幂等性:简而言之,就是对接口发起的一次调用和多次调用,锁生产的结果都是一致的。
某些接口具有天然的幂等性: 比如长查询接口,不管是查询一次还是多次,返回的结果都是一致的。


若接口没有保障幂等性,那么就出现问题
案例一:比如订单提交的过程中,用户点了一次提交,但是由于网络等原因,导致后端处理延时,客户就连
续点击了多次,在没有幂等性的条件下,那么就会造成订单的重复提交。


解决方案:在保存订单的时候,根据生成的系统全局唯一ID(这里订单号+业务类型),并且把该唯一ID 调用
redis 的setnx命令保存起来,在第一次保存的时候,由于redis中没有该key,那么就会
把全局唯一ID 进行设置上,此时订单就会保存成功,。这个时候若出现前端重复点击按钮, 由于第一步已经
setnx上了 就会阻止后面的保存。
 

二、MQ 是如何解决幂等性的
发送消息流程

第一步:消息生产者向MQ服务端发送消息
第二步:MQ服务端把消息进行落地
第三步:MQ服务端向消息生产者发送ack
第四步:消息消费者消费消息
第五步:消费者向MQ服务端发送ack
第六步: MQ服务端将落地消息删除
 

消息重复发送消息原因:

为了保障消息的百分之百的投递,我们使用了消息重发,确认机制,使得消息可能被重复发送,由上图可知
道,由于网络原因,第三步的上半场ack丢失还是第五步的下半场ack丢失
都会导致消息重复发送。

消息重复发送的导致后果:

例如上半场消息生产者是用户支付模块,专门是用来给用户扣费的,而下半场的消息消费者服务是会员卡服务,
是通过接受扣费服务发送的消息来进行发卡的,由于第三步或者是第五步ack丢失,那么就会导致上游服务重复发送消息就会导致扣一次款,发多次卡

MQ服务端是如何保证幂等性的?
消息队列的服务中,对每一条消息都会生成一个全局唯一的与业务无关的ID(inner_msg_id),当mq_server接受到消息的时候,先根据inner_msg_id 是否需要重复发送,再决定消息是否落DB ,这样保证每条消息都只会落一次DB。

消费端如何来做到幂等性的?
把对每条消息做生成一个唯一性的ID 通过redis的来setnx命令来保证幂等性。


5.消费端限流量(消费者)

一:什么是消费端的限流

场景:

首先,我们迎来了订单的高峰期,在mq的broker上堆积了成千上万条消息没有处理,这个时候,我们随便打开了
消费者,那么就会瞬间有成千上万的消息推送给消费者,我们的消费者不能处理这么多消息 就会导致消费者出现巨大压力,甚至服务器崩溃。

解决方案:
rabbitmq 提供一个qos(服务质量保证),也就是在关闭了消费端的自动ack的前提下,我们可以设置阈值(出队)的消息数,消息没有被确认(手动确认),那么就不会推送消息过来;限流的级别(consumer级别或者是channel级别)。

实现的方式 :

void BasicQos(uint prefetchSize,ushort prefetchCount ,bool global)
uint prefetchSize :指定的是设定消息的大小(rabbitmq还没有该功能,所以一般是填写0表示不限制)
ushort perfetchCount :表示设置消息的阈值,每次过来几条消息(一般是填写1 一条 一条的处理消息)
bool global:表示是channel级别的还是 consumer的限制(channel的限制rabbitmq 还没有该功能)

消费端核心代码

//消费端channel使用Qos()方法
//gloabl设置为ture 那么就是channel级别的限流,若为false 就是consumer级别的限制流量
channel.basicQos(0,1,false);
//关闭自动签收
channel.basicConsume(queueName,false,new SimpleCustomConsumer(channel));

自定义消费者代码

//自定义消费者监听,集成DefaultConsumer类
public class SimpleCustomConsumer extends DefaultConsumer {
    private Channel channel;
    /**
     * Constructs a new instance and records its association to the passed-in channel.
     * @param channel the channel to which this consumer is attached
     */
    public AngleCustomConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    } 
        
    /**
     * 处理消息
     * @param consumerTag
     * @param envelope
     * @param properties
     * @param body
     * @throws IOException
     */
    public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties                 
     properties, byte[] body) throws IOException
    {
        System.out.println("自定义的消息消费端");
        System.out.println("consumerTag="+consumerTag);
        System.out.println("envelope="+envelope);
        System.out.println("properties="+properties);
        System.out.println("body="+new String(body));
        //消费端的手动签收,假如关闭手动签收,也关闭自动签收,那么消费端只会接收到一条消息
        //multiple:false标识不批量签收
        //一定要有消费者签收,如果没有如下代码,则限流模式下,仅能打印出来channel.basicQos(0, 1, false);第二参数的1条信息
        channel.basicAck(envelope.getDeliveryTag(),false);
    }
}


6.消费端的ack(消费者)

消费端的ack模式

消费端的ack类型:自动ack 和手动ack
做消息限流的时候,我们需要关闭自动ack 然后进行手动ack的确认,若我们业务出现了问题,我们就可以进行nack重回队列
当消费端进行了nack的操作的时候,我们可以通过设置来进行对消息的重回队列的操作(但是一般我们不会设置重回队列的操作)
 

手动ack 以及重回队列操作
消费端核心代码

//gloabl设置为ture 那么就是channel级别的限流,若为false 就是consumer级别的限制流量
//channel.basicQos(0,1,false);
//关闭自动签收
channel.basicConsume("test.ack.queue",false,new AngleCustomConsumer(channel));

自定义消费者consumer:

public class AngleCustomConsumer extends DefaultConsumer {
    private Channel channel;
    /**
     * Constructs a new instance and records its association to the passed-in channel.
     * @param channel the channel to which this consumer is attached
     */
    public AngleCustomConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    } 
    /**
     * 处理消息
     * @param consumerTag
     * @param envelope
     * @param properties
     * @param body
     * @throws IOException
     */
    public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties     
     properties, byte[] body) throws IOException
    {
        try {
            Thread.currentThread().sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } 
        System.out.println(properties.getHeaders());
        String num = properties.getHeaders().get("num").toString();
        if(num .equals("1")) {
            System.out.println("业务系统处理消息异常消息重新回队列"+ new String(body));
            channel.basicNack(envelope.getDeliveryTag(),false,true);
        }else {
            System.out.println("自定义的消息消费端");
            System.out.println("consumerTag="+consumerTag);
            System.out.println("envelope="+envelope);
            System.out.println("properties="+properties);
            System.out.println("body="+new String(body));
            //消费端的手动签收,假如关闭手动签收,也关闭自动签收,那么消费端只会接收到一条消息
            channel.basicAck(envelope.getDeliveryTag(),false);
        }
    }
}


7.死信队列(死信交换机)

一:死信队列DLX(Dead-leater-exchange)

1.1)什么是死信?
就是在队列中的消息如果没有消费者消费,那么该消息就成为一个死信,那这个消息被重新发送到另外一个exchange上的话,
那么后面这个exhcange就是死信队列。
1.2)消息变成死信的几种情况
消息被拒绝:(basic.reject/basic.nack)并且requeue(重回队列)的属性设置为false 表示不需要重回队列,那么该消息就是一个死信消息;
消息TTL过期:消息本身设置了过期时间,或者队列设置了消息过期时间x-message-ttl队列达到最大长度:比如队列最大长度是3000 ,那么3001消息就会被送到死信队列上;
1.3)死信队列也是一个正常的exchange,也会通过routingkey 绑定到具体的队列上
 

生产端核心代码:

//消息十秒没有被消费,那么就会转到死信队列上
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
    .deliveryMode(2)
    .expiration("10000")
    .build();
//声明正常的队列
String nomalExchangeName = "zyf.nomaldlx.exchange";
String routingKey = "zyf.dlx.key1";
String message = "我是测试的死信消息";
for(int i=0;i<100;i++) {                   
     channel.basicPublish(nomalExchangeName,routingKey,basicProperties,message.getBytes());
}

消费端核心代码:

//声明交换机
channel.exchangeDeclare(nomalExchangeName,exchangeType,true,false,null);

//设置队列参数
Map<String,Object> queueArgs = new HashMap<>();
//正常队列上绑定死信队列
queueArgs.put("x-dead-letter-exchange",dlxExhcangeName);
//设置正常队列上最大消息数为4
queueArgs.put("x-max-length",4);
//声明正常队列,通过路由key将正常队列与交换机绑定
channel.queueDeclare(nomalqueueName,true,false,false,queueArgs);
channel.queueBind(nomalqueueName,nomalExchangeName,routingKey);

//声明死信交换机、死信队列并绑定
channel.exchangeDeclare(dlxExhcangeName,exchangeType,true,false,null);
channel.queueDeclare(dlxQueueName,true,false,false,null);
channel.queueBind(dlxQueueName,dlxExhcangeName,"#");
//消费消息,关闭自动接收
channel.basicConsume(nomalqueueName,false,new DlxConsumer(channel));

 

自定义死信消费者代码:

public class DlxConsumer extends DefaultConsumer {

    private Channel channel;

    public DlxConsumer(Channel channel) {
        super(channel);
        this.channel = channel;
    }

    public void handleDelivery(String consumerTag,Envelope envelope,AMQP.BasicProperties properties,byte[] body)throws IOException
    {
        System.out.println("接受到消息:"+new String(body));
        //消费端拒绝签收,并且不支持重回队列,那么该条消息就是一条死信消息
        channel.basicNack(envelope.getDeliveryTag(),false,false);

        //channel.basicAck(envelope.getDeliveryTag(),false);
    }
}

 

8.自定义消费者(消费者)

1.非自定义消费者消费消息

//创建消费者
QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
channel.basicConsume(queueName,true,queueingConsumer);

while (true) {
    QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();
    String reserveMsg = new String(delivery.getBody());
    System.out.println("消费消息:"+reserveMsg);
}

2.自定义消费者消费消息

//通过继承DefaultConsumer类来实现自定义的消息端

public class MyConsumer extends DefaultConsumer {

    public MyConsumer(Channel channel) {
        super(channel);
    }

    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException
    {
        //consumerTag: 内部生成的消费标签 
        System.out.println("consumerTag:"+consumerTag);

        //envelope包含属性:deliveryTag(标签), redeliver, exchange, routingKey
        //redeliver是一个标记,如果设为true,表示消息之前可能已经投递过了,现在是重新投递消息到监听队列的消费者
        System.out.println("envelope:"+envelope);

        //properties: 消息属性 
        System.out.println("properties:"+properties);

        //body: 消息内容 
        System.out.println("body:"+new String(body));

    }
}
//自定义消费者消费消息输出结果
consumerTag:amq.ctag-Jqj-_VID7MkNRiPyNdTiGQ
envelope:Envelope(deliveryTag=1, redeliver=true, exchange=zyf.customconsumer.direct, routingKey=zyf.customconsumer.key)
properties:#contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
body:你好zyf0
consumerTag:amq.ctag-Jqj-_VID7MkNRiPyNdTiGQ
envelope:Envelope(deliveryTag=2, redeliver=true, exchange=zyf.customconsumer.direct, routingKey=zyf.customconsumer.key)
properties:#contentHeader<basic>(content-type=null, content-encoding=null, headers=null, delivery-mode=null, priority=null, correlation-id=null, reply-to=null, expiration=null, message-id=null, timestamp=null, type=null, user-id=null, app-id=null, cluster-id=null)
body:你好zyf1

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值