消息中间件——RabbitMQ(七)高级特性 2

news2025/1/20 3:51:29

前言

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

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

1. 自定义消费者

1.1 消费端自定义监听

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

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

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

1.2 代码演示

1.2.1 生产者
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 消费者
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
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: true\false 是否将上面设置应用于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 生产者
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 消费者
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
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

查看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.


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

3.1 打印结果

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

4. 消费端ACK与重回队列

4.1 消费端的手工ACK和NACK

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

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

4.2 消费端的重回队列

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

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

4.3 代码演示

4.3.1 生产者
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 消费者
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
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 直接通过管控台进行演示

通过管控台创建一个队列

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

添加exchange

Queue与Exchange进行绑定

点击 test_ttl_exchange 进行绑定

查看是否绑定成功

通过管控台发送消息

消息未处理自动清除

生产端设置过期时间

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 生产者
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 消费者

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
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

查看queue

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

关闭Consumer,只运行Producer

过10秒钟后,消息过期

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

小结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1269192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

hive里如何高效生成唯一ID

常见的方式&#xff1a; hive里最常用的方式生成唯一id&#xff0c;就是直接使用 row_number() 来进行&#xff0c;这个对于小数据量是ok的&#xff0c;但是当数据量大的时候会导致&#xff0c;数据倾斜&#xff0c;因为最后生成全局唯一id的时候&#xff0c;这个任务是放在一个…

Linux 进程(二)

1.当前工作目录 Linux 下使用 ls /proc 查看程序中的进程&#xff0c;其中这些蓝色的数字代表的就是进程。 其中cwd(current working directory)就是当前工作目录&#xff0c;那么为什么cwd 和 exe 是在同一级目录下呢因为 进程需要依赖可执行程序&#xff0c;可执行程序需要依…

局部内部类(内部类) - Java

局部内部类 说明&#xff1a;局部内部类是定义在外部类的局部位置&#xff0c;比如方法中&#xff0c;并且有类名。 LocalInnerClass.java 非常重要的几点&#xff01;&#xff01; 局部内部类本质还是一个类&#xff0c;该有的属性方法也都可以有。【举例a.见下文】可以直接…

KT1404C语音芯片为什么用着用着,声音就变大了,发指令设置音量?

一、问题简介 有客户反馈&#xff0c;使用KT404C语音芯片&#xff0c;每次主板上电的时候&#xff0c;都会发指令将音量设置为20级&#xff0c;但是实际到使用现场&#xff0c;就会有终端的客人反馈&#xff0c;机器的音量变大了&#xff0c;这个是什么情况呢&#xff0c;该如…

微服务API网关Spring Cloud Gateway实战

概述 微服务网关是为了给不同的微服务提供统一的前置功能&#xff1b;网关服务可以配置集群&#xff0c;以承载更多的流量&#xff1b;负载均衡与网关互相成就&#xff0c;一般使用负载均衡&#xff08;例如 nginx&#xff09;作为总入口&#xff0c;然后将流量分发到多个网关…

远程办公和密码管理的好伴侣

在当今快速变化的商业环境中&#xff0c;远程办公已经不再是一种简单的应急措施&#xff0c;而是被视为企业长期发展的关键战略之一。 随着科技的不断进步和全球化的推动&#xff0c;远程办公为企业提供了更大的灵活性和适应性&#xff0c;使得员工能够更好地平衡工作和生活。这…

ardupilot开发 --- ROS 篇

0. 前言 关于机载计算机&#xff1b; 关于ROS; 关于基于ROS的视觉SLAM和避障&#xff1b; 1. APSync 说到机载计算机Companion computer就不得不提另一个关键词APSync&#xff1b;APSync简化了机载计算机的设置&#xff0c;以便它可以为ArduPilot提供额外的功能&#xff0c;…

Debian arm系统安装wxPython

一、系统版本 二、安装wxPython-4.0.4.tar.gz 1、下载依赖 >sudo apt update >sudo apt-get install build-essential libgtk-3-dev libwebkit2gtk-4.0-dev libssl-dev libcurl4-openssl-dev libgstreamer-plugins-base1.0-dev libnotify-dev freeglut3 freeglut3-dev …

Python爬虫遇到重定向URL问题时如何解决?

什么是重定向 重定向是指当用户请求一个URL时&#xff0c;服务器返回一个中断请求的URL的响应。这种情况通常发生在网站对URL进行了修改或者重定向到其他页面的情况下。其中&#xff0c;如果处理不当开发&#xff0c;可能会导致爬虫无法获取所需的数据&#xff0c;从而影响爬虫…

