zoukankan      html  css  js  c++  java
  • 消息中间件——RabbitMQ(八)高级特性全在这里!(下)

    求关注

    高级特性全在这里!(下)

    前言

    上一篇消息中间件——RabbitMQ(七)高级特性全在这里!(上)中我们介绍了消息如何保障100%的投递成功?,幂等性概念详解,在海量订单产生的业务高峰期,如何避免消息的重复消费的问题?,Confirm确认消息、Return返回消息。这篇我们来介绍下下面内容。

    • 自定义消费者
    • 消息的限流(防止占用内存过多,节点宕机)
    • 消息的ACK与重回队列
    • TTL消息
    • 死信队列

    1. 自定义消费者

    1.1 消费端自定义监听

    我们一般就在代码中编写while循环,进行consumer.nextDelivery方法进行获取下一条消息,然后进行消费处理!

    但是这种轮训的方式肯定是不好的,代码也比较low。

    • 我们使用自定义的Consumer更加的方便,解耦性更加的强,也是在实际工作中最常见的使用方式!

    消费端自定义监听

    1.2 代码演示

    1.2.1 生产者

    
    /**
     * 
    * @ClassName: Producer 
    * @Description: 生产者
    * @author Coder编程
    * @date2019年7月30日 下午23:15:51 
    *
     */
    public class Producer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		//1 创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		String exchange = "test_consumer_exchange";
    		String routingKey = "consumer.save";
    		
    		String msg = "Hello RabbitMQ Consumer Message";
    		
    		for(int i =0; i<5; i ++){
    			channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
    		}
    		
    	}
    }
    
    
    

    1.2.2 消费者

    
    /**
     * 
    * @ClassName: Consumer 
    * @Description: 消费者
    * @author Coder编程
    * @date2019年7月30日 下午23:13:51 
    *
     */
    public class Consumer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		
    		// 创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		
    		String exchangeName = "test_consumer_exchange";
    		String routingKey = "consumer.#";
    		String queueName = "test_consumer_queue";
    		
    		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    		channel.queueDeclare(queueName, true, false, false, null);
    		channel.queueBind(queueName, exchangeName, routingKey);
    		
    		//实现自己的MyConsumer()
    		channel.basicConsume(queueName, true, new MyConsumer(channel));
    	}
    }
    
    
    

    1.2.3 自定义类:MyConsumer

    /**
     * 
    * @ClassName: MyConsumer 
    * @Description: TODO
    * @author Coder编程
    * @date 2019年7月30日 下午23:11:55 
    *
     */
    public class MyConsumer extends DefaultConsumer {
    
    
    	public MyConsumer(Channel channel) {
    		super(channel);
    	}
    
    	//根据需求,重写自己需要的方法。
    	@Override
    	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    		System.err.println("-----------consume message----------");
    		//消费标签
    		System.err.println("consumerTag: " + consumerTag);
    		//这个对象包含许多关键信息
    		System.err.println("envelope: " + envelope);
    		System.err.println("properties: " + properties);
    		System.err.println("body: " + new String(body));
    	}
    
    
    }
    
    
    

    1.3 打印结果

    打印结果

    2. 消费端限流

    2.1 什么是消费端的限流?

    • 假设一个场景,首先,我们Rabbitmq服务器有上万条未处理的消息,我们随便打开一个消费者客户端,会出现下面情况:
    • 巨量的消息瞬间全部推送过来,但是我们单个客户端无法同时处理这么多数据!这个时候很容易导致服务器崩溃,出现故障。

    为什么不在生产端进行限流呢?

    因为在高并发的情况下,客户量就是非常大,所以很难在生产端做限制。因此我们可以用MQ在消费端做限流。

    • RabbitMQ提供了一种qos(服务质量保证)功能,即在非自动确认消息的前提下,如果一定数目的消息(通过基于consume或者channel设置Qos的值)未被确认前,不进行消费新的消息。
      在限流的情况下,千万不要设置自动签收,要设置为手动签收
    • void BasicQos(uint prfetchSize,ushort prefetchCount,bool global);

    参数解释:
    prefetchSize:0
    prefetchCount:会告诉RabbitMQ不要同时给一个消费者推送多于N个消息,即一旦有N个消息还没有ack,则该consumer将block掉,直到有消息ack。
    global: truefalse 是否将上面设置应用于channel,简单点说,就是上面限制是channel级别还是consumer级别。
    prefetchSize和global这两项,rabbitmq没有实现,暂且不研究prefetch_count在no_ask = false的情况下生效,即在自动应答的情况下这两个值是不生效的。

    第一个参数:消息的限制大小,消息多少兆。一般不做限制,设置为0
    第二个参数:一次最多处理多少条,实际工作中设置为1就好
    第三个参数:限流策略在什么上应用。在RabbitMQ一般有两个应用级别:1.通道 2.Consumer级别。一般设置为false,true 表示channel级别,false表示在consumer级别

    2.2 代码演示

    2.2.1 生产者

    
    /**
     * 
    * @ClassName: Producer 
    * @Description: 生产者
    * @author Coder编程
    * @date2019年7月30日 下午23:15:51 
    *
     */
    public class Producer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		//1 创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		String exchange = "test_qos_exchange";
    		String routingKey = "qos.save";
    		
    		String msg = "Hello RabbitMQ QOS Message";
    		
    		for(int i =0; i<5; i ++){
    			channel.basicPublish(exchange, routingKey, true, null, msg.getBytes());
    		}
    		
    	}
    }
    
    
    

    2.2.2 消费者

    
    /**
     * 
    * @ClassName: Consumer 
    * @Description: 消费者
    * @author Coder编程
    * @date2019年7月30日 下午23:13:51 
    *
     */
    public class Consumer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		
    		//1 创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		
    		String exchangeName = "test_qos_exchange";
    		String queueName = "test_qos_queue";
    		String routingKey = "qos.#";
    		
    		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    		channel.queueDeclare(queueName, true, false, false, null);
    		channel.queueBind(queueName, exchangeName, routingKey);
    		
    		//1 限流方式  第一件事就是 autoAck设置为 false
    		//设置为1,表示一条一条数据处理
    		channel.basicQos(0, 1, false);
    		
    		channel.basicConsume(queueName, false, new MyConsumer(channel));
    		
    		
    	}
    }
    
    
    

    2.2.3 自定义类:MyConsumer

    
    /**
     * 
    * @ClassName: MyConsumer 
    * @Description: TODO
    * @author Coder编程
    * @date 2019年7月30日 下午23:11:55 
    *
     */
    public class MyConsumer extends DefaultConsumer {
    
    
    	private Channel channel ;
    	
    	public MyConsumer(Channel channel) {
    		super(channel);
    		this.channel = channel;
    	}
    
    	@Override
    	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    		System.err.println("-----------consume message----------");
    		System.err.println("consumerTag: " + consumerTag);
    		System.err.println("envelope: " + envelope);
    		System.err.println("properties: " + properties);
    		System.err.println("body: " + new String(body));
    		
    		//需要做签收,false表示不支持批量签收
    		channel.basicAck(envelope.getDeliveryTag(), false);
    		
    	}
    
    
    }
    
    
    

    2.2.4 测试结果

    我们先注释掉:channel.basicAck(envelope.getDeliveryTag(), false);然后启动Consumer。
    查看Exchange

    Exchange

    查看Queues

    Queues

    然后再启动Producer。查看打印结果:

    打印结果

    我们会发现消费端,只收到了一条消息。这是为什么呢?

    第一点因为我们在consumer中

    channel.basicConsume(queueName, false, new MyConsumer(channel));
    

    第二个参数设置为false为手动签收。

    第二点在qos中设置只接受一条消息。如果这一条消息不给Broker Ack应答的话,那么Broker会认为你并没有消费完这一条消息,那么就不会继续发送消息。

    channel.basicQos(0, 1, false);
    

    可以看下管控台,unack=1,Ready=4,total=5.

    queues


    接下来我们放开注释channel.basicAck(envelope.getDeliveryTag(), false); 进行消息签收。重启服务。

    3.1 打印结果

    打印结果

    可以看到正常打印五条结果

    4. 消费端ACK与重回队列

    4.1 消费端的手工ACK和NACK

    消费端进行消费的时候,如果由于业务异常我们可以进行日志的记录,然后进行补偿!

    如果由于服务器宕机等严重问题,那我们就需要手工进行ACK保障消费端消费成功!

    4.2 消费端的重回队列

    消费端重回队列是为了对没有处理成功的消息,把消息重新传递给Broker!

    一般我们在实际应用中,都会关闭重回队列,也就是设置为False.

    4.3 代码演示

    4.3.1 生产者

    
    /**
     * 
    * @ClassName: Producer 
    * @Description: 生产者
    * @author Coder编程
    * @date2019年7月30日 下午23:15:51 
    *
     */
    public class Producer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		//1创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		String exchange = "test_ack_exchange";
    		String routingKey = "ack.save";
    		
    		
    		
    		for(int i =0; i<5; i ++){
    			
    			Map<String, Object> headers = new HashMap<String, Object>();
    			headers.put("num", i);
    			
    			//添加属性,后续会使用到
    			AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    					.deliveryMode(2) //投递模式,持久化
    					.contentEncoding("UTF-8")
    					.headers(headers)
    					.build();
    			String msg = "Hello RabbitMQ ACK Message " + i;
    			channel.basicPublish(exchange, routingKey, true, properties, msg.getBytes());
    		}
    		
    	}
    }
    
    
    

    4.3.2 消费者

    
    /**
     * 
    * @ClassName: Consumer 
    * @Description: 消费者
    * @author Coder编程
    * @date2019年7月30日 下午23:13:51 
    *
     */
    public class Consumer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		
    		//1创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		
    		String exchangeName = "test_ack_exchange";
    		String queueName = "test_ack_queue";
    		String routingKey = "ack.#";
    		
    		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    		channel.queueDeclare(queueName, true, false, false, null);
    		channel.queueBind(queueName, exchangeName, routingKey);
    		
    		// 手工签收 必须要关闭 autoAck = false
    		channel.basicConsume(queueName, false, new MyConsumer(channel));
    		
    		
    	}
    }
    
    
    

    4.3.3 自定义类:MyConsumer

    /**
     * 
    * @ClassName: MyConsumer 
    * @Description: TODO
    * @author Coder编程
    * @date 2019年7月30日 下午23:11:55 
    *
     */
    public class MyConsumer extends DefaultConsumer {
    
    
    	private Channel channel ;
    	
    	public MyConsumer(Channel channel) {
    		super(channel);
    		this.channel = channel;
    	}
    
    	@Override
    	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    		System.err.println("-----------consume message----------");
    		System.err.println("body: " + new String(body));
    		try {
    			Thread.sleep(2000);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    		if((Integer)properties.getHeaders().get("num") == 0) {
    			//Nack三个参数  第二个参数:是否是批量,第三个参数:是否重回队列(需要注意可能会发生重复消费,造成死循环)
    			channel.basicNack(envelope.getDeliveryTag(), false, true);
    		} else {
    			channel.basicAck(envelope.getDeliveryTag(), false);
    		}
    		
    	}
    
    
    }
    
    
    

    5.1 打印结果:

    打印结果

    注意:
    可以看到重回队列会出现重复消费导致死循环的问题,这时候最好设置重试次数,比如超过三次后,消息还是消费失败,就将消息丢弃。

    6. TTL队列/消息

    6.1 TTL

    • TTL是Time To Live的缩写,也就是生存时间
    • RabbitMQ支持消息的过期时间,在消息发送时可以进行指定
    • RabbitMQ支持队列的过期时间,从消息入队列开始计算,只要超过了队列的超时时间配置,那么消息会自动的清除

    6.2 代码演示

    6.2.1 直接通过管控台进行演示

    添加queue

    通过管控台创建一个队列

    x-max-length 队列的最大大小
    x-message-ttl 设置10秒钟,如果消息还没有被消费的话,就会被清除。

    queue添加成功

    添加exchange

    添加exchange

    Queue与Exchange进行绑定

    点击 test_ttl_exchange 进行绑定

    进行绑定

    查看是否绑定成功

    绑定1

    绑定2

    通过管控台发送消息

    发送消息

    queue中有消息

    消息未处理自动清除
    十秒后消息消失

    生产端设置过期时间

    AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    				.deliveryMode(2)
    				.contentEncoding("UTF-8")
    				.expiration("10000")
    				.headers(headers)
    				.build();
    
    

    这两个属性并不相同,一个对应的是消息体,一个对应的是队列的过期。

    7. 死信队列

    7.1 概念理解

    死信队列:DLX,Dead-Letter-Exchange
    RabbitMQ的死信队里与Exchange息息相关

    • 利用DLX,当消息在一个队列中变成死信(dead message)之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX

    消息变成死信有以下几种情况

    • 消息被拒绝(basic.reject/basic.nack)并且requeue=false
    • 消息TTL过期
    • 队列达到最大长度

    DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性

    当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。

    可以监听这个队列中消息做相应的处理,这个特征可以弥补RabbitMQ3.0以前支持的immediate参数的功能。

    7.2 代码演示

    • 死信队列设置:
    • 首先需要设置死信队列的exchange和queue,然后进行绑定:
      Exchange:dlx.exchange
      Queue:dlx.queue
      RoutingKey:#
    • 然后我们进行正常声明交换机、队列、绑定,只不过我们需要在队列加上一个参数即可:arguments.put("x-dead-letter-exchange","dlx.exchange");
    • 这样消息在过期、requeue、队列在达到最大长度时,消息就可以直接路由到死信队列!

    7.2.1 生产者

    /**
     * 
    * @ClassName: Producer 
    * @Description: 生产者
    * @author Coder编程
    * @date2019年7月30日 下午23:15:51 
    *
     */
    public class Producer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		//创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		String exchange = "test_dlx_exchange";
    		String routingKey = "dlx.save";
    		
    		String msg = "Hello RabbitMQ DLX Message";
    		
    		for(int i =0; i<1; i ++){
    			
    			AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    					.deliveryMode(2)
    					.contentEncoding("UTF-8")
    					.expiration("10000")
    					.build();
    			channel.basicPublish(exchange, routingKey, true, properties, msg.getBytes());
    		}
    		
    	}
    }
    
    
    

    7.2.2 消费者

    /**
     * 
    * @ClassName: Consumer 
    * @Description: 消费者
    * @author Coder编程
    * @date2019年7月30日 下午23:13:51 
    *
     */
    public class Consumer {
    
    	
    	public static void main(String[] args) throws Exception {
    		
    		
    		//创建ConnectionFactory
    		Connection connection = ConnectionUtils.getConnection();
    		Channel channel = connection.createChannel();
    		
    		// 这就是一个普通的交换机 和 队列 以及路由
    		String exchangeName = "test_dlx_exchange";
    		String routingKey = "dlx.#";
    		String queueName = "test_dlx_queue";
    		
    		channel.exchangeDeclare(exchangeName, "topic", true, false, null);
    		
    		Map<String, Object> agruments = new HashMap<String, Object>();
    		agruments.put("x-dead-letter-exchange", "dlx.exchange");
    		//这个agruments属性,要设置到声明队列上
    		channel.queueDeclare(queueName, true, false, false, agruments);
    		channel.queueBind(queueName, exchangeName, routingKey);
    		
    		//要进行死信队列的声明:
    		channel.exchangeDeclare("dlx.exchange", "topic", true, false, null);
    		channel.queueDeclare("dlx.queue", true, false, false, null);
    		channel.queueBind("dlx.queue", "dlx.exchange", "#");
    		
    		channel.basicConsume(queueName, true, new MyConsumer(channel));
    		
    		
    	}
    }
    
    
    

    7.2.3 自定义类:MyConsumer

    
    /**
     * 
    * @ClassName: MyConsumer 
    * @Description: TODO
    * @author Coder编程
    * @date 2019年7月30日 下午23:11:55 
    *
     */
    public class MyConsumer extends DefaultConsumer {
    
    
    	public MyConsumer(Channel channel) {
    		super(channel);
    	}
    
    	@Override
    	public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    		System.err.println("-----------consume message----------");
    		System.err.println("consumerTag: " + consumerTag);
    		System.err.println("envelope: " + envelope);
    		System.err.println("properties: " + properties);
    		System.err.println("body: " + new String(body));
    	}
    }
    
    

    7.2.4 测试结果

    运行Consumer,查看管控台
    查看Exchanges

    exchange

    查看queue

    queue

    可以看到test_dlx_queue多了DLX的标识,表示当队列中出现死信的时候,会将消息发送到死信队列dlx_queue中

    关闭Consumer,只运行Producer

    查看queue10秒钟前

    过10秒钟后,消息过期
    查看queue10秒钟后

    在我们工作中,死信队列非常重要,用于消息没有消费者,处于死信状态。我们可以才用补偿机制。

    小结

    本次主要介绍了RabbitMQ的高级特性,首先介绍了互联网大厂在实际使用中如何保障100%的消息投递成功和幂等性的,以及对RabbitMQ的确认消息、返回消息、ACK与重回队列、消息的限流,以及对超时时间、死信队列的使用

    文末

    欢迎关注个人微信公众号:Coder编程
    获取最新原创技术文章和免费学习资料,更有大量精品思维导图、面试资料、PMP备考资料等你来领,方便你随时随地学习技术知识!
    新建了一个qq群:315211365,欢迎大家进群交流一起学习。谢谢了!也可以介绍给身边有需要的朋友。

    文章收录至
    Github: https://github.com/CoderMerlin/coder-programming
    Gitee: https://gitee.com/573059382/coder-programming
    欢迎关注并star~
    微信公众号

    参考文章:

    《RabbitMQ消息中间件精讲》

    推荐文章:

    消息中间件——RabbitMQ(五)快速入门生产者与消费者,SpringBoot整合RabbitMQ!

    消息中间件——RabbitMQ(六)理解Exchange交换机核心概念!

    消息中间件——RabbitMQ(七)高级特性全在这里!(上)

  • 相关阅读:
    opencv源码编译安装后使用时出现undefined reference cv::imwrite
    OPPO手机永久打开USB调试模式
    bash 顺序执行等待前一个脚本执行完成
    cpp
    多线程
    关于nvme下ubuntu无法识别硬盘的问题
    极限建站
    新生赛
    pc_mbed_fpga_communication
    color_sensor_mbed
  • 原文地址:https://www.cnblogs.com/coder-programming/p/11424152.html
Copyright © 2011-2022 走看看