RabbitMQ的工作模式

RabbitMQ

简介

首先来了解一下AMQP

AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议 的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中 间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。

2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。RabbitMQ 采用 Erlang 语言开发。 Erlang 语言由 Ericson 设计,专门为开发高并发分布式系统的一种语言,在电信领域使用广泛。

RabbitMQ 基础架构如下图:

image-20230810174039355

RabbitMQ中的相关概念

  1. Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
  2. Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网 络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多 个vhost,每个用户在自己的 vhost 创建 exccange/queue 等
  3. Connection:publisher/consumer 和 broker 之间的 TCP 连接
  4. channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线 程,通常每个thread创建单独的 Channel 进行通讯,AMQP method 包含了Channel id 帮助客户端和 message broker 识别 Channel,所以 Channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
  5. Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
  6. Queue:消息最终被送到这里等待 consumer 取走
  7. Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存 到 exccange 中的查询表中,用于 message 的分发依据

工作模式

RabbitMQ 提供了7 种工作模式:简单模式、工作队列模式、发布与订阅模式、路由模式、主题模式、RPC 远程调用模式、发布确认模式(该模式不做讲解)

创建一个maven项目

<project xmlns="http://maven.apacce.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSccema-instance"
	xsi:sccemaLocation="http://maven.apacce.org/POM/4.0.0 https://maven.apacce.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>tedu.cn</groupId>
	<artifactId>rabbitmq</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>rabbitmq</name>

	<dependencies>
		<dependency>
			<groupId>com.rabbitmq</groupId>
			<artifactId>amqp-client</artifactId>
			<version>5.4.3</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.8.0-alpha2</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.8.0-alpha2</version>
		</dependency>
	</dependencies>

访问路径:http://192.168.144.160:15672

用户名和密码都是guest

​ 或

用户名:user 密码:123456

5672是rabbitMQ的客户端端口

15672是rabbitMQ的ui界面访问端口

25672是rabbitMQ中的erlang发现口

简单模式

只有一个消费者

image-20230810175546157

生产者代码
//模拟生产者
public class  SimpleProducer{
	public static void main(String[] args) throws Exception {
		//创建连接工厂,并设置连接信息
		ConnectionFactory cf = new ConnectionFactory();
		cf.setHost("192.168.64.140");
		cf.setPort(5672);//可选,5672是默认端口
		cf.setUsername("guest");
		cf.setPassword("guest");
		
		//1.建立连接 2.在连接中创建通道
		Connection nc = cf.newConnection();
		Channel cc = nc.createChannel();
		
		//2.创建队列
		//和服务器通信,告诉服务器需要一个队列来收发数据:helloworld
		//特点:如果队列不存在,会创建新的队列;如果存在,就什么都不做
		/* 第一个参数:队列名称
		 * 第二个参数:是否是一个持久队列
		 * 第三个参数:是否是独占队列
		 * 第四个参数:是否自动删除
		 * 第五个参数:其他参数属性的设置
		 */
		cc.queueDeclare("helloworld", false, false, false, null);
		//消息数据
		String s="Hello World"+System.currentTimeMillis();
		
	    //3.发送信息
		/**
		 * 第一个参数:先忽略
		 * 第二个参数:队列名称
		 * 第三个参数:其他属性配置
		 * 第四个参数:消息数据,需要转成byte[]类型
		 */
		cc.basicPublish("", "helloworld", null, s.getBytes());
		
		nc.close();
		
	}
}
消费者代码
//模拟消费者
public class SimpleConsumer {
	public static void main(String[] args) throws Exception{
		ConnectionFactory cf=new ConnectionFactory();
		cf.setHost("192.168.64.140");
		//cf.setPort(5672);
		cf.setUsername("guest");
		cf.setPassword("guest");
		//1.创建连接,在连接上创建通道
		Connection nc = cf.newConnection();
		Channel cc = nc.createChannel();
		
		//2.创建队列
		cc.queueDeclare("helloworld", false, false, false, null);
		//处理数据的回调函数
		DeliverCallback deliverCallback = new DeliverCallback() {
			
			@Override
			public void handle(String consumerTag, Delivery message) throws IOException {
				byte[] a = message.getBody();
				String s = new String(a);
				System.out.println(Thread.currentThread().getName()+"收到:"+s);
				System.out.println("-----------");
			}
		};
		//取消数据的回调函数
		CancelCallback cancelCallback = new CancelCallback() {
			@Override
			public void handle(String consumerTag) throws IOException {
				
			}
		};
		//3.接收信息
		/**
		 * 第一个参数:队列名称
		 * 第二个参数:先设为true
		 * 第三个参数:处理数据的回调函数
		 * 第四个参数:取消接收的回调函数
		 */
		cc.basicConsume("helloworld", true,deliverCallback,cancelCallback);
		//nc.close();
	}
}

