RabbitMQ 高级特性

一、如何保证消息100%投递成功

①保障消息成功发出
②保障MQ节点成功接收
③发送端收到MQ节点(Broker)确认应答
④完善的消息进行补偿机制(因为前面可以能哪一步出现网络问题导致失败)

解决方案如下:
1、消息入库,然后发送成功后修改状态,外加定时任务轮询发送失败的消息
在这里插入图片描述
生产者在发送消息前,先将业务信息和要发送的消息分布入库到对应的数据库;第二步在将消息发送的MQ中;第三步是MQ收到的结果应答给生产端;第四步监听确认接收到了消息更新消息状态。
①如果步骤一都失败那就需要使用快速失败;
②有一个分布式定时任务间隔时间去扫描隔了多少分钟消息状态还是失败将进行消息重发机制,当重发次数达到一定次数将不再重发(可能是exchange、routingkey等删除或格式和内容问题导致)

这种方案如果使用事务的在高并发情况下,会遇见IO瓶颈,对数据库操作两次

2、消息延迟投递,做二次确认,回调检查
在这里插入图片描述

上游服务先入库业务然后发送两条消息(一条是直接发送,另一条是延迟投递检查消息)到MQ,下游服务监听到消息进行处理后,将结果组成一条新的消息发送到MQ中,回调服务监听到消息就会入库消息状态等信息。

延迟投递检查消息(具有相同业务内容,只是发送到不同队列)发送到MQ,被回调服务监听到,然后查询数据库,没有记录则或者处理结果为失败场景则进行RPC回调上游服务查询后重发。

二、幂等性

同一个操作,一次请求和多次请求的结果都是一样的。
在这里插入图片描述
消费者端 幂等性保障;在海量的订单产生的业务高峰期,如何避免消息的重复消费;由于网络等原因重发多次,即使收到了多条一样消息只能消费一次。

方案一: 唯一id +指纹码 机制,利用数据库去重。
业务操作前先去数据库查询,没有进行插入数据,有就丢弃 表示已经处理过了;
这里还加指纹码是为了保证某一瞬间连续操作可能导致不唯一问题(指纹码可以是时间戳或业务规则)。
优点是实现简单,在高并发情况下有数据库写入瓶颈(解决方案:根据id分表分库进行算法路由)
在这里插入图片描述

方案二: 利用redis原子性实现,(比如看是否存在、自增)

三、消息确认confirm

在这里插入图片描述

生产者将消息投递到MQ broker,然后broker确认消息给生产者应答,生产者用来确定消息是否正常送达到broker,这就是可靠性投递核心保障(ack失败重发)。

步骤:①confirmSelect、②addConfirmListener

public class Product {

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个连接工厂,并配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.25.128");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        //创建一个连接
        Connection connection = connectionFactory.newConnection();
        //创建一个channel用通信
        Channel channel = connection.createChannel();
        //开启消息确认模式
        channel.confirmSelect();
        //监听不可达消息
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                                     AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.err.println("接收到不可达消息 replyCode " + replyCode);
                System.err.println("接收到不可达消息 replyText " + replyText);
                System.err.println("接收到不可达消息 exchange " + exchange);
                System.err.println("接收到不可达消息 routingKey " + routingKey);
                System.err.println("接收到不可达消息 body" + new String(body,"UTF-8"));
            }
        });

        //发布一条消息
        String msg = "test one message!";
        //mandatory如果为true,则return监听器会接收到不可达消息
        channel.basicPublish("test_direct_exchange","test_direct1",true,null,msg.getBytes());
        //异步监听broker接收反馈
        channel.addConfirmListener(new ConfirmListener() {
            @Override//消息成功处理
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                //deliveryTag;唯一消息标签  multiple:是否批量
                System.err.println("Broker ack 成功了!");
            }

            @Override //消息失败处理
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("Broker ack 失败了!");
            }
        });
    }
    }
}
接收到不可达消息 replyCode 312
接收到不可达消息 replyText NO_ROUTE
接收到不可达消息 exchange test_direct_exchange
接收到不可达消息 routingKey test_direct
接收到不可达消息 bodytest one message!
Broker ack 成功了!

四、Return消息机制

用于处理一些生产者发布的不可路由的消息,比如exchange或者Routingkey不存在,我们就需要使用return Listener去监听

mandatory:如果为true,则监听器会接收到不可达消息,然后进行处理;false broker端会自动删除该消息(例子在上面这块代码中);只有设置mandatory为true条件下return监听器才有用

