一、消息队列
什么是消息队列呢?
我们之前如果需要进行远程调用,那么一般可以通过发送HTTP请求来完成,而现在,我们可以使用第二种方式,就是消息队列,它能够将发送方发送的信息放入队列中,当新的消息入队时,会通知接收方进行处理,一般消息发送方称为生产者,接收方称为消费者。
BabbitMq,Kafka,RocketMq
-
RabbitMQ - 性能很强,吞吐量很高,支持多种协议,集群化,消息的可靠执行特性等优势,很适合企业的开发。
-
Kafka - 提供了超高的吞吐量,ms级别的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。
-
RocketMQ - 阿里巴巴推出的消息队列,经历过双十一的考验,单机吞吐量高,消息的高可靠性,扩展性强,支持事务等,但是功能不够完整,语言支持性较差。
RabbitMq工作原理介绍
Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker
Virtual host:出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网 络中的 namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个vhost,每个用户在自己的 vhost 创建 exchange/queue 等
Connection:publisher/consumer 和 broker 之间的 TCP 连接
Channel:如果每一次访问 RabbitMQ 都建立一个 Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线 程,通常每个thread创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客户端和 message broker 识别 channel,所以 channel 之间是完全隔离的。Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到 queue 中去。常用的类型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)
Queue:消息最终被送到这里等待 consumer 取走
Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存 到 exchange 中的查询表中,用于 message 的分发依据
二、java实现六种工作模式
1、简单模式
简单的模型:生产者——>消息队列——>消费者
环境依赖:
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.14.2</version> </dependency>
创建生产者:
package org.example.hellodayone; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; public class producer { public static void main(String[] args) throws IOException, TimeoutException { //1、创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2、设置访问ip connectionFactory.setHost("localhost"); connectionFactory.setPort(5672); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); connectionFactory.setVirtualHost("/"); //3、创建connection连接对象 Connection connection = connectionFactory.newConnection(); //4、创建通道 Channel channel = connection.createChannel(); //5、设置对列属性 /** * 第一个参数:队列名称 * 第二个参数:队列是否要持久化 * 第三个参数:是否排他性 * 第四个参数:是否自动删除 * 第五个参数:是否要设置些额外参数 */ channel.queueDeclare("01-hello",false,false,false,null); //6、设置交换机属性 /** * 第一个参数:交换机名称 * 第二个参数:交换机类型:direct(直连) fanout(扇形) topic(主体) header(头) * 第三个参数:是否需要设置额外参数 */ //channel.exchangeDeclare("01-hello-exchange","direct",false); //7、将队列绑定到交换机 /** * - queue:需要绑定的队列名称。 * - exchange:需要绑定的交换机名称。 * - routingKey:不用多说了吧。 */ //channel.queueBind("01-hello","01-hello-exchange","01-hello-channel"); //8、发送消息 /** * 第一个参数:交换机名称 * 第二个参数:路由key * 第三个参数:消息属性 * 第四个参数:消息内容 */ String message = "hello rabbitmq,this is my first"; channel.basicPublish("","01-hello",null,message.getBytes()); //9、关闭资源 channel.close(); connection.close(); } }
其中queueDeclare方法的参数如下:
-
queue:队列的名称(默认创建后routingKey和队列名称一致)
-
durable:是否持久化。
-
exclusive:是否排他,如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。排他队列是基于Connection可见,同一个Connection的不同Channel是可以同时访问同一个连接创建的排他队列,并且,如果一个Connection已经声明了一个排他队列,其他的Connection是不允许建立同名的排他队列的,即使该队列是持久化的,一旦Connection关闭或者客户端退出,该排他队列都会自动被删除。
-
autoDelete:是否自动删除。
-
arguments:设置队列的其他一些参数,这里我们暂时不需要什么其他参数。
其中queueBind方法参数如下:
-
queue:需要绑定的队列名称。
-
exchange:需要绑定的交换机名称。
-
routingKey:不用多说了吧。
其中basicPublish方法的参数如下:
-
exchange: 对应的Exchange名称,我们这里就使用第二个直连交换机。
-
routingKey:这里我们填写绑定时指定的routingKey,其实和之前在管理页面操作一样。
-
props:其他的配置。
-
body:消息本体
消费者:
package org.example.hellodayone; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Consumer { public static void main(String[] args) throws IOException, TimeoutException { //1、创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2、设置访问ip connectionFactory.setHost("localhost"); connectionFactory.setPort(5672); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); connectionFactory.setVirtualHost("/"); //3、创建connection连接对象 Connection connection = connectionFactory.newConnection(); //4、创建通道 Channel channel = connection.createChannel(); //5、设置对列属性 /** * 第一个参数:队列名称 * 第二个参数:队列是否要持久化 * 第三个参数:是否排他性 * 第四个参数:是否自动删除 * 第五个参数:是否要设置些额外参数 */ channel.queueDeclare("01-hello",false,false,false,null); //6、使用channel去rabbitmq中进行消费 /** * 第一个参数:队列名称 * 第二个参数:是否自动签收 */ channel.basicConsume("01-hello", true, new DeliverCallback() { /** * 当消息从mq中取出来后回调这个函数,消费者消费消息就在handler中进行处理 * @param s * @param delivery * @throws IOException */ @Override public void handle(String s, Delivery delivery) throws IOException { System.out.println("从生产者获取到的消息是:" + new String(delivery.getBody())); /**basicAck是确认应答 * 第一个参数:当前的消息标签 * 第二个参数:是否批量处理队列中所有信息,为false,则只处理当前消息 */ channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); /**basicNack是拒绝应答 * 最后一个参数表示是否将当前消息放回队列,如果为false,那么消息就会被丢弃 */ //channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false); } }, new CancelCallback() { /** * 当消息取消会调用这个handler方法 * @param s * @throws IOException */ @Override public void handle(String s) throws IOException { System.out.println("消息已经取消"); } }); } }
访问的路径:http://127.0.0.1:15672/
2、work工作队列模式
(1)当队列里消息较多时,我们通常会开启多个消费者处理消息;公平分发和轮询分发都是我们经常使用的模式。
(2)轮询分发的主要思想是“按均分配”,不考虑消费者的处理能力,所有消费者均分;这种情况下,处理能力弱的服务器,一直都在处理消息,而处理能力强的服务器,在处理完消息后,处于空闲状态;
(3)公平分发的主要思想是”能者多劳”,按需分配,能力强的干的多。
1、按均分配:
生产者:
package org.example.work_02; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class Workproducer { public static void main(String[] args) throws IOException, TimeoutException { //1、创建工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("localhost");//设置访问ip //2、创建连接对象 Connection connection = connectionFactory.newConnection(); //3、创建channel连接通道 Channel channel = connection.createChannel(); //4、设置队列属性 channel.queueDeclare("02-work-queue",false,false,false,null); /** * work队列模式是不涉及交换机的,exchange,所以是由队列直接通过channel通道传递给消费者 */ //5、发送消息 for (int i = 0; i < 20; i++) { String message = "hello rabbitmq_" + i; channel.basicPublish("","02-work-queue",null,message.getBytes()); } } }
消费者:
package org.example.work_02; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class WorkConsumer01 { public static void main(String[] args) throws IOException, TimeoutException { //1、创建工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); connectionFactory.setHost("localhost");//设置访问ip //2、创建连接对象 Connection connection = connectionFactory.newConnection(); //3、创建channel连接通道 Channel channel = connection.createChannel(); //4、设置队列属性 channel.queueDeclare("02-work-queue",false,false,false,null); //5、消费者进行消费 channel.basicConsume("02-work-queue", true, new DeliverCallback() { //消费者消费消息会回调这个handler函数 @Override public void handle(String s, Delivery delivery) throws IOException { try { Thread.sleep(1000); System.out.println("消费者1获得的信息为:" + new String(delivery.getBody())); //channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false); } catch (InterruptedException e) { throw new RuntimeException(e); } } }, new CancelCallback() { //消息取消了会回调这个函数 @Override public void handle(String s) throws IOException { System.out.println("消息已经取消了"); } }); } }
测试结果:
2、按劳分配:
-
设置通道channel.basicQos(1); //只限一条消息给同一个消费者,消息处理完后,才发送第二条消息
-
需要将basicConsume的第二个参数,autoAck改为手动签收,也就是参数设置为false
-
在DriverCallBack中的handler1方法中添加
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
测试结果:
3、pub/sub发布订阅模式
业务场景:发送消息,或者是发送邮件的时候(群发,保证接受的所有对象都能收倒消息)。
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
C:消费者,消息的接收者,会一直等待消息到来
Queue:消息队列,接收消息、缓存消息
Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、
递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
➢ Fanout:广播,将消息交给所有绑定到交换机的队列
➢ Direct:定向,把消息交给符合指定routing key 的队列
➢ Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与 Exchange 绑定,或者没有符合路由规则的队列,那么消息会丢失!
生产者
package org.example._03_pubsub; import com.rabbitmq.client.Channel; import com.rabbitmq.client.Connection; import com.rabbitmq.client.ConnectionFactory; import java.io.IOException; import java.util.concurrent.TimeoutException; public class PubSubProducer { public static void main(String[] args) throws IOException, TimeoutException { //1、创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2、设置访问ip connectionFactory.setHost("localhost"); connectionFactory.setPort(5672); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); connectionFactory.setVirtualHost("/"); //3、创建connection连接对象 Connection connection = connectionFactory.newConnection(); //4、创建通道 Channel channel = connection.createChannel(); //在发布订阅模式当中,生产者是直接连接的交换机,然后,消费者通过channel与交换机连接,通过匿名队列进行消费 //6、设置交换机属性 /** * 第一个参数:交换机名称 * 第二个参数:交换机类型:direct(直连) fanout(扇形) topic(主体) header(头) * 第三个参数:是否需要设置额外参数 */ channel.exchangeDeclare("03-pubsub-exchange","fanout",false); //8、发送消息 /** * 第一个参数:交换机名称 * 第二个参数:路由key * 第三个参数:消息属性 * 第四个参数:消息内容 */ for (int i = 0; i < 10; i++) { String message = "hello rabbitmq,this is publish/subscribe model --" + i; channel.basicPublish("03-pubsub-exchange","",null,message.getBytes());//发布订阅模式是直接发送到交换机上的 } //9、关闭资源 channel.close(); connection.close(); } }
消费者:
package org.example._03_pubsub; import com.rabbitmq.client.*; import java.io.IOException; import java.util.concurrent.TimeoutException; public class PubSubConsumer { public static void main(String[] args) throws IOException, TimeoutException { //1、创建连接工厂 ConnectionFactory connectionFactory = new ConnectionFactory(); //2、设置访问ip connectionFactory.setHost("localhost"); connectionFactory.setPort(5672); connectionFactory.setUsername("guest"); connectionFactory.setPassword("guest"); connectionFactory.setVirtualHost("/"); //3、创建connection连接对象 Connection connection = connectionFactory.newConnection(); //4、创建通道 Channel channel = connection.createChannel(); //5、设置交换机属性 /** * 参数一:交换机命名 * 参数二:交换机类型 * 参数三:额外的参数设置 */ channel.exchangeDeclare("03-pubsub-exchange","fanout"); //6、交换机绑定匿名队列 /** * 参数一:队列名 * 参数二:交换机名 * 参数三:routing key */ String queue = channel.queueDeclare().getQueue(); channel.queueBind(queue,"03-pubsub-exchange",""); //6、使用channel去rabbitmq中进行消费 /** * 第一个参数:队列名称 * 第二个参数:是否自动签收 */ channel.basicConsume(queue, true, new DeliverCallback() { /** * 当消息从mq中取出来后回调这个函数,消费者消费消息就在handler中进行处理 * @param s * @param delivery * @throws IOException */ @Override public void handle(String s, Delivery delivery) throws IOException { System.out.println("消费者1接收到的消息:" + new String(delivery.getBody())); /**basicAck是确认应答 * 第一个参数:当前的消息标签 * 第二个参数:是否批量处理队列中所有信息,为false,则只处理当前消息 */ channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); /**basicNack是拒绝应答 * 最后一个参数表示是否将当前消息放回队列,如果为false,那么消息就会被丢弃 */ //channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, false); } }, new CancelCallback() { /** * 当消息取消会调用这个handler方法 * @param s * @throws IOException */ @Override public void handle(String s) throws IOException { System.out.println("消息已经取消"); } }); } }
4、routing路由模式
队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由key)
消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey
Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的 Routingkey 与消息的 Routing key 完全一致,才会接收到消息
生产者:
//6、设置交换机属性 /** * 第一个参数:交换机名称 * 第二个参数:交换机类型:direct(直连) fanout(扇形) topic(主体) header(头) * 第三个参数:是否需要设置额外参数 */ //在routing模式下,这里的交换机类型是direct channel.exchangeDeclare("04-routing-exchange","direct",false); //8、发送消息 /** * 第一个参数:交换机名称 * 第二个参数:路由key * 第三个参数:消息属性 * 第四个参数:消息内容 */ //这里设置routing key 为info String message = "hello rabbitmq,this is routing model" ; String routingKey = "info"; channel.basicPublish("04-routing-exchange",routingKey,null,message.getBytes());//发布订阅模式是直接发送到交换机上的
消费者:
//5、设置交换机属性 /** * 参数一:交换机命名 * 参数二:交换机类型 * 参数三:额外的参数设置 */ channel.exchangeDeclare("04-routing-exchange","direct"); //6、交换机绑定匿名队列 /** * 参数一:队列名 * 参数二:交换机名 * 参数三:routing key */ //这里设置了一个绑定队列,但是分别有三个不同的bind虚拟连接 error、info、warning 连接绑定队列 String queue = channel.queueDeclare().getQueue(); channel.queueBind(queue,"04-routing-exchange","error"); channel.queueBind(queue,"04-routing-exchange","info"); channel.queueBind(queue,"04-routing-exchange","warning");
5、topic模式
Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型
Exchange 可以让队列在绑定 Routing key 的时候使用通配符!
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert
图解:
红色 Queue:绑定的是 usa.# ,因此凡是以 usa. 开头的 routing key 都会被匹配到
黄色 Queue:绑定的是 #.news ,因此凡是以 .news 结尾的 routing key 都会被匹配
生产者:
//6、设置交换机属性 /** * 第一个参数:交换机名称 * 第二个参数:交换机类型:direct(直连) fanout(扇形) topic(主题) header(头) * 第三个参数:是否需要设置额外参数 */ channel.exchangeDeclare("05-topic-exchange","topic"); //8、发送消息 /** * 第一个参数:交换机名称 * 第二个参数:路由key * 第三个参数:消息属性 * 第四个参数:消息内容 */ //这里设置routing key 为info String message = "hello rabbitmq,this is topic model" ; channel.basicPublish("05-topic-exchange","user.delete",null,message.getBytes());//发布订阅模式是直接发送到交换机上的
消费者:
//5、设置交换机属性 /** * 参数一:交换机命名 * 参数二:交换机类型 * 参数三:额外的参数设置 */ channel.exchangeDeclare("05-topic-exchange","topic"); //6、交换机绑定匿名队列 /** * 参数一:队列名 * 参数二:交换机名 * 参数三:routing key */ //设置routing key 为employee String queue = channel.queueDeclare().getQueue(); channel.queueBind(queue,"05-topic-exchange","employee.*");