工作队列模式

image-20230810175725817

与简单模式相比,工作队列模式是多个消费者

多个消费者从同一个队列中消费消息,对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。

工作队列模式与简单模式的代码几乎是一样的。可以完全复制,并多复制一个消费者进行多 个消费者同时对消费消息的测试。

运行测试
运行:
一个生产者
两个消费者
生产者发送多条消息,
如: 1,2,3,4,5. 两个消费者分别收到:
消费者一: 1,3,5
消费者二: 2,4
rabbitmq在所有消费者中轮询分发消息,把消息均匀地发送给所有消费者
消息确认

一个消费者接收消息后,在消息没有完全处理完时就挂掉了,那么这时会发生什么呢?

就现在的代码来说,rabbitmq把消息发送给消费者后,会立即删除消息,那么消费者挂掉后,它没来得及处理的消息就会丢失

如果生产者发送以下消息:

1…

2

3

4

5
两个消费者分别收到:
消费者一: 1…, 3, 5
消费者二: 2, 4
当消费者一收到所有消息后,要话费7秒时间来处理第一条消息,这期间如果关闭该消费者,那么1未处理完成,3,5则没有被处理

为了确保消息不会丢失,rabbitmq支持消息确认(回执)。当一个消息被消费者接收到并且执行完成后,消费者会发送一个ack (acknowledgment) 给rabbitmq服务器, 告诉他我已经执行完成了,你可以把这条消息删除了。

如果一个消费者没有返回消息确认就挂掉了(信道关闭,连接关闭或者TCP链接丢失),rabbitmq就会明白,这个消息没有被处理完成,rabbitmq就会把这条消息重新放入队列,如果在这时有其他的消费者在线,那么rabbitmq就会迅速的把这条消息传递给其他的消费者,这样就确保了没有消息会丢失。

这里不存在消息超时, rabbitmq只在消费者挂掉时重新分派消息, 即使消费者花非常久的时间来处理消息也可以

手动消息确认默认是开启的,前面的例子我们通过autoAck=ture把它关闭了。我们现在要把它设置为false,然后工作进程处理完意向任务时,发送一个消息确认(回执)。