步骤:①mandatory设置为true ②addReturnListener添加监听器

五、消费端限流

从MQ Broker 中堆积着巨量的消息,推送消费端会造成服务器崩溃或故障。消息生产端不可能做限流因为就是这么大并发需求,所以只能使用MQ做一个流量削峰,然后做消费端限流(不然有可能消费端崩溃或性能下降)。
RabbitMQ提供了qos服务保障功能,在非自动ack前提下,如果一定数目的消息未被确认前(通过基于consumer或者channel设置Qos值),不进行消费新的消息

步骤:①设置消费端手动签收 ②basicQos开启条件限流;

public class Product {

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个连接工厂,并配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.25.128");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");

        //创建一个连接
        Connection connection = connectionFactory.newConnection();
        //创建一个channel用通信
        Channel channel = connection.createChannel();
        //开启消息确认模式
        channel.confirmSelect();
        //监听不可达消息
        channel.addReturnListener(new ReturnListener() {
            @Override
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey,
                                     AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.err.println("接收到不可达消息 replyCode" + replyCode);
                System.err.println("接收到不可达消息 replyText" + replyText);
                System.err.println("接收到不可达消息 exchange" + exchange);
                System.err.println("接收到不可达消息 routingKey" + routingKey);
                System.err.println("接收到不可达消息 body" + new String(body/*,"UTF-8"*/));
            }
        });

        //发布一条消息
        String msg = "test one message!";
        for (int i = 0; i < 5; i++) {
            //mandatory如果为true,则return监听器会接收到不可达消息
            channel.basicPublish("test_direct_exchange", "test_direct", true, null, msg.getBytes());
        }
        //异步监听broker回传
        channel.addConfirmListener(new ConfirmListener() {
            @Override//消息成功处理
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                //deliveryTag;唯一消息标签  multiple:是否批量
                System.err.println("Broker ack 成功了!");
            }

            @Override //消息失败处理
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.err.println("Broker ack 失败了!");
            }
        });
    }
    }
}

public class Customer {

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个连接工厂,并配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.25.128");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setAutomaticRecoveryEnabled(true);//自动重连
        connectionFactory.setNetworkRecoveryInterval(3000);//自动重连时间间隔3秒

        //创建一个连接
        Connection connection = connectionFactory.newConnection();
        //创建一个channel用通信
        Channel channel = connection.createChannel();

        //声明一个test_direct_exchange名称、类型为direct、持久化的交换机
        String exchangName = "test_direct_exchange";
        channel.exchangeDeclare(exchangName,"direct",true);
        //声明一个队列
        String queueName = "test_direct_queue";
        channel.queueDeclare(queueName,true,false,false,null);
        //将队列绑定到交换机上,按照键test_direct路由
        channel.queueBind(queueName,exchangName,"test_direct");
        //prefetchSize为0是不限制消息大小;prefetchCount为限制一次传送给消费者的消息数;
        // global  false:消费服务器级别限制(常用)  true:channel级别限制
        channel.basicQos(0,1,false);
        //监听队列,AutoAck为false才会消费一个,成功签收后才能继续消费
        channel.basicConsume(queueName,false,new MyConsumer(channel));
    }
}
public class MyConsumer extends DefaultConsumer {
    //MQ传输消息的信道
    private Channel channel;

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

    @Override //处理MQ传送的消息
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                               byte[] body) throws IOException {
        System.err.println("接收到消息 consumerTag: " + consumerTag);
        System.err.println("接收到消息 envelope: " + envelope);
        System.err.println("接收到消息 properties: " + properties);
        System.err.println("接收到message:"+ new String(body));
        //签收操作 deliveryTag是消息的唯一性标签; multiple:false不批量签收
        channel.basicAck(envelope.getDeliveryTag(),false);
    }
}

如果出现消费服务消息没有处理完,所以没有ack签收消息,所以MQ将不会再次发生消息过来,这样就达到限流效果。
在这里插入图片描述

消费服务端:

接收到消息 consumerTag: amq.ctag-zl-ZR63L3dzIbvhG3psrKQ
接收到消息 envelope: Envelope(deliveryTag=1, redeliver=false, exchange=test_direct_exchange, routingKey=test_direct)
接收到消息 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)
接收到message:test one message!

生产者服务端:这里正在处理第二条消息

Broker ack 成功了!
Broker ack 成功了!

