文章目录
RabbitMQ(一)
点击链接,查看上一篇,rabbitmq的安装及基本概念: RabbitMQ(一)
一、rabbitmq六种工作模式
1.1 简单模式
RabbitMQ是一个消息中间件,你可以想象它是一个邮局。当你把信件放到邮箱里时,能够确信邮递员会正确地递送你的信件。RabbitMq就是一个邮箱、一个邮局和一个邮递员。
- 发送消息的程序是生产者
- 队列就代表一个邮箱。虽然消息会流经RbbitMQ和你的应用程序,但消息只能被存储在队列里。队列存储空间只受服务器内存和磁盘限制,它本质上是一个大的消息缓冲区。多个生产者可以向同一个队列发送消息,多个消费者也可以从同一个队列接收消息.
- 消费者等待从队列接收消息
1.1.1 创建Maven项目
1.1.2 pom.xml
添加 slf4j 依赖, 和 rabbitmq amqp 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.tedu</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
<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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
1.1.3 生产者发送消息
- 连接rabbitmq服务器
- 定义队列
- 发送消息
- 关闭连接
package m1_simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class Producer {
public static void main(String[] args) throws Exception {
//1.连接rabbitmq服务器
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
// 用谁的服务器,设置一个自己的虚拟主机,用虚拟机的话就不必操作这行代码
//f.setVirtualHost("/lx");
/*
* 与rabbitmq服务器建立连接,
* rabbitmq服务器端使用的是nio,会复用tcp连接,
* 并开辟多个信道与客户端通信
* 以减轻服务器端建立连接的开销
*/
Connection con = f.newConnection();
//建立信道
Channel c = con.createChannel();
//2.定义队列(会通知服务器想使用一个“helloworld队列”,服务器会找到这个队列,如果不存在,服务器会新建队列)
/*
* 声明队列,会在rabbitmq中创建一个队列
* 如果已经创建过该队列,就不能再使用其他参数来创建
*
* 参数含义:
* -queue: 队列名称
* -durable: 队列持久化,true表示RabbitMQ重启后队列仍存在
* -exclusive: 排他,true表示限制仅当前连接可用
* -autoDelete: 当最后一个消费者断开后,是否删除队列
* -arguments: 其他参数
*/
c.queueDeclare("helloworld111",false,false,false,null);
//3.发送消息
/*
* 发布消息
* 这里把消息向默认交换机发送.
* 默认交换机隐含与所有队列绑定,routing key即为队列名称
*
* 参数含义:
* -exchange: 交换机名称,空串表示默认交换机"(AMQP default)",不能用 null
* -routingKey: 对于默认交换机,路由键就是目标队列名称
* -props: 其他参数,例如头信息
* -body: 消息内容byte[]数组
*/
c.basicPublish("","helloworld",null,"hello".getBytes());
System.out.println("消息已发送!!");
//4.断开链接
c.close();
con.close();
}
}
1.1.4 消费者接收消息
package m1_simple;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws Exception {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");
f.setPort(5672);//默认端口,可以不设置
f.setUsername("admin");
f.setPassword("admin");
//建立通道
Channel c = f.newConnection().createChannel();
//2.定义队列
c.queueDeclare("helloworld",false,false,false,null);
//4.创建回调对象
//4.处理消息的回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
//处理消息
byte[] body = message.getBody();
//把消息还原成字符串
String msg = new String(body);
System.out.println("收到:"+msg);
}
};
//5.取消消息的回调
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//3.从 helloworld 队列接收消息,消费消息
c.basicConsume("helloworld",true,deliverCallback,cancelCallback);
}
}
1.2 工作模式
工作队列(即任务队列)背后的主要思想是避免立即执行资源密集型任务,并且必须等待它完成。相反,我们将任务安排在稍后完成。
我们将任务封装为消息并将其发送到队列。后台运行的工作进程将获取任务并最终执行任务。当运行多个消费者时,任务将在它们之间分发。
使用任务队列的一个优点是能够轻松地并行工作。如果我们正在积压工作任务,我们可以添加更多工作进程,这样就可以轻松扩展。
1.2.1生产者发送消息
这里模拟耗时任务,发送的消息中,每个点使工作进程暂停一秒钟,例如"Hello…"将花费3秒钟来处理
package m2_work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
//建立通道
Channel c = f.newConnection().createChannel();
//2.定义队列
c.queueDeclare("helloworld",false,false,false,null);
//3.发消息
while (true) {
System.out.println("输入消息:");
String msg = new Scanner(System.in).nextLine();
c.basicPublish("", "helloworld", null, msg.getBytes());
}
}
}
1.2.2 消费者接收消息
package m2_work;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws Exception {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
//建立通道
Channel c = f.newConnection().createChannel();
//2.定义队列
c.queueDeclare("helloworld",false,false,false,null);
//4.建立回调对象
//4.处理消息的回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg = new String(message.getBody());
System.out.println("收到:"+msg);
for (int i=0;i<msg.length();i++){
if (msg.charAt(i)=='.'){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
}
}
}
System.out.println("消息处理完成\n");
}
};
//5.取消消息的回调
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//3.开始消费数据
c.basicConsume("helloworld",true,deliverCallback,cancelCallback);
}
}
工作模式:多次启动Consumer测试,
在eclipse或者STS里面的话,直接多次run就行,但是在idea里面需要设置,如图所示:
1.2.3 启动多个消费者测试
(自己注意观察,这种情况是平均分配)
运行:
- 一个生产者
- 两个消费者
生产者发送多条消息,如: 1,2,3,4,5. 两个消费者分别收到:
- 消费者一: 1,3,5
- 消费者二: 2,4
rabbitmq在所有消费者中 轮询分发 消息,把消息均匀地发送给所有消费者
1.2.3 消息确认(Ack)
一个消费者接收消息后,在消息没有完全处理完时就挂掉了,那么这时会发生什么呢?
就现在的代码来说,rabbitmq把消息发送给消费者后,会立即删除消息,那么消费者挂掉后,它没来得及处理的消息就会丢失
如果生产者发送以下消息:
1……
2
3
4
5
两个消费者分别收到:
- 消费者一: 1…, 3, 5
- 消费者二: 2, 4
当消费者一收到所有消息后,要话费7秒时间来处理第一条消息,这期间如果关闭该消费者,那么1未处理>完成,3,5则没有被处理
我们并不想丢失任何消息, 如果一个消费者挂掉,我们想把它的任务消息派发给其他消费者
为了确保消息不会丢失,rabbitmq支持消息确认(回执)。当一个消息被消费者接收到并且执行完成后,消费者会发送一个ack (acknowledgment) 给rabbitmq服务器, 告诉他我已经执行完成了,你可以把这条消息删除了。
如果一个消费者没有返回消息确认就挂掉了(信道关闭,连接关闭或者TCP链接丢失),rabbitmq就会明白,这个消息没有被处理完成,rebbitmq就会把这条消息重新放入队列,如果在这时有其他的消费者在线,那么rabbitmq就会迅速的把这条消息传递给其他的消费者,这样就确保了没有消息会丢失。
这里不存在消息超时, rabbitmq只在消费者挂掉时重新分派消息, 即使消费者花非常久的时间来处理消息也可以
手动消息确认默认是开启的,前面的例子我们通过autoAck=ture把它关闭了。我们现在要把它设置为false,然后工作进程处理完意向任务时,发送一个消息确认(回执)。
消费者设置 ACK
- 开始消费数据时,设置手动ACK:
c.basicConsume(队列, false, 回调对象。。。)
- 处理完消息后,要发送回执
回调对象中处理完消息后:
c.basicAck(消息回执, 是否确认多条消息);
package m2_work;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws Exception {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
//建立通道
Channel c = f.newConnection().createChannel();
//2.定义队列
c.queueDeclare("helloworld",false,false,false,null);
//4.建立回调对象
//4.处理消息的回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg = new String(message.getBody());
System.out.println("收到:"+msg);
for (int i=0;i<msg.length();i++){
if (msg.charAt(i)=='.'){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
}
}
}
///
//消息回执数据
//false表示不确认多条回执数据,只确认当前这一条
c.basicAck(message.getEnvelope().getDeliveryTag(),false);
///
System.out.println("消息处理完成\n");
}
};
//5.取消消息的回调
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//3.消费数据
///
c.basicConsume("helloworld",false,deliverCallback,cancelCallback);
System.out.println("开始消费数据");
///
}
}
使用以上代码,就算杀掉一个正在处理消息的工作进程也不会丢失任何消息,工作进程挂掉之后,没有确认的消息就会被自动重新传递。
忘记确认(ack)是一个常见的错误, 这样后果是很严重的, 由于未确认的消息不会被释放, rabbitmq会吃掉越来越多的内存
可以使用下面命令打印工作队列中未确认消息的数量rabbitmqctl list_queues name messages_ready messages_unacknowledged
当处理消息时异常中断, 可以选择让消息重回队列重新发送.
nack 操作可以是消息重回队列, 可以使用 basicNack() 方法:// requeue为true时重回队列, 反之消息被丢弃或被发送到死信队列 c.basicNack(tag, multiple, requeue)
1.2.4 合理分发(Qos)
rabbitmq会一次把多个消息分发给消费者, 这样可能造成有的消费者非常繁忙, 而其它消费者空闲. 而rabbitmq对此一无所知, 仍然会均匀的分发消息
我们可以使用 basicQos(1)
方法, 这告诉rabbitmq一次只向消费者发送一条消息, 在返回确认回执前, 不要向消费者发送新消息. 而是把消息发给下一个空闲的消费者
1.2.5 消息持久化
当rabbitmq关闭时, 我们队列中的消息仍然会丢失, 除非明确要求它不要丢失数据
要求rabbitmq不丢失数据要做如下两点: 把队列和消息都设置为可持久化(durable)
队列设置为可持久化, 可以在定义队列时指定参数durable为true
//第二个参数是持久化参数durable
ch.queueDeclare("helloworld", true, false, false, null);
由于之前我们已经定义过队列"hello"是不可持久化的, 对已存在的队列, rabbitmq不允许对其定义不同的参数, 否则会出错, 所以这里我们定义一个不同名字的队列"task_queue"
//定义一个新的队列,名为 task_queue
//第二个参数是持久化参数 durable
ch.queueDeclare("task_queue", true, false, false, null);
生产者和消费者代码都要修改
这样即使rabbitmq重新启动, 队列也不会丢失. 现在我们再设置队列中消息的持久化, 使用MessageProperties.PERSISTENT_TEXT_PLAIN参数
消息本身也要设置成持久消息
c.basicPublish("", "helloworld", MessageProperties.PERSISTENT_TEXT_PLAIN, 消息)
//第三个参数设置消息持久化
ch.basicPublish("", "task_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
msg.getBytes());
下面是"工作模式"最终完成的生产者和消费者代码
生产者代码
package m2_work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
public class Producer {
public static void main(String[] args) throws Exception {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
//建立通道
Channel c = f.newConnection().createChannel();
//2.定义队列
//c.queueDeclare("helloworld",false,false,false,null);
//持久队列
c.queueDeclare("test_queue",true,false,false,null);
//3.发消息
while (true) {
System.out.println("输入消息:");
String msg = new Scanner(System.in).nextLine();
//c.basicPublish("", "helloworld", null, msg.getBytes());
c.basicPublish("", "test_queue", MessageProperties.PERSISTENT_TEXT_PLAIN, msg.getBytes());
}
}
}
消费者代码
package m2_work;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
public static void main(String[] args) throws Exception {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
//建立通道
Channel c = f.newConnection().createChannel();
//2.定义队列
//c.queueDeclare("helloworld",false,false,false,null);
c.queueDeclare("test_queue",true,false,false,null);
//4.建立回调对象
//4.处理消息的回调
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws IOException {
String msg = new String(message.getBody());
System.out.println("收到:"+msg);
for (int i=0;i<msg.length();i++){
if (msg.charAt(i)=='.'){
try {
Thread.sleep(1000);
}catch (InterruptedException e){
}
}
}
//消息回执数据
//false表示不确认多条回执数据,只确认当前这一条
c.basicAck(message.getEnvelope().getDeliveryTag(),false);
System.out.println("消息处理完成\n");
}
};
//5.取消消息的回调
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
/**
* Qos:Quality of Service
* 理解:每次抓取的消息数量
* 如果设置成1,每次只抓取一条消息,这条消息处理完之前,不会继续抓取下一条
* 必须在手动的Ack模式下才有效
*/
c.basicQos(1);
//3.消费数据
//c.basicConsume("helloworld",false,deliverCallback,cancelCallback);
c.basicConsume("test_queue",false,deliverCallback,cancelCallback);
System.out.println("开始消费数据");
}
}
1.3 发布订阅模式
在前面的例子中,我们任务消息只交付给一个工作进程。在这部分,我们将做一些完全不同的事情——我们将向多个消费者传递同一条消息。这种模式称为“发布/订阅”。
为了说明该模式,我们将构建一个简单的日志系统。它将由两个程序组成——第一个程序将发出日志消息,第二个程序接收它们。
在我们的日志系统中,接收程序的每个运行副本都将获得消息。这样,我们就可以运行一个消费者并将日志保存到磁盘; 同时我们可以运行另一个消费者在屏幕上打印日志。
最终, 消息会被广播到所有消息接受者
1.3.1 Exchanges 交换机
RabbitMQ消息传递模型的核心思想是,生产者永远不会将任何消息直接发送到队列。实际上,通常生产者甚至不知道消息是否会被传递到任何队列。
相反,生产者只能向交换机(Exchange)发送消息。交换机是一个非常简单的东西。一边接收来自生产者的消息,另一边将消息推送到队列。交换器必须确切地知道如何处理它接收到的消息。它应该被添加到一个特定的队列中吗?它应该添加到多个队列中吗?或者它应该被丢弃。这些规则由exchange的类型定义。
有几种可用的交换类型:direct、topic、header和fanout。我们将关注最后一个——fanout。让我们创建一个这种类型的交换机,并称之为 logs: ch.exchangeDeclare("logs", "fanout");
fanout交换机非常简单。它只是将接收到的所有消息广播给它所知道的所有队列。这正是我们的日志系统所需要的。
我们前面使用的队列具有特定的名称(还记得hello和task_queue吗?)能够为队列命名对我们来说至关重要——我们需要将工作进程指向同一个队列,在生产者和消费者之间共享队列。
但日志记录案例不是这种情况。我们想要接收所有的日志消息,而不仅仅是其中的一部分。我们还只对当前的最新消息感兴趣,而不是旧消息。
要解决这个问题,我们需要两件事。首先,每当我们连接到Rabbitmq时,我们需要一个新的空队列。为此,我们可以创建一个具有随机名称的队列,或者,更好的方法是让服务器为我们选择一个随机队列名称。其次,一旦断开与使用者的连接,队列就会自动删除。在Java客户端中,当我们不向queueDeclare()提供任何参数时,会创建一个具有生成名称的、非持久的、独占的、自动删除队列
//自动生成队列名
//非持久,独占,自动删除
String queueName = ch.queueDeclare().getQueue();
1.3.2 绑定 Bindings
我们已经创建了一个fanout交换机和一个队列。现在我们需要告诉exchange向指定队列发送消息。exchange和队列之间的关系称为绑定。
//指定的队列,与指定的交换机关联起来
//成为绑定 -- binding
//第三个参数时 routingKey, 由于是fanout交换机, 这里忽略 routingKey
ch.queueBind(queueName, "logs", "");
现在, logs交换机将会向我们指定的队列添加消息
列出绑定关系:
rabbitmqctl list_bindings
1.3.3代码展示
生产者
生产者发出日志消息,看起来与前一教程没有太大不同。最重要的更改是,我们现在希望将消息发布到logs交换机,而不是无名的日志交换机。我们需要在发送时提供一个routingKey,但是对于fanout交换机类型,该值会被忽略。
package m3_publishsubscribe;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
public class Producer {
public static void main(String[] args) throws Exception {
//1.连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
Channel c = f.newConnection().createChannel();
//2.定义交换机,两种写法
//c.exchangeDeclare("logs","fanout");
c.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
//向交换机发送数据
while (true){
System.out.println("输入消息:");
String msg = new Scanner(System.in).nextLine();
//第二个参数,对与fanout交换机无效
c.basicPublish("logs","",null,msg.getBytes());
}
}
}
启动查看一下代码有没有成功!访问一下rabbitmq(要确保rabbitmq虚拟机处于运行状态)
这是我的Linux虚拟机的IP地址,自己查看一下你的虚拟机的ip:http://192.168.126.129:15672
此时,还没有建立消费者,那么数据都哪去了呢?
答:没有接收者,数据就消失了!!!
消费者
如果还没有队列绑定到交换器,消息就会丢失,但这对我们来说没有问题;如果还没有消费者在听,我们可以安全地丢弃这些信息。
package m3_publishsubscribe;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.UUID;
public class Consumer {
public static void main(String[] args) throws Exception {
//一、连接
ConnectionFactory f = new ConnectionFactory();
f.setHost("192.168.126.129");//写rabbitmq服务器地址
f.setPort(5672);//5672是通信端口,收发消息是5672,15672是管理界面
f.setUsername("admin");
f.setPassword("admin");
Channel c = f.newConnection().createChannel();
/**
* 二、先做三步:
* 1、定义交换机
* 2、定义随机队列
* 3、绑定到交换机
*/
//定义交换机
c.exchangeDeclare("logs", BuiltinExchangeType.FANOUT);
//随机定义队列名方法 ①
//String queue = UUID.randomUUID().toString();//随机定义队列名
// //随机队列名 非持久 独占 自动删除 其他参数
//c.queueDeclare(queue,false,true,true,null);
//随机定义队列名方法 ②
// 让rabbitmq服务器来随机命名,非持久,独占,自动删除,
String queue = c.queueDeclare().getQueue();
//绑定交换机
//第三个参数,对fanout交换机无效
c.queueBind(queue,"logs","");
//四、创建匿名内部类
DeliverCallback deliverCallback = new DeliverCallback() {
@Override
public void handle(String consumerTag, Delivery message) throws Exception {
//将消息还原成字符串
String msg = new String(message.getBody());
System.out.println("收到: "+msg);
}
};
CancelCallback cancelCallback = new CancelCallback() {
@Override
public void handle(String consumerTag) throws IOException {
}
};
//三、正常消费数据
c.basicConsume(queue, true, deliverCallback, cancelCallback);
}
}
消费者启动需要启动多个!!
设置好之后多次启动
当启动多个消费者的时候,还可以进行反向查看。
- 展开bindings
- 点击选中的②
- 可以查看到如下图所示
生产者、消费者都定义同一个交换机的原因??
答:谁先启动谁负责把交换机创建出来,也就是和启动顺序有关!