Linux中安装RabbitMQ
Linux环境搭建Rabbitmq集群
Rabbitmq常用命令
为什么要用消息队列+各个消息队列框架该如何选择?
所有 MQ 产品从模型抽象上来说都是一样的过程:
消费者(consumer)订阅某个队列。生产者(pr‘’oducer)创建消息,然后发布到队列(queue)中,最后将消息发送到监听的消费者。
不同的MQ产品有不同的机制,RabbitMQ实际基于AMQP协议的一个开源实现,因此RabbitMQ内部也是AMQP的基本概念。
1、Message
消息,消息是不具体的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
那么谁应该负责创建这个queue呢?是Consumer,还是Producer?
如果queue不存在,当然Consumer不会得到任何的Message。但是如果queue不存在,那么Producer Publish的Message会被丢弃。所以,还是为了数据不丢失,Consumer和Producer都try to create the queue!反正不管怎么样,这个接口都不会出问题。
6、Connection
网络连接,比如一个TCP连接。
7、Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
8、Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker
表示消息队列服务器实体。
对于基本消息模型,我们可以只做一个入门概念的了解,它并没有使用交换器和消息绑定机制,消费者生产消息放到队列中,即可进行自己后续的操作,队列中的消息会被消费者监听到,消费者监听到有消息存在后,变会获取消息执行任务。
P(producer/ publisher):生产者,一个发送消息的用户应用程序。
C(consumer):消费者,消费和接收有类似的意思,消费者是一个主要用来等待接收消息的用户应用程序。
队列(红色区域):rabbitmq内部类似于邮箱的一个概念。虽然消息流经rabbitmq和你的应用程序,但是它们只能存储在队列中。队列只受主机的内存和磁盘限制,实质上是一个大的消息缓冲区。许多生产者可以发送消息到一个队列,许多消费者可以尝试从一个队列接收数据。
想要使用rabbitmq,必须先引入先关依赖,spring提供了spring-boot-starter-amqp依赖,只需要简单的配置即可与spring无缝整合。
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
建立RabbitMQ的连接类
public class ConnectionUtil {
/**
* 建立与RabbitMQ的连接
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//定义连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置服务地址
factory.setHost("192.168.237.139");
//端口
factory.setPort(5672);
//设置账号信息,用户名、密码、vhost
factory.setVirtualHost("/");
factory.setUsername("zhy");
factory.setPassword("zhy");
// 通过工程获取连接
Connection connection = factory.newConnection();
return connection;
}
}
生产者:
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 从连接中创建通道,使用通道才能完成消息相关的操作
Channel channel = connection.createChannel();
// 声明(创建)队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 消息内容
String message = "Hello World!";
// 向指定的队列中发送消息
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
//关闭通道和连接
channel.close();
connection.close();
}
消费者1:
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
//int a=1/0;
String msg = new String(body);
System.out.println(" [x] received : " + msg + "!");
}
};
// 监听队列,第二个参数:是否自动进行消息确认,这里选择true,即只要拿到消息就立即确认,从消息队列中移除。
channel.basicConsume(QUEUE_NAME, true, consumer);
}
上面这种监听队列模式,存在一定的弊端,比如,消费者在获得消息,进行任务处理的时候服务器异常,但是这时候消息队列中的消息已经被确认消费,消息队列中已经没有了这条消息,这将导致消息丢失的现象。
比如我们在消费者代码中写入除零异常:int a=1/0;
消费之前队列内容:
执行消费后,虽然代码报异常,但是消息仍然被消费:
解决方式:
我们可以不进行自动确认,即自己进行手动ack确认,如消费者2:
需要添加代码:
channel.basicAck(envelope.getDeliveryTag(), false);
消费者2:
private final static String QUEUE_NAME = "simple_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 创建通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
int i=1/0;
String msg = new String(body);
System.out.println(" [x] received : " + msg + "!");
// 手动进行ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列,第二个参数false,手动进行ACK
channel.basicConsume(QUEUE_NAME, false, consumer);
}
我们执行消费者2代码,消息没有被消费时,消息队列中有内容:
执行消费者代码后,报异常,但是消息队列中的消息并没有被消费:
P:生产者:任务的发布者
C1:消费者,领取任务并且完成任务,假设完成速度较快
C2:消费者2:领取任务并完成任务,假设完成速度慢
工作队列,又称任务队列。主要思想就是避免执行资源密集型任务时,必须等待它执行完成。
对于资源密集型任务,我们可以设置多个消费者来监听同一个队列。并且当多个消费者消费消息的速度不一致时,我们开可以开启能者多劳模式,即消费速度快的多消费,消费速度慢的少消费。这样来提高资源密集型任务的执行效率,避免消息堆积。
应用场景:秒杀、抢红包。
在生产者中同时生产50个任务,两个消费者进行消费,其中一个消费者做睡眠处理,模拟消费时出现卡顿的现象(保证消费者先启动,然后再启动生产者),运行结果会发现,两个消费者是轮询进行的,没有被设置卡顿的消费者很快轮询结束自己的任务,但是被设置睡眠的消费者,还在一点一点的执行着自己的任务。
生产者
// 生产者
public class Send {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 循环发布任务
for (int i = 0; i < 50; i++) {
// 消息内容
String message = "task .. " + i;
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println(" [x] Sent '" + message + "'");
Thread.sleep(i * 2);
}
// 关闭通道和连接
channel.close();
connection.close();
}
}
消费者1
// 消费者1
public class Recv {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者1] received : " + msg + "!");
try {
// 模拟完成任务的耗时:1000ms
Thread.sleep(1000);
} catch (InterruptedException e) {
}
// 手动ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列。
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
消费者2
//消费者2
public class Recv2 {
private final static String QUEUE_NAME = "test_work_queue";
public static void main(String[] argv) throws Exception {
// 获取到连接
Connection connection = ConnectionUtil.getConnection();
// 获取通道
final Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
// 定义队列的消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
// 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
byte[] body) throws IOException {
// body 即消息体
String msg = new String(body);
System.out.println(" [消费者2] received : " + msg + "!");
// 手动ACK
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
// 监听队列。
channel.basicConsume(QUEUE_NAME, false, consumer);
}
}
运行结果:
结果中,两个消费者各自消费了25个任务,实现了任务的分发,当消费者2已经消费结束后,消费者1由于设置了睡眠时间,导致消费时间很慢。
为了解决这个问题,我们可以开启能者多劳,使用basicQos方法和prefetchCount = 1设置。 这告诉RabbitMQ一次不要向工作人员发送多于一条消息。 或者换句话说,不要向工作人员发送新消息,直到它处理并确认了前一个消息。 相反,它会将其分派给不是仍然忙碌的下一个工作人员。
在两个消费者中加上如下代码:
// 设置每个消费者同时只能处理一条消息
channel.basicQos(1);
重新运行消费者和生产者:
结果中,消费比较快的消费者多消费,体现能者多劳。
注意:
1、Queue的消息只能被同一个消费者消费,如果没有消费监听队列那么消息会存放到队列中持久化保存,直到有消费者来消费这个消息,如果以有消费者监听队列则立即消费发送到队列中的消息.
2、Queue的消息可以保证每个消息都一定能被消费.
在说订阅模型之前我们先来了解一下AMQP中的消息路由。
RabbitMQ是基于AMQP协议的一个开源实现,AMQP 中增加了 Exchange 和 Binding 的角色。生产者把消息发布到 Exchange 上,消息最终到达队列并被消费者接收,而 Binding 决定交换器的消息应该发送到那个队列。
Spring最擅长的事情就是封装,把他人的框架进行封装和整合。
Spring为AMQP提供了统一的消息处理模板:AmqpTemplate。
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers 。headers 匹配 AMQP 消息的 header 而不是路由键,此外 headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到了,所以直接看另外三种类型。
消息中的路由键(routing key)如果和 Binding 中的 binding key 一致, 交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如一个队列绑定到交换机要求路由键为“dog”,则只转发 routing key 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard”等等。它是完全匹配、单播的模式。消费者只需监听某个队列以后,就会获取队列中的消息。
关于订阅模型的实例我们通过构建两个工程来实现,一个消费者工程,一个生产者工程。
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
编写配置文件
#配置rabbitmq的相关链接信息(单机版)
spring.rabbitmq.host=192.168.237.135
spring.rabbitmq.port=5672
spring.rabbitmq.username=zhy
spring.rabbitmq.password=zhy
编写一个 config文件类,用来向spring容器中注入交换机、队列、和交换机与队列的绑定
@Configuration
public class RabbitmqConfig {
//配置一个Direct类型的交换机
@Bean
public DirectExchange directExchange(){
return new DirectExchange("bootDirectExchange");
}
//配置一个队列
@Bean
public Queue directQueue() {
return new Queue("bootDirectQueue");
}
/**
* 配置一个队列和交换机绑定
* @param directQueue 需要绑定的队列对象,参数名必须要与某个@Bean的方法名完全相同保证自动注入
* @param directExchange 需要绑定的交换机的对象,参数名必须要与某个@Bean的方法名完全相同保证自动注入
* @return
*/
@Bean
public Binding directBinding(Queue directQueue,DirectExchange directExchange){
// 完成绑定
return BindingBuilder.bind(directQueue).to(directExchange).with("bootDirectRoutingKey");
}
}
编写service接口
public interface SendService {
void sendMessage(String message);
}
编写service实现类
@Service("sendService")
public class SendServiceImpl implements SendService {
// 注入amqp模板类,利用这个对象来发送和接受消息
@Resource
private AmqpTemplate amqpTemplate;
@Override
public void sendMessage(String message){
amqpTemplate.convertAndSend("bootDirectExchange","bootDirectRoutingKey",message);
}
}
启动类
@SpringBootApplication
public class RabbitmqSendBootApplication {
public static void main(String[] args) {
ApplicationContext ac= SpringApplication.run(RabbitmqSendBootApplication.class, args);
SendService service= (SendService) ac.getBean("sendService");
service.sendMessage("boot测试数据");
}
}
同样导入依赖、编写配置文件、编写一个 config文件类,代码与生产者工程代码相同,这里不再重复。
编写 接口
public interface ReceiveService {
void receive();
}
接口实现类
@Service("receiveService")
public class ReceiveServiceImpl implements ReceiveService {
// 注入amqp模板类,利用这个对象来发送和接受消息
@Resource
private AmqpTemplate amqpTemplate;
// 不是不间断的接收消息,每执行一次这个方法只能接收一次消息,如果有新的消息进入则不会自动接收消息
@Override
public void receive() {
String message= (String) amqpTemplate.receiveAndConvert("bootDirectQueue");
System.out.println(message);
}
启动类
@SpringBootApplication
public class RabbitmqReceiveBootApplication {
public static void main(String[] args) {
ApplicationContext ac= SpringApplication.run(RabbitmqReceiveBootApplication.class, args);
ReceiveService service= (ReceiveService) ac.getBean("receiveService");
service.receive();
}
}
分别启动生产者和消费者,我们将看到消费者能够拿到队列中的消息:
但是这种写法并不完善,因为消费者此时并不能不间断的接收消息,每次执行一次receive这个方法,只能接收一次消息,如果有新的消息进入则不会自动接收消息。
比如生产者我们进行多条消息发送:
重新启动生产者和消费者,发现队列中只被消费了一条信息:
消费者只拿到一条数据:
为了解决这个问题,我们可以在方法上添加一个监听的注解@RabbitListener,用来持续性的自动接收消息。
// 不是不间断的接收消息,没执行一次这个方法只能接收一次消息,如果有新的消息进入则不会自动接收消息
// @Override
// public void receive() {
// String message= (String) amqpTemplate.receiveAndConvert("bootDirectQueue");
// System.out.println(message);
// }
@RabbitListener(queues = {"bootDirectQueue"})
/**
* 该注解用于标记当前方法是一个rabbitmq的消息监听方法,作用是持续性的自动接收消息,不需要手动调用spring会自动运行监听
* 参数queues用于指定一个 已经存在的队列名,用于进行队列的监听
* 注意:如果当前监听方法正常结束spring就会自动确认消息,如果出现异常则不会确认消息
* 因此在消息处理时我们需要做好消息的防止重复处理工作
*/
public void directReceive(String message){
System.out.println("监听器接收的消息---"+message);
}
注意:
1、使用direct消息模式时必须要指定RoutingKey(路由键),将指定的消息绑定到指定的路由键上
2、使用Exchange的direct模式时接收者的RoutingKey必须要与发送时的RoutingKey完全一致否则无法获取消息
3、接收消息时队列名也必须要和发送消息时的完全一致
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。fanout 交换器不处理路由键,没有Routingkey以及Bindingkey的概念,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout 类型转发消息是最快的。
缺点:
必须现在消费中监听队列,否则如果消息先发送了,那么消费者永远错过消息。
优点:
速度最快。
我们改造一下上面的工程
在confing文件类中添加配置一个fanout类型的交换机
/**
* 配置一个fanout类型的交换机
* @return
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanoutExchange");
}
service接口和实现类中添加发送fanoutmessage的方法 :
public void sendFanoutmessage(String message){
amqpTemplate.convertAndSend("fanoutExchange","",message);
}
启动类中调用该方法,注解掉原来的方法调用避免出现混淆:
// for (int i = 0; i < 10; i++) {
// service.sendMessage("boot测试数据 "+i);
// }
service.sendFanoutmessage("boot的fandout测试数据");
在service类中添加两个fanout的消费者方法(为了避免混淆可以将 原来的 方法注释掉):
@RabbitListener(bindings = {
// @QueueBinding注解要完成队列和交换机
@QueueBinding(value = @Queue(),//@Queue创建一个队列(没有指定参数则表示创建一个随机队列)
exchange = @Exchange(name = "fanoutExchange",type = "fanout") //创建一个交换机
)})
public void fanoutReceive01(String message){
System.out.println("fanoutReceive01监听器接收的消息---"+message);
}
@RabbitListener(bindings = {
// @QueueBinding注解要完成队列和交换机
@QueueBinding(value = @Queue(),//@Queue创建一个队列(没有指定参数则表示创建一个随机队列)
exchange = @Exchange(name = "fanoutExchange",type = "fanout") //创建一个交换机
)})
public void fanoutReceive02(String message){
System.out.println("fanoutReceive02监听器接收的消息---"+message);
}
先启动消费者进行监听,后启动生产者工程:
查看rabbitmq中有fanout交换机的存在
消费者进程中,两个消费方法都拿到了消息:
1、fanout模式的消息需要将一个消息同时绑定到多个队列中因此这里不能创建并指定某个队列
2、使用fanout模式获取消息时不需要绑定特定的队列名称,获取一个随机的队列名称,然后绑定到指定的Exchange即可获取消息。
3、这种模式中可以同时启动多个接收者只要都绑定到同一个Exchang即可让所有接收者同时接收同一个消息是一种广播的消息机制。
4、必须现在消费中监听队列,即消费者先启动,否则如果消息先发送了,那么消费者永远错过消息。
topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,“*”匹配不多不少一个单词。
topic交换机基本概念和使用与fanout相同,但是需要使用bindingkey。
topic也会丢失消息,所以需要先启动消费者监听。
topic更适合不同的功能模块来接收同一个消息,如订单成功。
AMQP 协议中的核心思想就是生产者和消费者的解耦,生产者从不直接将消息发送给队列。生产者通常不知道是否一个消息会被发送到队列中,只是将消息发送到一个交换机。先由 Exchange 来接收,然后 Exchange 按照特定的策略转发到 Queue 进行存储。Exchange 就类似于一个交换机,将各个消息分发到相应的队列中。
在实际应用中我们只需要定义好 Exchange 的路由策略,而生产者则不需要关心消息会发送到哪个 Queue 或被哪些 Consumer 消费。在这种模式下生产者只面向 Exchange 发布消息,消费者只面向 Queue 消费消息,Exchange 定义了消息路由到 Queue 的规则,将各个层面的消息传递隔离开,使每一层只需要关心自己面向的下一层,降低了整体的耦合度。
改动上面的工程
在config文件类中添加topic类型的 交换机(为了避免混淆,可以注释掉原来的交换机)
/**
* 配置一个Topic类型的交换机
* @return
*/
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
在service接口和实现类中,添加 发送topicmessage消息的方法:
public void sendTopicmessage(String message){
amqpTemplate.convertAndSend("topicExchange","aa.bb",message);
}
启动类中调用发送消息的方法
// for (int i = 0; i < 10; i++) {
// service.sendMessage("boot测试数据 "+i);
// }
// service.sendFanoutmessage("boot的fandout测试数据");
service.sendTopicmessage("boot的topic测试数据key 为 aa.bb");
在service类中,添加三个监听topic类型交换机的方法,并分别为其指定路由键的匹配规则
@RabbitListener(bindings = {
// @QueueBinding注解要完成队列和交换机
@QueueBinding(value = @Queue("topic01"),//@Queue创建一个队列(没有指定参数则表示创建一个随机队列)
key = {"aa"},
exchange = @Exchange(name = "topicExchange",type = "topic") //创建一个交换机
)})
public void topicReceive01(String message){
System.out.println("topic01消费者 ---aa---"+message);
}
@RabbitListener(bindings = {
// @QueueBinding注解要完成队列和交换机
@QueueBinding(value = @Queue("topic02"),//@Queue创建一个队列(没有指定参数则表示创建一个随机队列)
key = {"aa.*"},
exchange = @Exchange(name = "topicExchange",type = "topic") //创建一个交换机
)})
public void topicReceive02(String message){
System.out.println("topic02消费者 ---aa.*---"+message);
}
@RabbitListener(bindings = {
// @QueueBinding注解要完成队列和交换机
@QueueBinding(value = @Queue("topic03"),//@Queue创建一个队列(没有指定参数则表示创建一个随机队列)
key = {"aa.#"},
exchange = @Exchange(name = "topicExchange",type = "topic") //创建一个交换机
)})
public void topicReceive03(String message){
System.out.println("topic03消费者 ---aa.#---"+message);
}
先启动消费者进程,进行监听,在启动生产者进程:
1、在topic模式中必须要指定Routingkey,并且可以同时指定多层的RoutingKey,每个层次之间使用 点分隔即可 例如 test.myRoutingKey
2、Topic模式的消息接收时必须要指定RoutingKey并且可以使用# 和 *来做统配符号,#表示通配任意一个单词 *表示通配任意多个单词,例如消费者的RoutingKey为test.#或#.myRoutingKey都可以获取RoutingKey为test.myRoutingKey发送者发送的消息。