激光切割机切割工件出现锯齿是什么原因?

金属激光切割机因切割速度快&#xff0c;效率高&#xff0c;切割效果好受到广大金属加工需求的厂家追捧&#xff0c;但在使用时不免出现一些小问题&#xff0c;如&#xff1a;在激光切割加工的时候出现锯齿的问题。 编辑搜图 请点击输入图片描述&#xff08;最多18字&#xff…

传感器:探索Android中的传感器功能与使用

传感器&#xff1a;探索Android中的传感器功能与使用 一、传感器介绍1.1 Android 平台三大类传感器1.2 Android 平台支持的传感器1.3 传感器框架 二、传感器的使用2.1 识别传感器和传感器特性2.2 针对不同制造商的传感器或传感器的不同版本优化2.3 监控传感器事件2.4 处理不同的…

Docker中Alpine容器中配置MariaDB

1.更新镜像源 apk update2.安装 Mysql apk add --no-cache mysql mysql-client # 安装命令也可使用 apk add mariadb mariadb-client&#xff0c;alpine 中 mysql 就是 mariadb3. 安装openrc openrc是Alpine服务控制器&#xff0c;负责Alpine服务启动&#xff0c;添加、删除…

Linux 基本语句_13_消息队列

概念&#xff1a; 不同进程能通过消息队列来进行通信&#xff0c;不同进程也能获取或发送特定类型的消息&#xff0c;即选择性的收发消息。 一般一个程序采取子进程发消息&#xff0c;父进程收消息的模式 常用函数功能&#xff1a; fork(); // 创建子进程 struct msgbuf{ …

探索测试开发工程师的通往成功的秘密路径!

「作者说」随着近几年国内IT行业高速发展&#xff0c;对测试工程师的要求也越来越高&#xff0c;其作用也越来越重要&#xff0c;但很多测试工程师也迎来了个人发展的瓶颈&#xff0c;下一步该向哪个方向发展&#xff0c;该如何发展&#xff1f;本文将概述测试工程师的现状及发…

图片伪装,将RAR文件隐藏到图片里

下载链接 效果图&#xff1a; 代码&#xff1a; ECHO OFF TITLE PtoR MODE con COLS55 LINES25 color 0A:main cls echo.当前时间&#xff1a;%date% %time% echo.欢迎使用图片伪装&#xff0c;本脚本可以将RAR文件隐藏到图片里. echo.set /p "imagefile①请拖入图像文件…

Condition原码分析及实现原理

一、引言 Java作为一种广泛应用于企业级开发的编程语言&#xff0c;其内部机制和特性被许多开发者所关注。本文将深入分析Java Condition原码&#xff0c;以及Condition接口的实现原理&#xff0c;为大家提供一个更深入的了解。 二、Condition概述 Condition是Java并发编程中一…

tcp/ip协议 error=10022 Winsock.reg Winsock2.reg

tcp/ip协议 error10022 这2个注册表选项千万不能删除&#xff0c;否则上不了网。 按下windows键R键&#xff0c;输入regedit&#xff0c;打开注册表&#xff0c;在文件目录里找到如下两个文件夹&#xff0c;删除这两个文件夹。 路径&#xff1a;HKEY_LOCAL_MACHINE\System\C…

Python---练习:求某同学成绩的总分及平均分

需求&#xff1a; 已知某同学的语文(70)、数学(90) 、英语(80)、历史(75)、地理(85)五门课的成绩,编程求该同学的总分以及平均分。 思考&#xff1a; 要求是算总分和平均分&#xff0c;先看总分&#xff0c;已经知道了各科成绩&#xff0c;那么可以用把成绩赋值给每个学科的…

JVM——内存溢出和内存泄漏

目录 1. 内存溢出和内存泄漏内存泄漏的常见场景解决内存溢出的思路1.发现问题 – Top命令2.发现问题 – VisualVM3.发现问题 – Arthas4.发现问题 – Prometheus Grafana5.发现问题 – 堆内存状况的对比![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/058d113715b…

手机厂商集体入局大模型:谁在抢占「新高地」?

手机这个赛道&#xff0c;已然迎来了新的排位赛。大模型这条路上固然走的艰难&#xff0c;但也必须迎头赶上。大模型背后&#xff0c;手机厂商锚定的更是下一个入口战。 作者|斗斗 编辑|皮爷 出品|产业家 手机厂商们开始在大模型领域“卷”起来了。 今年8月&#xff0c;小米…