菜鸟的RabbitMQ学习总结
说明
更新时间:2020/9/13 23:32,更新到了rabbitmq集群
更新时间:2020/9/10 21:26,更新到了第五种模型-topic
更新时间:2020/9/9 22:46,更新到了第一种模型-直连
本文主要对RabbitMQ进行学习与记录,本文会持续更新,不断地扩充
本文仅为记录学习轨迹,如有侵权,联系删除
一、概念
(1)什么是RabbitMQ
具体的概念可以自行访问官网:https://www.rabbitmq.com/#getstarted
简单讲一下RabbitMQ的基本概念,RabbitMQ是目前非常热门的一款消息中间件,它可以用来做消息队列(Message Queue),是一种跨进程、异步的通信机制,用于上下游传递消息。由消息系统来确保消息的可靠传递。它采用AMQP(Advanced Message Queuing Protocol)高级消息队列协议:高级消息队列协议。它是应用层协议的一个开放标准,为面向消息的中间件设计,基于此协议的客户端与消息中间件可传递消息,并不受产品、开发语言等条件的限制。
(2)7种消息模型
重点讲一下里面的7种模型,官网上是英文的,这里直接翻译成中文,7种模型能干嘛里面已经写得很清楚了,看一下就可以了
(3)生产者和消费者模型
Server | 主机地址 |
Virtuol Host | 虚拟主机,这个就像数据库里面的一个一个的库,一个项目对应一个虚拟主机,虚拟主机之间相互隔离 |
Exchange | 交换机,如上面的7种模型,第3、4、5种消息模型中,紫色的那个就是交换机,生产者将数据丢给交换机,再由交换机分发出去 |
Quene | 消息队列,生产者可以直接将数据丢给消息队列,消费者再去队列拿消息,也可以经过交换机将数据丢给消息队列 |
二、RabbitMQ安装
关于安装可以看一下本人的另一篇博文:菜鸟学习Docker实例,里面有RabbitMQ的安装,采用docker一键安装,简单方便
三、创建MQ虚拟主机以及用户
先进入rabbitmq的界面,为项目创建一个虚拟主机
创建用户名和密码
此时创建的用户还没有授权,点击用户进行授权
将创建的虚拟主机授权给用户即可
至此创建MQ虚拟主机以及用户结束。
三、第一种模型-直连
p表示生产者,将数据丢进消息队列,c表示消费者,消费消息队列里面的消息,下面直接进入实战
创建一个maven项目,引入相关的依赖
<?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>com.zsc</groupId>
<artifactId>rabbitmq01</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--rabbitmq依赖-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.2</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
</dependencies>
</project>
创建生产者实体类,并创建一个消息队列,往里面传值
package com.zsc.rabbitmq01;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Provider
* @Description : 消息队列第一种模型:点对点,生成者
* @Author : CJH
* @Date: 2020-09-09 20:10
*/
public class Provider {
private final static String QUEUE_NAME = "hello";
//生产者发送消息
@Test
public void sendMessage() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
/**
* 通道声明
*
* 这里关于参数的说明
* 参数1:队列名称,如果队列不存在则自动创建
* 参数2:用于定义队列特性是否要持久化,true为是,false为否,如果为false,重启mq服务后,里面的消息会被清除,如果为true,队列不会被删除,
* 但消息会被清空,它必须配合下面 channel.basicPublish发布消息时,将第三个参数设置为”MessageProperties.PERSISTENT_TEXT_PLAIN“才能实现队列和消息同时持久化
*
* 参数3:exclusive 表示是否独占队列,true为是,false为否,一般为false,即该通道可以被共享
* 参数4:autoDelete 是否在消费完成后自动删除队列,true为是,false为否
* 参数5:额外的附加参数
*/
/**注意:经常出错的地方,这里的queueDeclare所有的参数设置必须跟消费者的queueDeclare一致,这样才能准确的消费哪一个队列,否则会报错**/
channel.queueDeclare("hello",false,false,false,null);
//发布消息
/**
* 这里关于参数的说明
* 参数1:交换机名称
* 参数2:队列名称
*
* 参数3:传递消息额外设置,配合上面的channel.queueDeclare,设置为MessageProperties.PERSISTENT_TEXT_PLAIN,可实现队列和消息同时持久化
* 参数4:消息的具体内容
*/
channel.basicPublish("","hello", MessageProperties.PERSISTENT_TEXT_PLAIN,"hello world".getBytes());
System.out.println("向hello消息队列发送消息成功");
channel.close();
connection.close();
}
}
创建消费者,只有队列里面有消息就进行消费
package com.zsc.rabbitmq01;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第一种模型:点对点,消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
/**
* 通道声明
*
* 这里关于参数的说明
* 参数1:队列名称,如果队列不存在则自动创建
* 参数2:用于定义队列特性是否要持久化,true为是,false为否,如果为false,重启mq服务后,里面的消息会被清除,如果为true,队列不会被删除,
* 但消息会被清空,它必须配合下面 channel.basicPublish发布消息时,将第三个参数设置为”MessageProperties.PERSISTENT_TEXT_PLAIN“才能实现队列和消息同时持久化
*
* 参数3:exclusive 表示是否独占队列,true为是,false为否,一般为false,即该通道可以被共享
* 参数4:autoDelete 是否在消费完成后自动删除队列,true为是,false为否
* 参数5:额外的附加参数
*/
/**注意:这里必须跟生产者的通道声明一致才不会报错**/
channel.queueDeclare("hello",false,false,false,null);
channel.basicQos(0, 1, false);//这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。
channel.basicConsume("hello",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println(new String(body));
}
});
// channel.close();
// connection.close();
}
}
测试,生产者先往消息队列存放数据,运行3次,存放三条数据
消费者消费队列里面的消息
四、第二种模型-work
第二种模型相较于第一种模型,解决了当消息队列的消息过多的情况,单消费者消费速率有限导致的消息堆积的问题。
生产者的创建,生产者的代码跟上面的第一种模型的代码基本差不多
package com.zsc.rabbitmq02;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Provider
* @Description : 消息队列第一种模型:生成者
* @Author : CJH
* @Date: 2020-09-09 20:10
*/
public class Provider {
//生产者发送消息
@Test
public void sendMessage() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
/**
* 通道声明
*
* 这里关于参数的说明
* 参数1:队列名称,如果队列不存在则自动创建
* 参数2:用于定义队列特性是否要持久化,true为是,false为否,如果为false,重启mq服务后,里面的消息会被清除,如果为true,队列不会被删除,
* 但消息会被清空,它必须配合下面 channel.basicPublish发布消息时,将第三个参数设置为”MessageProperties.PERSISTENT_TEXT_PLAIN“才能实现队列和消息同时持久化
*
* 参数3:exclusive 表示是否独占队列,true为是,false为否,一般为false,即该通道可以被共享
* 参数4:autoDelete 是否在消费完成后自动删除队列,true为是,false为否
* 参数5:额外的附加参数
*/
/**注意:经常出错的地方,这里的queueDeclare所有的参数设置必须跟消费者的queueDeclare一致,这样才能准确的消费哪一个队列,否则会报错**/
channel.queueDeclare("work",true,false,false,null);
//发布消息
/**
* 这里关于参数的说明
* 参数1:交换机名称
* 参数2:队列名称
*
* 参数3:传递消息额外设置,配合上面的channel.queueDeclare,设置为MessageProperties.PERSISTENT_TEXT_PLAIN,可实现队列和消息同时持久化
* 参数4:消息的具体内容
*/
for (int i = 0; i < 100; i++) {
channel.basicPublish("","work", MessageProperties.PERSISTENT_TEXT_PLAIN,("this is "+i).getBytes());
System.out.println("向hello消息队列发送消息成功");
}
channel.close();
connection.close();
}
}
消费者有两个以上,但大部分的代码都是差不多的
消费者1
package com.zsc.rabbitmq02;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第二种模型:消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer1 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
/**
* 通道声明
*
* 这里关于参数的说明
* 参数1:队列名称,如果队列不存在则自动创建
* 参数2:用于定义队列特性是否要持久化,true为是,false为否,如果为false,重启mq服务后,里面的消息会被清除,如果为true,队列不会被删除,
* 但消息会被清空,它必须配合下面 channel.basicPublish发布消息时,将第三个参数设置为”MessageProperties.PERSISTENT_TEXT_PLAIN“才能实现队列和消息同时持久化
*
* 参数3:exclusive 表示是否独占队列,true为是,false为否,一般为false,即该通道可以被共享
* 参数4:autoDelete 是否在消费完成后自动删除队列,true为是,false为否
* 参数5:额外的附加参数
*/
/**注意:这里必须跟生产者的通道声明一致才不会报错**/
channel.queueDeclare("work",true,false,false,null);
//表示通道一次只能消费一个消息
channel.basicQos(1);
/**
* 参数1:队列名称
* 参数2:消息自动确认
* true:表示一接到消息,立刻向mq发送确认消息,不管你消息后,这条消息的有没有执行完,这样会有一个问题,瞬间接收到10条消息,但是只执行了4条消息后,突然宕机了,这样其余没来得及消费的6条消息就会丢失了
* false:表示接到消息后不会立刻发送确认消息,需要人为发送确认消息,mq接到确认消息后才会消除队列里面的消息,这样就可以人为的做到在执行完消息后再发送确认消息,不会造成消息的丢失
*/
channel.basicConsume("work",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1:"+new String(body));
//手动确认消息 参数1:手动确认消息标识 参数2;false 每次确认一个
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// channel.close();
// connection.close();
}
}
消费者2
package com.zsc.rabbitmq02;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第二种模型:消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer2 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定对应消息队列
/**
* 通道声明
*
* 这里关于参数的说明
* 参数1:队列名称,如果队列不存在则自动创建
* 参数2:用于定义队列特性是否要持久化,true为是,false为否,如果为false,重启mq服务后,里面的消息会被清除,如果为true,队列不会被删除,
* 但消息会被清空,它必须配合下面 channel.basicPublish发布消息时,将第三个参数设置为”MessageProperties.PERSISTENT_TEXT_PLAIN“才能实现队列和消息同时持久化
*
* 参数3:exclusive 表示是否独占队列,true为是,false为否,一般为false,即该通道可以被共享
* 参数4:autoDelete 是否在消费完成后自动删除队列,true为是,false为否
* 参数5:额外的附加参数
*/
/**注意:这里必须跟生产者的通道声明一致才不会报错**/
channel.queueDeclare("work",true,false,false,null);
//表示通道一次只能消费一个消息
channel.basicQos(1);
/**
* 参数1:队列名称
* 参数2:消息自动确认
* true:表示一接到消息,立刻向mq发送确认消息,不管你消息后,这条消息的有没有执行完,这样会有一个问题,瞬间接收到10条消息,但是只执行了4条消息后,突然宕机了,这样其余没来得及消费的6条消息就会丢失了
* false:表示接到消息后不会立刻发送确认消息,需要人为发送确认消息,mq接到确认消息后才会消除队列里面的消息,这样就可以人为的做到在执行完消息后再发送确认消息,不会造成消息的丢失
*/
channel.basicConsume("work",false,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者-2:"+new String(body));
//手动确认消息 参数1:手动确认消息标识 参数2;false 每次确认一个
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// channel.close();
// connection.close();
}
}
注意消费者代码里面的消息确认机制,为了实现"能者多劳",改为手动确认消息,同时在代码最后沉睡20秒,这样就可以持续监听队列了,测试的时候先运行两个消费者,再运行生产者,这样生产者一发送消息到队列,在20秒内,消费者监听到队列的消息,就可以进行消息的消费
五、第三种模型-publish
这种模型类似于广播模型,这里将消息全部存入交换机,再由交换机发到各个消息队列,经由消息队列发给各个消费者
这里的生产者需要用到交换机,与交换机进行绑定
package com.zsc.rabbitmq03;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Provider
* @Description : 消息队列第三种模型:广播 生成者
* @Author : CJH
* @Date: 2020-09-09 20:10
*/
public class Provider {
private final static String QUEUE_NAME = "hello";
//生产者发送消息
@Test
public void sendMessage() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//将通道声明为指定交换机 //参数1:交换机名称 参数2:交换机类型 fanout表示广播类型
channel.exchangeDeclare("logs","fanout");
//发送消息
channel.basicPublish("logs","",null,"this is 广播类型".getBytes());
channel.close();
connection.close();
}
}
这里的消费者代码也跟上面的有很大的区别,消费者需要绑定交换机和队列
消费者1
package com.zsc.rabbitmq03;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第三种模型:广播 消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer1 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs","fanout");
//临时队列
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queue,"logs","");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1:" + new String(body));
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者2
package com.zsc.rabbitmq03;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第三种模型:广播 消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer2 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs","fanout");
//临时队列
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列
channel.queueBind(queue,"logs","");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2:" + new String(body));
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行的时候,只要生产者向交换机传递消息,那么交换机就会将消息同时发送给绑定的两个消费者
六、第四种模型-routing
第四种模型,路由模式,相较于第三种模型,功能可以说是在第三种的模型上加了个routingKey,生产者传递消息的时候带上一个routingKey,这样消费者只有带有相同的routingKey才会接收到消息,否则就不会接收到消息
生产者,生产消息时带上了一个routingKey
package com.zsc.rabbitmq04;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Provider
* @Description : 消息队列第四种模型:路由 生成者
* @Author : CJH
* @Date: 2020-09-09 20:10
*/
public class Provider {
private final static String QUEUE_NAME = "hello";
//生产者发送消息
@Test
public void sendMessage() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//将通道声明为指定交换机 //参数1:交换机名称 参数2:交换机类型 direct表示路由模式
channel.exchangeDeclare("logs_direct","direct");
//设置routingKey
String routingKey = "info";
//发送消息
channel.basicPublish("logs_direct",routingKey,null,("this is 路由类型,routingKey为["+routingKey+"]").getBytes());
channel.close();
connection.close();
}
}
消费者也带上一个routingKey,这样只有生产者与消费者的routingKey相同时才会接收到消息
消费者1
package com.zsc.rabbitmq04;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第四种模型:路由 消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer1 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_direct","direct");
//临时队列
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列,设置routingKey为info,这样只要生产者发送的消息带有info的routingKey,它就会接收到
channel.queueBind(queue,"logs_direct","info");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1:" + new String(body));
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者2
package com.zsc.rabbitmq04;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第四种模型:路由 消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer2 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_direct","direct");
//临时队列
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列,设置routingKey为error,这样只要生产者发送的消息带有error的routingKey,它就会接收到
channel.queueBind(queue,"logs_direct","error");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2:" + new String(body));
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试,生产者的routingKey为info
七、第五种模型-topics
第五种模型为动态路由,说白了就是在第四种模型的基础上增加了通配符*和#,它可以做到routingKey的匹配
生产者的代码,将交换机类型改为topic
package com.zsc.rabbitmq05;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Provider
* @Description : 消息队列第五种模型:动态路由 生成者
* @Author : CJH
* @Date: 2020-09-09 20:10
*/
public class Provider {
private final static String QUEUE_NAME = "hello";
//生产者发送消息
@Test
public void sendMessage() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//将通道声明为指定交换机 //参数1:交换机名称 参数2:交换机类型 direct表示路由模式
channel.exchangeDeclare("logs_topic","topic");
//设置routingKey
String routingKey = "user.add";
//发送消息
channel.basicPublish("logs_topic",routingKey,null,("this is 路由类型,routingKey为["+routingKey+"]").getBytes());
channel.close();
connection.close();
}
}
消费者1
package com.zsc.rabbitmq05;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第五种模型:动态路由 消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer1 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_topic","topic");
//临时队列
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列,设置routingKey为多个,这样只要生产者发送的消息带有对应的routingKey,它就会接收到
channel.queueBind(queue,"logs_topic","person.*");
channel.queueBind(queue,"logs_topic","user.*");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-1:" + new String(body));
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
消费者2
package com.zsc.rabbitmq05;
import com.rabbitmq.client.*;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @ClassName : Customer
* @Description : 消息队列第五种模型:动态路由 消费者
* @Author : CJH
* @Date: 2020-09-09 21:57
*/
public class Customer2 {
@Test
public void receive() throws IOException, TimeoutException {
//创建连接mq的工厂对象
ConnectionFactory connectionFactory = new ConnectionFactory();
//设置连接rabbitmq主机
connectionFactory.setHost("39.96.22.34");
//设置端口号
connectionFactory.setPort(5672);//注意端口号是5672不是15672
//设置虚拟主机
connectionFactory.setVirtualHost("/test01");
//设置访问的虚拟主机的用户名和密码
connectionFactory.setUsername("user01");
connectionFactory.setPassword("123");
//获取连接对象
Connection connection = connectionFactory.newConnection();
//获取连接通道
Channel channel = connection.createChannel();
//通道绑定交换机
channel.exchangeDeclare("logs_topic","topic");
//临时队列
String queue = channel.queueDeclare().getQueue();
//绑定交换机和队列,设置routingKey为多个,这样只要生产者发送的消息带有对应的routingKey,它就会接收到
channel.queueBind(queue,"logs_topic","person.*");
channel.queueBind(queue,"logs_topic","student.*");
//消费消息
channel.basicConsume(queue,true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消费者-2:" + new String(body));
}
});
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
由于生产者设置的交换机为topic,且routingKey为user.add,那么消费者1的routingKey有user.*,所以会匹配到,即会接收到消息,而消费者2没有user. *,所以匹配不到,即收不到消息
八、springboot整合rabbitmq
(1)第一种模型(hello)
pom引入坐标依赖,这里贴出本人正在使用的完整项目坐标依赖
<dependencies>
<!--rabbitmq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
整合后需要进行相关配置,这里的ip和用户密码本人给屏蔽了,给了个假的ip和密码
spring:
application:
#随便起个名字
name: springboot_rabbitmq
rabbitmq:
#主机ip
host: 139.86.131.20
#用户名
username: test01
#密码
password: 123456
#虚拟主机
virtual-host: /test01
这里配置好之后,它会自动注入一个模板RabbitTemplate方便我们的操作,注意,如果创建好生产者之后向队列发送消息,如果该队列提前不存在的话,消息会发送失败,所以需要提前创建消费者,让消费者提前创建好队列后,监听队列后,再生产者发送消息,所以先创建消费者
package com.zsc.rabbitmq01;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* @ClassName : Customer
* @Description : 消息队列第一种模型:点对点,消费者
* @Author : CJH
* @Date: 2020-09-13 14:43
*/
@Component//加入容器
//@RabbitListener(queuesToDeclare = @Queue(name = "person",durable = "false"))//durable持久化,除此外@Queue注解还可以配置其他参数
@RabbitListener(queuesToDeclare = @Queue("person"))//如果队列不存在则创建person队列,默认队列是持久化,非独占式的
public class Customer {
//消费者如果监听到消息队列有消息传入,则会自动消费
@RabbitHandler
public void receive(String message){
System.out.println("消费者接收到消息,message: "+message);
}
}
创建生产者,用单元测试的方式创建方便演示
package com.zsc.rabbitmq01;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @ClassName : Provider
* @Description : 消息队列第一种模型:点对点,生成者
* @Author : CJH
* @Date: 2020-09-13 14:39
*/
@SpringBootTest
public class Provider {
//自动配置好的mq模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void test01(){
//向person队列发送信息(注意:此时必须消费者提前创建好该队列)
rabbitTemplate.convertAndSend("person","my name is Tom");
}
}
启动生产者后,因为整个springboot启动,所以消费者会监听队列,然后生产者发送消息,消费者获取消息
(2)第二种模型(work)
生产者
@Component//加入容器
public class Customer2 {
//消费者如果监听到消息队列有消息传入,则会自动消费
@RabbitListener(queuesToDeclare = @Queue("work"))//如果队列不存在则创建person队列,默认队列是持久化,非独占式的
public void receive1(String message){
System.out.println("消费者1接收到消息,message: "+message);
}
//消费者如果监听到消息队列有消息传入,则会自动消费
@RabbitListener(queuesToDeclare = @Queue("work"))//如果队列不存在则创建person队列,默认队列是持久化,非独占式的
public void receive2(String message){
System.out.println("消费者2接收到消息,message: "+message);
}
}
消费者
@SpringBootTest
public class Provider {
//自动配置好的mq模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void test01(){
//向队列发送信息(注意:此时必须消费者提前创建好该队列)
for (int i = 0; i < 10; i++) {
rabbitTemplate.convertAndSend("work","("+i+")第二种模型:work");
}
}
}
注意,这里配置的第二种队列是采用轮询的方式,所有的消息平均分给各个消费者,上面创建了两个消费者,所以它们会各自消费一半的消息,后期可以改为能者多劳"的模式,执行快的,消息执行得多这样的方式
(3)第三种模型(fanout)
第三种模型是交换机的广播模式,先创建消费者,注意这里需要给消费者用注解将交换机和队列进行绑定
@Component//加入容器
public class Customer3 {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//不写名字默认创建临时队列
exchange = @Exchange(value = "school",type = "fanout")//绑定了一个名为school的交换机,同时类型为fanout广播类型
)
})
public void receive1(String message) {
System.out.println("消费者1接收到消息,message: " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//不写名字默认创建临时队列
exchange = @Exchange(value = "school",type = "fanout")//绑定了一个名为school的交换机,同时类型为fanout广播类型
)
})
public void receive2(String message) {
System.out.println("消费者2接收到消息,message: " + message);
}
}
生产者
@SpringBootTest
public class Provider {
//自动配置好的mq模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void test01(){
//向队列发送信息(注意:此时必须消费者提前创建好该队列)
rabbitTemplate.convertAndSend("school","","第三种模型:fanout");
}
}
此时,生产者发送一条数据,所有的消费者都会收到消息
(4)第四种模型
消费者
**@Component//加入容器
public class Customer4 {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//不写名字默认创建临时队列
exchange = @Exchange(value = "theKey",type = "direct"),//绑定了一个名为theKey的交换机,同时类型为direct路由类型
key = {"key1","key2","key3"}
)
})
public void receive1(String message) {
System.out.println("消费者1接收到消息,message: " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//不写名字默认创建临时队列
exchange = @Exchange(value = "theKey",type = "direct"),//绑定了一个名为theKey的交换机,同时类型为direct路由类型
key = {"key1"}
)
})
public void receive2(String message) {
System.out.println("消费者2接收到消息,message: " + message);
}
}**
生产者
@SpringBootTest
public class Provider {
//自动配置好的mq模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void test01(){
//向队列发送信息(注意:此时必须消费者提前创建好该队列)
rabbitTemplate.convertAndSend("theKey","key3","第四种模型:direct");
}
}
生产者发送消息,routingKey为key3,此时,只有具有key3的消费者才能接收到数据
(5)第五种模型(topic)
消费者
@Component//加入容器
public class Customer5 {
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//不写名字默认创建临时队列
exchange = @Exchange(value = "theTopic",type = "topic"),//绑定了一个名为theTopic的交换机,同时类型为topic订阅发布类型
key = {"user.*","person.*"}
)
})
public void receive1(String message) {
System.out.println("消费者1接收到消息,message: " + message);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue,//不写名字默认创建临时队列
exchange = @Exchange(value = "theTopic",type = "topic"),//绑定了一个名为theTopic的交换机,同时类型为topic订阅发布类型
key = {"user.*"}
)
})
public void receive2(String message) {
System.out.println("消费者2接收到消息,message: " + message);
}
}
生产者
@SpringBootTest
public class Provider {
//自动配置好的mq模板
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void test01(){
//向队列发送信息(注意:此时必须消费者提前创建好该队列)
rabbitTemplate.convertAndSend("theTopic","user.add","第五种模型:topic");
}
}
测试
九、rabbitmq集群
前言:这里采用docker来进行rabbitmq集群的搭建,主要是采用”一主二从“的方式进行搭建,即一个主节点,两个从节点
(1)常规集群的搭建
这种常规的集群搭建并不是一种高可用的集群,为什么这么说,可以看一下下面的架构图
一个主节点,两个从节点,消费者均可以从任意节点消费消息,如果主节点添加有新数据,从节点那边都会显示该数据,但只是从节点通过主节点的数据得知有这些数据,所以从表面上来讲数据像是同步的,甚至从节点的数据变化,3个节点都会同步变化,但是一旦主节点挂掉后,从节点相关数据也跟着挂掉。
所以说白了就是,3个节点的数据变化是同步,但是主节点挂掉后,从节点的对应数据也会消失,这种集群不是高可用的,但可以用分担一台服务器上的rabbitmq的压力。
挂载文件准备
下面开始搭建集群,首先需要在服务器上创建3个文件夹用与挂载3个rabbitmq镜像实例的数据
mkdir rabbitmq
cd rabbitmq
mkdir rabbitmq01 rabbitmq02 rabbitmq03
创建的3个文件如下
镜像实例创建
docker run -d --hostname rabbitmq01 --name rabbitmqCluster01 -v $PWD/rabbitmq/rabbitmq01:/var/lib/rabbitmq -p 15672:15672 -p 5672:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' rabbitmq:management
docker run -d --hostname rabbitmq02 --name rabbitmqCluster02 -v $PWD/rabbitmq/rabbitmq02:/var/lib/rabbitmq -p 15673:15672 -p 5673:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' --link rabbitmqCluster01:rabbitmq01 rabbitmq:management
docker run -d --hostname rabbitmq03 --name rabbitmqCluster03 -v $PWD/rabbitmq/rabbitmq03:/var/lib/rabbitmq -p 15674:15672 -p 5674:5672 -e RABBITMQ_ERLANG_COOKIE='rabbitmqCookie' --link rabbitmqCluster01:rabbitmq01 --link rabbitmqCluster02:rabbitmq02 rabbitmq:management
部分参数解释:
这里重点讲一下erlang.cookie,它是erlang实现分布式的必要文件,erlang分布式的每个节点上要保持相同的.erlang.cookie文件,同时保证文件的权限是400。
-e RABBITMQ_ERLANG_COOKIE=‘rabbitmqCookie’ | 这个命令是为了让每个节点上要保持相同的.erlang.cookie文件 |
–link rabbitmqCluster01:rabbitmq01 | docker run --link可以用来链接2个容器,使得源容器(被链接的容器)和接收容器(主动去链接的容器)之间可以互相通信,并且接收容器可以获取源容器的一些数据,如源容器的环境变量 |
创建成功后,已经成功运行起来了
同时可以访问
配置主节点
进入主节点镜像实例: docker exec -it rabbitmqCluster01 bash
进入容器后,分别输入下面命令
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
exit
运行截图
配置第一个从节点
进入主节点镜像实例: docker exec -it rabbitmqCluster02 bash
进入容器后,分别输入下面命令
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq01
rabbitmqctl start_app
exit
运行截图
配置第二个从节点
进入主节点镜像实例: docker exec -it rabbitmqCluster03 bash
进入容器后,分别输入下面命令
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl join_cluster --ram rabbit@rabbitmq01
rabbitmqctl start_app
exit
运行截图
配置完之外,可以登录进主节点看一下rabbitmq,会有从节点信息
集群测试
像主节点发送一条消息,其他从节点也会有相应消息
从节点消费该消息,其他节点的消息也显示被消费了
注意:当向任意一个节点发送消息后,其他的节点都会消息同步,但这种同步不是真正意义上的同步,因为如果那个发送消息的节点挂掉后,其他的节点的对应的这个队列的消息都会挂掉。
挂掉的一个节点如下
(2)搭建高可用的镜像集群
RabbitMQ镜像策略set_policy
#设置策略的格式如下
rabbitmqctl set_policy [-p <vhost>] [--priority <priority>] [--apply-to <apply-to>] <name> <pattern> <definition>
#清除
rabbitmqctl clear_policy [-p <vhost>] <name>
#查看
rabbitmqctl list_policies [-p <vhost>]
部分参数的解读
ha-mode:策略键
1.all 队列镜像在群集中的所有节点上。当新节点添加到群集时,队列将镜像到该节点
2.exactly 集群中的队列实例数。
3.nodes 队列镜像到节点名称中列出的节点。
ha-sync-mode:队列同步
1.manual手动<默认模式>.新的队列镜像将不会收到现有的消息,它只会接收新的消息。
2.automatic自动同步.当一个新镜像加入时,队列会自动同步。队列同步是一个阻塞操作。
具体的可以参考这篇博文:https://www.jianshu.com/p/f81d62a8de02
设置高可用集群
可以通过设置镜像策略来实现集群的高可用,即各个节点的数据只要有更新就在各个节点上复制一份镜像,这样就做到真正意义上的同步,即使其中一个节点挂掉,其他节点照样可以使用
在上面的常规集群的基础上进行改造,进入任意一个节点的容器里面(以第一个主节点为例)
sudo docker exec -it rabbitmqCluster01 bash
输入命令,设置镜像策略
rabbitmqctl set_policy ha-all "^" '{"ha-mode":"all"}'
这个命令的意思是:将所有队列设置为镜像队列,即队列会被同步复制到各个节点,"^“表示匹配所有的镜像队列,如果改为” ^hello"则表示匹配所有以hello结尾的队列,即以hello结尾的队列将会被同步复制到各个节点。
设置完之后的截图
接下来可以通过java向其中一个从节点发送消息,之后可以rabbitmq上面的队列多了+2标志,同时还有ha-all标志
此时向一个从节点发送消息后,再将该节点挂掉,依然能从其他节点消费该消息
注意:假设向从节点1发送消息,当从节点1挂掉后,依然可以从其他节点进行消息消费,但是如果从节点1因为挂掉了所以访问不了,所以当有请求访问从节点1的时候会报错,所以这还不是真正意义上的高可用,真正意义上的高可用应该是配合nginx服务器进行部署,通过代理的方式,将请求合理的分发给各个节点,当有一个节点挂掉后,nginx可以检测到,并且不会向已经挂掉的节点分发请求,而是向其他两个没挂掉的节点分发请求,即做到即使有任一节点挂掉后,也不会影响整个服务的正常运行。