六、消费端ACK与重回队列机制

步骤:①设置消费端手动签收,如果没有给返回ack应答,那么这条消息会继续存在unacked状态下,占据Broker队列的空间。

public class Customer {

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个连接工厂,并配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.25.128");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setAutomaticRecoveryEnabled(true);//自动重连
        connectionFactory.setNetworkRecoveryInterval(3000);//自动重连时间间隔3秒

        //创建一个连接
        Connection connection = connectionFactory.newConnection();
        //创建一个channel用通信
        Channel channel = connection.createChannel();

        //声明一个test_direct_exchange名称、类型为direct、持久化的交换机
        String exchangName = "test_direct_exchange";
        channel.exchangeDeclare(exchangName,"direct",true);
        //声明一个队列
        String queueName = "test_direct_queue";
        channel.queueDeclare(queueName,true,false,false,null);
        //将队列绑定到交换机上,按照键test_direct路由
        channel.queueBind(queueName,exchangName,"test_direct");
        //prefetchSize为0是不限制消息大小;prefetchCount为限制一次传送给消费者的消息数;
        // global  false:消费服务器级别限制(常用)  true:channel级别限制
//        channel.basicQos(0,1,false);
        //监听队列,AutoAck为false才会消费一个,成功签收后才能继续消费
        channel.basicConsume(queueName,false,new MyConsumer(channel));
    }
}
public class MyConsumer extends DefaultConsumer {
    //MQ传输消息的信道
    private Channel channel;

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

    @Override //处理MQ传送的消息
    public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                               byte[] body) throws IOException {
        System.err.println("接收到message:"+ new String(body));
       if((Integer)properties.getHeaders().get("num") == 0){
       		//消费端回传不签收  requeue: true重新到队列的尾部,false丢弃
            channel.basicNack(envelope.getDeliveryTag(),false,true);
            //消费丢弃  
            //channel.basicNack(envelope.getDeliveryTag(),false,false);
       }else {
//            deliveryTag是消息的唯一性标签; multiple:false不批量签收
            channel.basicAck(envelope.getDeliveryTag(),false);
       }

    }
}

控制台:会有消息0一直重回队列尾部,然后重发给消费端。

接收到message:test one message!0
接收到message:test one message!1
接收到message:test one message!2
接收到message:test one message!3
接收到message:test one message!4
接收到message:test one message!0
接收到message:test one message!0
接收到message:test one message!0

七、TTL消息详解

消息、队列的过期时间,从消息进入队列开始计算,超过指定时间将会被自动清除。

八、死信队列

DLX:dead-letter-exchange

这条消息没人去消费,变成死信了。RabbitMQ会将这种消息重新publish到另一个DLX的exchange的队列上。

1、消息变成死信有如下几种情况:

  • 消息被拒绝(basic.reject/basic.nack)并且requeue=false
  • 消息TTL过期(消息设置属性过期时间 或者 设置 队列过期时间)
  • 队列达到最大长度
public class Customer {

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建一个连接工厂,并配置
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.25.128");
        connectionFactory.setPort(5672);
        connectionFactory.setVirtualHost("/");
        connectionFactory.setAutomaticRecoveryEnabled(true);//自动重连
        connectionFactory.setNetworkRecoveryInterval(3000);//自动重连时间间隔3秒

        //创建一个连接
        Connection connection = connectionFactory.newConnection();
        //创建一个channel用通信
        Channel channel = connection.createChannel();

        //声明一个test_direct_exchange名称、类型为direct、持久化的交换机
        String exchangName = "test_direct_exchange";
        channel.exchangeDeclare(exchangName,"direct",true);
        //声明一个队列
        String queueName = "test_direct_queue_dxl";
        //定义当前队列死信后publish到死信交换机dlx.exchange对应的队列
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-dead-letter-exchange", "dlx.exchange");
        channel.queueDeclare(queueName,true,false,false,arguments);
        //将队列绑定到交换机上,按照键test_direct路由
        channel.queueBind(queueName,exchangName,"test_direct");

        //定义一个dlx.exchange交换机死信队列dlx.queue
        channel.exchangeDeclare("dlx.exchange","topic",true,false,null);
        channel.queueDeclare("dlx.queue",true,false,false,null);
        channel.queueBind("dlx.queue","dlx.exchange","#");
    }
}

如下图所示由于消息没有被消费导致死信,然而也配置了对应的死信队列,所以如下显示
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值