目录
MQ
什么是mq? MQ (MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。百度上对消息队列的解释是:消息(Message)是指在应用之间传送的数据,消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。
消息队列(Message Queue)是一种应用间的通信方式,消息发送后可以立即返回,有消息系统来确保信息的可靠专递,消息发布者只管把消息发布到MQ中而不管谁来取,消息使用者只管从MQ中取消息而不管谁发布的,这样发布者和使用者都不用知道对方的存在。
那为什么会需要用到这种消息队列呢?
同步
可以想一下同步是不是有点类似于我们打电话的时候,我们拨出了电话,就必须要等着,等另一方也接通。对应的微服务中Feigen的调用也是属于同步调用。
但是同步调用这样的方式存在很大的缺点:
- 耦合度很高。每次加入新的需求,都要修改原来的代码
- 性能严重降低。调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和
- 浪费资源。调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
- 会发生联级失败。如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障
所以出现了我们的异步调用方式
异步
异步调用就类似于我们发消息,消息发送方发送了消息以后就什么都不用管了。异步调用也常见于订单服务,当某个用户下了订单以后,需要把订单发给仓库中心,或者通过短信的方式发给用户确认,这些都是典型的异步调用。
相对于同步调用来说,异步调用确实有很多优点,但它没什么时效性,所以总体来说还得根据我们的业务场景来选择。
而RabbitMQ就是实现了这么一个功能的框架
RabbitMQ
概念
RabbitMQ中有五种最经典的消息模型:
基本消息队列(BasicQueue)
工作消息队列(WorkQueue)
广播模型(Fanout Exchange)
路由模型(Direct Exchange)
主题模型(Topic Exchange)
上面的图中:
- 蓝色的椭圆p即Publisher,消息发送者
- 蓝色的椭圆c即Consumer,消息消费者
- 红色的矩形q即Queue,消息队列,用来存储消息
- 紫色的椭圆X,则表示路由,它并不存储消息。而是当我们有多个消息队列queue时,它就决定了消息是路由到哪一个消息队列
上面这五种消息模型很重要,概念一定要好好理解!!!图也要牢记于心,有助于我们理解下面的代码,当然下面的代码我也会给上详细的注释!!!
案例一:Basic Queue
在使用RabbitMQ之前,记得导包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
消息发送者
@SpringBootTest
public class Publisher {
@Test
public void test()throws Exception{
ConnectionFactory factory = new ConnectionFactory();
//注意,这里的账号密码并不是登录管理端的账号密码哦,而是在管理端创建的用户的用户名和密码
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");//主机ip地址,这里我就不写了
factory.setPort(5672);//主机端口号
factory.setVirtualHost("/");//相当于mysql中的一个数据库,需要去RabbitMQ查看,如果不存在会报错
Connection connection = factory.newConnection();
System.out.println(connection);
//创建并绑定队列
Channel channel = connection.createChannel();
//参数1:队列名称,如果队列不存在则自动创建
//参数2:队列是否持久化。true表示持久化,false表示不持久化。
//参数3:是否独占队列。true表示独占,false表示不独占。
//参数4:消费完成后是否自动删除队列。true表示删除,false表示不删除。
//参数5:额外附加参数
channel.queueDeclare("queueTest",false,false,false,null);
//发布
//参数1:交换机名称
//参数2:队列名称
//参数3:传递消息额外设置
//参数4:消息的具体内容
channel.basicPublish("","queueTest",null,"rabbitmq ruuning...".getBytes());
//关闭
channel.close();
connection.close();
}
}
消息消费者
@SpringBootTest
public class Consumer {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queueTest", false, false, false, null);
//参数1:队列名称
//参数2:是否开启消息的自动确认机制
//参数3:消费时的回调接口:new 一个DefaultConsumer对象,重写其方法,并传入通道参数
channel.basicConsume("queueTest", 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));
}
});
}
}
案例二:Work Queue(平均分配)
与上面的基本模型的区别就是工作模型是多个消费者,多个消费者绑定同一个队列来进行消费,同一条消息只会被一个消费者处理,相当于就是多个消费者来共同处理一个消息队列中的消息,默认是平均消费,每个消费者轮流来消费。
public class Publisher {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queueTest2",false,false,false,null);
for (int i=0;i<20;i++){
channel.basicPublish("","queueTest2",null,("work is running"+i).getBytes());
}
channel.close();
connection.close();
}
}
public class Consumer1 {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queueTest2",false,false,false,null);
channel.basicConsume("queueTest2",true,new DefaultConsumer(channel){//第二个参数为false表示关闭消息自动确认机制
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1接收消息:"+new String(body));
}
});
}
}
public class Consumer2 {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queueTest2",false,false,false,null);
channel.basicConsume("queueTest2",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));
}
});
}
}
案例三:Work Queue(能者多劳)
能者多劳的工作消息队列,谁消息速度快谁就消费多,这才是最公平的,而不是平均消费。
主要是三个步骤:
-
设置每个消费者每次只能消费一个消息
-
关闭自动确认机制
-
开启手动确认机制
public class Publisher {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("queueTest2",false,false,false,null);
for (int i=0;i<20;i++){
channel.basicPublish("","queueTest2",null,("work is running"+i).getBytes());
}
channel.close();
connection.close();
}
}
public class Consumer1 {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);//每个消费者每次只能消费一个消息
channel.queueDeclare("queueTest2",false,false,false,null);
channel.basicConsume("queueTest2",false,new DefaultConsumer(channel){//第二个参数为false表示关闭消息自动确认机制
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费者1接收消息:"+new String(body));
//开启手动确认
//参数1:每个消费者注册到RabbitMQ时会获得一个唯一标识ID
//参数2:false表示只确认当前收到的一条消息
channel.basicAck(envelope.getDeliveryTag(), false);
}
});
}
}
public class Consumer2 {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.basicQos(1);
channel.queueDeclare("queueTest2",false,false,false,null);
channel.basicConsume("queueTest2",false,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));
channel.basicAck(envelope.getDeliveryTag(),false);
}
});
}
}
案例四:Fanout Exchange
注意:
- 可以有多个队列,每个队列都要绑定到Exchange(交换机)
- 交换机不能缓存消息,只是用来将消息路由到队列,如果路由失败,消息会丢失
- 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
- 交换机把消息发送给绑定过的所有队列
- 订阅队列的消费者都能拿到消息,即每一条消息都会广播到每一个绑定的队列,然后消息者去队列里拿
public class Publisher {
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//参数1:交换机名称,可以随意
//参数2:交换机类型
//如果交换机需要持久化,就把第三个参数设为true即可
channel.exchangeDeclare("fanoutTest", "fanout");
//参数1:交换机名称
//参数2:路由key(广播模型中用不上)
//参数3:消息持久化的特性
//参数4:具体需要发送的消息
channel.basicPublish("fanoutTest", "", null, ("Fanout is running..." ).getBytes());
channel.close();
connection.close();
}
}
public class Consumer1 {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("fanoutTest","fanout");
//创建临时队列
String queue = channel.queueDeclare().getQueue();
//将交换机和队列绑定起来
//参数1:队列名称
//参数2:交换机名称
//参数3:路由key
channel.queueBind(queue,"fanoutTest","");
//消费消息
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));
}
});
}
}
public class Consumer2 {
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("fanoutTest","fanout");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"fanoutTest","");
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));
}
});
}
}
案例五:Direct Exchange
注意:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)
- 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 RoutingKey。
- Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key进行判断,只有队列的Routingkey与消息的 Routing key完全一致,才会接收到消息
- Direct模型和Fanout模型的区别就是:Fanout交换机会把每一条消息路由到每一个绑定的队列,而Direct交换机则是把消息路由到绑定的队列中的routingKey相同的所有队列(即不仅要和交换机绑定,还要routingKey相同的队列才能收到消息)。
public class Publisher {
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("directTest","direct");
String routingKey="test1";
channel.basicPublish("directTest",routingKey,null,"direct is running...".getBytes());
channel.close();
connection.close();
}
}
public class Consumer1 {
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("directTest","direct");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"directTest","test1");
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));
}
});
}
}
public class Consumer2 {
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("directTest","direct");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"directTest","test1");
channel.queueBind(queue,"directTest","test2");
channel.queueBind(queue,"directTest","test3");
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));
}
});
}
}
案例六:Topic Exchange
注意:
- Topic与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定routingkey 的时候使用通配符!
- routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
- #:匹配任意个词,包括0个
- *:匹配1个词
public class Publisher {
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topicTest","topic");
String routeKey="a.test.user";
channel.basicPublish("topicTest",routeKey,null,"topic is running...".getBytes());
channel.close();
connection.close();
}
}
public class Consumer1 {
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topicTest","topic");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"topicTest","test.*");
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));
}
});
}
}
public class Consumer2 {
public static void main(String[] args)throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setUsername("songdiao");
factory.setPassword("sd460429");
factory.setHost("");
factory.setPort(5672);
factory.setVirtualHost("/");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("topicTest","topic");
String queue = channel.queueDeclare().getQueue();
channel.queueBind(queue,"topicTest","#.test.*");
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));
}
});
}
}
小结
- 上面我们是介绍了五种消息队列的模型,分别是 基本消息队列,工作消息队列,广播模型,路由模型,动态路由模型
- 基本消息模型:很简单,就是一对一的关系
- 工作消息模型:一般我们Publisher发送的消息较多时,一个Consumer根本来不及消费,所以就有了工作消息,多个消费者一起消费(同一条消息只能由一个消费者来消费)。工作模型又分为 平均消费 和 能者多劳
- 广播模型:和上面两者都有很大差距,上面都是同一条消息只能由一个消费者来消费,但是广播模型是与路由绑定的每个队列的每个消费者都会消费所有消息
- 路由模型:和广播模型差不多,但也有一点区别,路由模型是routingKey对应的队列才能收到消息,才能被该队列对应的消费者消费
- 动态路由模型:和路由模型差不多,我们可以理解为就是路由模型的升级版,升级的地方就是routingkey并不是要一样,而是满足一定的匹配规则即可
- 其实我们这一篇将的都是RabbitMQ的手动实现,显得代码比较冗余,其实这些Spring都已经帮我们进行封装好了,SpringAMQP,我们下篇文章讲