public class Test2 {
	public static void main(String[] args) throws Exception {
		//1.创建连接
		ConnectionFactory cf = new ConnectionFactory();
		cf.setHost("192.168.64.140");
		cf.setPort(5672);
		cf.setUsername("guest");
		cf.setPassword("guest");
		Connection nc = cf.newConnection();
		Channel cc = nc.createChannel();
		//2.定义队列
		cc.queueDeclare("helloworld ", false, false, false, null);
		
		DeliverCallback deliverCallback = new DeliverCallback() {
			
			@Override
			public void handle(String consumerTag, Delivery message) throws IOException {
				byte[] a = message.getBody();
				String b = new String(a);
				System.out.println("收到:"+b);
				//处理消息,遇到".",暂停1s
				for (int i = 0; i < b.length(); i++) {
					//ccarAt(index),返回指定索引位置的字符
					if(b.ccarAt(i)=='.') {
						try {
							Thread.sleep(1000);
						} catcc (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
				System.out.println("消息处理完毕");
				//发送消息确认
				//第一个参数:消息的标签,需要从消息中获取
				//第二个参数:是否确认多条信息,false:只确认一条
				cc.basicAck(message.getEnvelope().getDeliveryTag(), false);
			}
		};
		CancelCallback cancelCallback = new CancelCallback() {
			@Override
			public void handle(String consumerTag) throws IOException {
				
			}
		};
		//3.接收数据
		/**第二个参数:
		 *         true:自动确认
		 *         false:手动确认
		 */
		
		cc.basicConsume("helloworld ",false, deliverCallback, cancelCallback);
	}
}

使用以上代码,就算杀掉一个正在处理消息的工作进程也不会丢失任何消息,工作进程挂掉之后,没有确认的消息就会被自动重新传递。

忘记确认(ack)是一个常见的错误, 这样后果是很严重的, 由于未确认的消息不会被释放, rabbitmq会吃掉越来越多的内存
可以使用下面命令打印工作队列中未确认消息的数量
rabbitmqctl list_queues name messages_ready messages_unacknowledged
当处理消息时异常中断, 可以选择让消息重回队列重新发送.
nack 操作可以是消息重回队列, 可以使用 basicNack() 方法:
// requeue为true时重回队列, 反之消息被丢弃或被发送到死信队列
cc.basicNack(tag, multiple, requeue)
合理地分发

rabbitmq会一次把多个消息分发给消费者, 这样可能造成有的消费者非常繁忙, 而其它消费者空闲. 而rabbitmq对此一无所知, 仍然会均匀的分发消息

我们可以使用 basicQos(1) 方法, 这告诉rabbitmq一次只向消费者发送一条消息, 在返回确认回执前, 不要向消费者发送新消息. 而是把消息发给下一个空闲的消费者

消息持久化

当rabbitmq关闭时, 我们队列中的消息仍然会丢失, 除非明确要求它不要丢失数据

要求rabbitmq不丢失数据要做如下两点: 把队列和消息都设置为可持久化(durable)

队列设置为可持久化, 可以在定义队列时指定参数durable为true

//第二个参数是持久化参数durable
cc.queueDeclare("helloworld", true, false, false, null);

由于之前我们已经定义过队列"hello"是不可持久化的, 对已存在的队列, rabbitmq不允许对其定义不同的参数, 否则会出错, 所以这里我们定义一个不同名字的队列"task_queue"

//定义一个新的队列,名为 task_queue
//第二个参数是持久化参数 durable
cc.queueDeclare("task_queue", true, false, false, null);

生产者和消费者代码都要修改

这样即使rabbitmq重新启动, 队列也不会丢失. 现在我们再设置队列中消息的持久化, 使用

MessageProperties.PERSISTENT_TEXT_PLAIN参数

//第三个参数设置消息持久化
cc.basicPublish("", "task_queue",
            MessageProperties.PERSISTENT_TEXT_PLAIN,
            msg.getBytes());

下面是"工作队列模式"最终完成的生产者和消费者代码

生产者代码
public class WorkProducer {
	public static void main(String[] args) throws Exception {
		//1.建立连接
		ConnectionFactory cf = new ConnectionFactory();
		cf.setHost("192.168.64.140");
		cf.setPort(5672);
		cf.setUsername("guest");
		cf.setPassword("guest");
		Connection nc = cf.newConnection();
		Channel cc = nc.createChannel();
		//2.创建队列
		cc.queueDeclare("task_queue", true, false, false, null);
		
		//3.发送消息
		//死循环在控制台中发送信息
		while(true) {
			System.out.print("输入:");
			String s = new Scanner(System.in).nextLine();
			cc.basicPublish("", "task_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, s.getBytes());
		}
	}
消费者代码
public class WorkConsumer {
	public static void main(String[] args) throws Exception {
		//1.创建连接
		ConnectionFactory cf = new ConnectionFactory();
		cf.setHost("192.168.64.140");
		cf.setPort(5672);
		cf.setUsername("guest");
		cf.setPassword("guest");
		Connection nc = cf.newConnection();
		Channel cc = nc.createChannel();
		//2.定义队列
		cc.queueDeclare("task_queue", true, false, false, null);
		
		DeliverCallback deliverCallback = new DeliverCallback() {
			
			@Override
			public void handle(String consumerTag, Delivery message) throws IOException {
				byte[] a = message.getBody();
				String b = new String(a);
				System.out.println("收到:"+b);
				//处理消息,遇到".",暂停1s
				for (int i = 0; i < b.length(); i++) {
					//ccarAt(index),返回指定索引位置的字符
					if(b.ccarAt(i)=='.') {
						try {
							Thread.sleep(1000);
						} catcc (InterruptedException e) {
							e.printStackTrace();
						}
					}
				}
				System.out.println("消息处理完毕");
				//发送消息确认
				//第一个参数:消息的标签,需要从消息中获取
				//第二个参数:是否确认多条信息,false:只确认一条
				cc.basicAck(message.getEnvelope().getDeliveryTag(), false);
			}
		};
		CancelCallback cancelCallback = new CancelCallback() {
			@Override
			public void handle(String consumerTag) throws IOException {
				
			}
		};
		//设置处理完一条消息之前,不接收其他消息
		cc.basicQos(1);
		//3.接收数据
		/**第二个参数:
		 *         true:自动确认
		 *         false:手动确认
		 */
		
		cc.basicConsume("task_queue",false, deliverCallback, cancelCallback);
	}
}

上一篇文章:MQ详细概念及其了解-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/Z0412_J0103/article/details/143226478下一篇文章: 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值