一、服务异步通讯
1 初识MQ
同步调用存在的问题
异步调用方案:常见实现就是事件驱动模式
MQ(消息队列),存放消息的队列,也就是事件驱动架构中的Broker
2 MQ基本概念
- MQ消息队列:是在消息地传输过程中保存消息的容器,多用于分布式系统之间进行通信
- 发送方成为生产者,接收方称为消费者
2.1 分布式系统子系统间两种通信方式
- 远程调用
- 通过第三方传递消息
2.2 MQ的优势
1、应用解耦
- 直接远程调用耦合度高
系统的耦合性越高,容错性就越低,可维护性就越低。
- 通过MQ实现解耦
使用MQ使得应用间解耦,提升容错性和可维护性。
2、异步提速
- 远程调用方式:同步方式
一个下单操作耗时:20+300+300+300 = 920ms
用户点击完下单按钮后,需要等待920ms才能得到下单响应,太慢!
- MQ方式通信:异步
用户点击完下单按钮后,只需等待25ms就能得到下单响应(20+5 = 25ms) 提升用户体验和系统吞吐量(单位时间内处理请求的数目)。
3、削峰填谷
- 调控调用
请求瞬间增多,导致A系统压力过大而宕机。
- MQ传递
使用了MQ之后,限制消费小溪的速度为1000,这样一来,高峰期产生的数据势必会挤压在MQ中,高峰就被“削”掉了,但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000,直到消费完积压的消息,这就叫做“填谷”。 使用MQ后,可以提高系统稳定性。
2.3 MQ劣势
系统可用性降低
- 系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机,就会对业务造成影响。如何保证MQ的高可用?
系统复杂度提高
- MQ的加入大大增加了系统的复杂度,以前系统间时同步的远程调用,现在是通过MQ异步调用。如何保证消息没有被重复消 费?怎样处理消息丢失情况?怎么保证消息传递的顺序性?
一致性问题
- A系统处理完业务,通过MQ给B、C、D三个系统发消息数据,如果B系统、C系统处理成功,D系统处理失败。如何保证消息数据处理的一致性?
既然MQ有优势也有劣势,那么使用MQ需要满足什么条件呢?
- ① 生产者不需要从消费者出获得反馈。引入消息队列之前的直接调用,其接口的返回值应该为null,这才让明白下层的动作还没做,上层却当成动作做完了继续往后走,即所谓异步成为了可能。
- ②容许短暂的不一致性。
- ③确实用了有效果。即解耦、提速、削峰这些方面的收益,超过加入MQ,管理MQ这些成本
3 RabbitMQ简介
- RabbitMQ是基于Erlang语言开发的开源消息通信中间件
AMQP,即 Advanced Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。给予此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP规范发布。类比http
RabbitMQ基础架构如下图:
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 connectino的开销
- 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的分发依据。
3.1常见消息模型
3.2 JMS
- JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件的API
- JMS是JavaEE规范中的一种,类比JDBC
- 很多消息中间件都实现了JMS规范,例如:ActiveMQ。RabbitMQ官方没有提供JMS的实现包,但是开源社区有
3.3 RabbitMQ的安装和配置
- RabbitMQ的官方地址:www.rabbitmq.com/
- 安装文档:资料/软件/安装RabbitMQ.md
4 RabbitMQ快速入门
4.1 简单模式快速入门
步骤:
- ① 创建工程(生产者、消费者)
- ② 分别添加依赖
- ③ 编写生产者发送消息
- ④ 编写消费者接收消息
生产者和消费者为两个模块
Producer_HelloWorld.java
package com.ph.producer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class Producer_HelloWorld {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建队列Queue
/*
* queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
* 参数:
* 1.queue:队列名称
* 2.durable:是否持久化,当mq重启之后,还在
* 3。exclusive:
* *是否独占。只能有一个消费者监听这队列
* *当Connection关闭时,是否删除队列
* 4、autoDelete:是否自动删除。当没有Consumer时,自动删除掉
* 5、arguments:参数
* */
//入门没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("hello_world",true,false,false,null);
//6、发送消息
/*
* basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
* 参数:
* 1、exchange:交换机名称。简单模式下交换机会使用默认的""
* 2、routingKey:路由名称,与队列名称一致即可绑定
* 3、props:配置信息
* 4、body:发送消息数据
* */
String body = "hello rabbitMQ!";
channel.basicPublish("","hello_world",null,body.getBytes());
//7、释放资源
channel.close();
connection.close();
}
}
Consumer_HelloWorld.java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_HelloWorld {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
//5、创建队列Queue
/*
* queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
* 参数:
* 1.queue:队列名称
* 2.durable:是否持久化,当mq重启之后,还在
* 3。exclusive:
* *是否独占。只能有一个消费者监听这队列
* *当Connection关闭时,是否删除队列
* 4、autoDelete:是否自动删除。当没有Consumer时,自动删除掉
* 5、arguments:参数
* */
//入门没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("hello_world",true,false,false,null);
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
//回调对象
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);
System.out.println("body"+new String(body));
}
};
channel.basicConsume("hello_world",true,consumer);
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
5 RabbitMQ的工作模式
5.1、Work queues工作队列模式
1、模式说明(一条消息只能被一个消费者消费.,多个消费者竞争)
- Work Queues:与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
- 应用场景:对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
代码:
Producer_WorkQueues.java
package com.ph.producer;
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_WorkQueues {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
//5、创建队列Queue
/*
* queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
* 参数:
* 1.queue:队列名称
* 2.durable:是否持久化,当mq重启之后,还在
* 3。exclusive:
* *是否独占。只能有一个消费者监听这队列
* *当Connection关闭时,是否删除队列
* 4、autoDelete:是否自动删除。当没有Consumer时,自动删除掉
* 5、arguments:参数
* */
//如果没有一个名字叫hello_world的队列,则会创建该队列,如果有则不会创建
channel.queueDeclare("work_queues",true,false,false,null);
//6、发送消息
/*
* basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
* 参数:
* 1、exchange:交换机名称。简单模式下交换机会使用默认的""
* 2、routingKey:路由名称
* 3、props:配置信息
* 4、body:发送消息数据
* */
for (int i = 0; i <= 10; i++) {
String body = i+"=>hello rabbitMQ work_queues!";
channel.basicPublish("","work_queues",null,body.getBytes());
}
//7、释放资源
channel.close();
connection.close();
}
}
Consumer_WorkQueues1.java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_WorkQueues1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
}
};
channel.basicConsume("work_queues",true,consumer); //输出1 3 5 7 9
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
Consumer_WorkQueues2.java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_WorkQueues2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
}
};
channel.basicConsume("work_queues",true,consumer);
//输出2 4 6 8
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
5.2 Pub/Sub订阅模式
1、模式说明(一条消息可以被多个消费者消费)
在订阅模型中,多了一个Exchange角色,而且过程略有变化:
- P:生产者,也就是发送消息的程序,但是不在发送到队列中,而是发给X(交换机)
- C:消费者,消息的接收者,会一直等待消息到来
- Queue:消息队列,接收消息、缓存消息
- Exchange:交换机(X)。一方面,接收生产者发送的消息。另一方面,直到如何处理消息,例如:递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。
Exchange有常见以下3中类型:
-
- Fanout:广播,将消息交给所有绑定到交换机的队列
-
- Direct:定向,把消息交给符合指定routing key的队列
-
- Topic:通配符,把消息交个符合routing pattern(路由模式)的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
代码:
Producer_SubPub .java
package com.ph.producer;
import com.rabbitmq.client.BuiltinExchangeType;
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_SubPub {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/*exchangeDeclare(String exchange,
BuiltinExchangeType type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments)
参数:
1、exchange:交换机名称
2、type:交换机类型
(1) DIRECT("direct"):定向
(2)FANOUT("fanout"):扇形(广播),发送消息到每一个与之绑定的队列。
(3)TOPIC("topic"):通配符的形式
(4)HEADERS("headers"):参数匹配
3、durable:是否持久化
4、autoDelete:自动删除
5、internal:内部使用。一般为false
6、arguments:参数
*/
String exchangeName = "test_fanout";
channel.exchangeDeclare(exchangeName,BuiltinExchangeType.FANOUT,true,false,false,null);
//6、创建队列
String queue1Name = "test_fanout_queue1";
String queue2Name = "test_fanout_queue2";
channel.queueDeclare(queue1Name,true,false,false,null);
channel.queueDeclare(queue2Name,true,false,false,null);
//7、绑定队列和交换机
/*
queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
参数:
1、queue:队列名称;
2、exchange:交换机名称;
3、routingKey:路由键,绑定规则:
如果交换机的类型为fanout,routingKey设置为“”字符串,表示消息会发给每一个与之绑定的queue
4、arguments:参数
*/
channel.queueBind(queue1Name,exchangeName,"",null);
channel.queueBind(queue2Name,exchangeName,"",null);
//8、发送消息
String body = "日志信息:张三调用了findAll方法,日志级别:info";
channel.basicPublish(exchangeName,"",null,body.getBytes());
//9、释放资源
channel.close();
connection.close();
}
}
Consumer_SubPub1 .java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_SubPub1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
String queue1Name = "test_fanout_queue1";
String queue2Name = "test_fanout_queue2";
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
System.out.println("将日志信息打印到控制台");
}
};
channel.basicConsume(queue1Name,true,consumer);
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
Consumer_SubPub2 .java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_SubPub2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
String queue1Name = "test_fanout_queue1";
String queue2Name = "test_fanout_queue2";
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
System.out.println("将日志信息保存数据库");
}
};
channel.basicConsume(queue2Name,true,consumer);
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
5.3、Routing路由模式
模式说明:
队列与交换机的绑定,不能是任意绑定了,而是要指定一个routingKey(路由key) 消息的发送方在向exchange发送消息时,也必须指定消息的routingKey Exchange不在把消息交给每一个绑定的队列,而是根据消息的RoutingKey进行判断,只有队列的RoutingKey与消息的RoutingKey完全一致,才会接收到消息
图解:
- p:生产者,向exchange发送消息,发送消息时,会制定一个routing key
- X:exchange(交换机),接收生产者消息,然后把消息递交给routing key完全匹配的队列
- C1:消费者,其所在队列指定需要routing key为error的消息
- C2:消费者,其所在队列指定需要routing key为info、error、warning的消息
Producer_Routing .java
package com.ph.producer;
import com.rabbitmq.client.BuiltinExchangeType;
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_Routing {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/*exchangeDeclare(String exchange,
BuiltinExchangeType type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments)
参数:
1、exchange:交换机名称
2、type:交换机类型
(1) DIRECT("direct"):定向
(2)FANOUT("fanout"):扇形(广播),发送消息到每一个与之绑定的队列。
(3)TOPIC("topic"):通配符的形式
(4)HEADERS("headers"):参数匹配
3、durable:是否持久化
4、autoDelete:自动删除
5、internal:内部使用。一般为false
6、arguments:参数
*/
String exchangeName = "test_direct";
channel.exchangeDeclare(exchangeName,BuiltinExchangeType.DIRECT,true,false,false,null);
//6、创建队列
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
channel.queueDeclare(queue1Name,true,false,false,null);
channel.queueDeclare(queue2Name,true,false,false,null);
//7、绑定队列和交换机
/*
queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
参数:
1、queue:队列名称;
2、exchange:交换机名称;
3、routingKey:路由键,绑定规则:
如果交换机的类型为fanout,routingKey设置为“”字符串,表示消息会发给每一个与之绑定的queue
4、arguments:参数
*/
//队列1的绑定 error
channel.queueBind(queue1Name,exchangeName,"error",null);
//队列2的绑定 info error warning
channel.queueBind(queue2Name,exchangeName,"info",null);
channel.queueBind(queue2Name,exchangeName,"error",null);
channel.queueBind(queue2Name,exchangeName,"warning",null);
//8、发送消息
String body = "日志信息:张三调用了findAll方法,日志级别:info";
channel.basicPublish(exchangeName,"info",null,body.getBytes());
//9、释放资源
channel.close();
connection.close();
}
}
Consumer_Routing1 .java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_Routing1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
System.out.println("将日志信息打印到控制台");
}
};
channel.basicConsume(queue1Name,true,consumer);//收不到消息
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
Consumer_Routing2 .java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_Routing2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
String queue1Name = "test_direct_queue1";
String queue2Name = "test_direct_queue2";
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
System.out.println("将日志信息打印到控制台");
}
};
channel.basicConsume(queue2Name,true,consumer);//可以接受消息
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
5.4、Topics通配符模式
1、模式说明
图解:
- 红色Queue:绑定的usa.#,因此凡是以usa.开头的routing key 都会被匹配到;
- 黄色Queue:绑定的是#.news,因此凡是以.news结尾的routing key都会被匹配到;
- *代表1个单词,#代表0到多个单词。
代码:
Producer_Topics .java
package com.ph.producer;
import com.rabbitmq.client.BuiltinExchangeType;
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_Topics {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、创建Channel
Channel channel = connection.createChannel();
//5、创建交换机
/*exchangeDeclare(String exchange,
BuiltinExchangeType type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments)
参数:
1、exchange:交换机名称
2、type:交换机类型
(1) DIRECT("direct"):定向
(2)FANOUT("fanout"):扇形(广播),发送消息到每一个与之绑定的队列。
(3)TOPIC("topic"):通配符的形式
(4)HEADERS("headers"):参数匹配
3、durable:是否持久化
4、autoDelete:自动删除
5、internal:内部使用。一般为false
6、arguments:参数
*/
String exchangeName = "test_topic";
channel.exchangeDeclare(exchangeName,BuiltinExchangeType.TOPIC,true,false,false,null);
//6、创建队列
String queue1Name = "test_topic_queue1";
String queue2Name = "test_topic_queue2";
channel.queueDeclare(queue1Name,true,false,false,null);
channel.queueDeclare(queue2Name,true,false,false,null);
//7、绑定队列和交换机
/*
queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments)
参数:
1、queue:队列名称;
2、exchange:交换机名称;
3、routingKey:路由键,绑定规则:
如果交换机的类型为fanout,routingKey设置为“”字符串,表示消息会发给每一个与之绑定的queue
4、arguments:参数
*/
//routing key 系统的名称.日志的级别
//需求:所有error级别的日志存入数据库,所有order系统的日志存入数据库
channel.queueBind(queue1Name,exchangeName,"#.error",null);
channel.queueBind(queue1Name,exchangeName,"order.*",null);
channel.queueBind(queue2Name,exchangeName,"*.*",null);
//8、发送消息
String body = "日志信息:张三调用了findAll方法,日志级别:info";
channel.basicPublish(exchangeName,"order.error",null,body.getBytes());
//9、释放资源
channel.close();
connection.close();
}
}
Consumer_Topics1 .java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_Topics1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
String queue1Name = "test_topic_queue1";
String queue2Name = "test_topic_queue2";
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
System.out.println("将日志信息打印到控制台");
}
};
channel.basicConsume(queue1Name,true,consumer);
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
Consumer_Topics2 .java
package org.example;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer_Topics2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1、创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
//2、设置参数
factory.setHost("172.16.98.133");//ip 默认值 localhost
factory.setPort(5672);//端口 默认值5672
factory.setVirtualHost("/itcast");//虚拟机 ,默认值/
factory.setUsername("heima");//用户名 默认 guest
factory.setPassword("heima");//密码 默认 guest
//3、创建连接 Connection
Connection connection = factory.newConnection();
//4、常见Channel
Channel channel = connection.createChannel();
String queue1Name = "test_topic_queue1";
String queue2Name = "test_topic_queue2";
//接收消息
/*
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数:
* 1、queue:队列名称
* 2、autoAck:是否自动确认
* 3、callback:回调对象
* */
Consumer consumer = new DefaultConsumer(channel){
/*
* 回调方法,当收到消息后,会自动执行该方法
* 1、consumerTag:标识
* 2、envelope:获取一些信息,交换机、路由key...
* 3、properties:配置信息
* 4、body:数据
* */
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
/*System.out.println("consumerTag"+consumerTag);
System.out.println("Exchange"+envelope.getExchange());
System.out.println("RoutingKey"+envelope.getRoutingKey());
System.out.println("properties"+properties);*/
System.out.println("body"+new String(body));
System.out.println("将日志信息打印到控制台");
}
};
channel.basicConsume(queue2Name,true,consumer);
//消费者不用关闭资源链接,消费者相当于监听程序
}
}
二、 Spring整合RabbitMQ
1 Spring整合RabbitMQ
生产者端代码
1、Maven文件pom.xml
<?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>org.example</groupId>
<artifactId>spring-rabbitmq-producers</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.7.RELEASE</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>
2、资源文件rabbitmq.properties
rabbitmq.host=172.16.98.133
rabbitmq.port=5672
rabbitmq.username=heima
rabbitmq.password=heima
rabbitmq.virtual-host=/itcast
3、spring配置文件spring-rabbitmq-producer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--加载配置文件-->
<context:property-placeholder location="classpath:rabbitmq.properties"/>
<!-- 定义rabbitmq connectionFactory -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<!--定义管理交换机、队列-->
<rabbit:admin connection-factory="connectionFactory"/>
<!--定义持久化队列,不存在则自动创建;不绑定到交换机则绑定到默认交换机
默认交换机类型为direct,名字为:"",路由键为队列的名称
-->
<!--
id:bean的名称
name:queue的名称
auto-declare:自动创建
auto-delete:自动删除。最后一个消费者和该队列断开连接后,自动删除队列
exclusive:是否独占(排他)
durable:是否持久化
-->
<rabbit:queue id="spring_queue" name="spring_queue" auto-declare="true"/>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~广播;所有队列都能收到消息~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_fanout_queue_1" name="spring_fanout_queue_1" auto-declare="true"/>
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_fanout_queue_2" name="spring_fanout_queue_2" auto-declare="true"/>
<!--定义广播类型交换机;并绑定上述两个队列-->
<rabbit:fanout-exchange id="spring_fanout_exchange" name="spring_fanout_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding queue="spring_fanout_queue_1"/>
<rabbit:binding queue="spring_fanout_queue_2"/>
</rabbit:bindings>
</rabbit:fanout-exchange>
<!-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~通配符;*匹配一个单词,#匹配多个单词 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -->
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_topic_queue_star" name="spring_topic_queue_star" auto-declare="true"/>
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_topic_queue_well" name="spring_topic_queue_well" auto-declare="true"/>
<!--定义广播交换机中的持久化队列,不存在则自动创建-->
<rabbit:queue id="spring_topic_queue_well2" name="spring_topic_queue_well2" auto-declare="true"/>
<rabbit:topic-exchange id="spring_topic_exchange" name="spring_topic_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding pattern="heima.*" queue="spring_topic_queue_star"/>
<rabbit:binding pattern="heima.#" queue="spring_topic_queue_well"/>
<rabbit:binding pattern="itcast.#" queue="spring_topic_queue_well2"/>
</rabbit:bindings>
</rabbit:topic-exchange>
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
</beans>
4、测试文件ProducerTest.java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
//1、注入RabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testHelloWorld(){
//2、发送消息
rabbitTemplate.convertAndSend("spring_queue","hello world spring....");
}
@Test
public void testFanout(){
//2、发送消息
rabbitTemplate.convertAndSend("spring_fanout_exchange","","spring fanout");
}
public void testTopic(){
//2、发送消息
rabbitTemplate.convertAndSend("spring_topic_exchange","heima.hehe.haha","spring topics");
}
}
消费者端代码
1、Maven文件pom.xml
<?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>org.example</groupId>
<artifactId>spring-rabbitmq-consumer</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.1.8.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.1.7.RELEASE</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>
2、资源文件rabbitmq.properties
rabbitmq.host=172.16.98.133
rabbitmq.port=5672
rabbitmq.username=heima
rabbitmq.password=heima
rabbitmq.virtual-host=/itcast
3、spring配置文件spring-rabbitmq-consumer.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--加载配置文件-->
<context:property-placeholder location="classpath:rabbitmq.properties"/>
<!-- 定义rabbitmq connectionFactory -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"/>
<bean id="springQueueListener" class="com.itheima.rabbitmq.listener.SpringQueueListener"/>
<!--<bean id="fanoutListener1" class="com.itheima.rabbitmq.listener.FanoutListener1"/>
<bean id="fanoutListener2" class="com.itheima.rabbitmq.listener.FanoutListener2"/>
<bean id="topicListenerStar" class="com.itheima.rabbitmq.listener.TopicListenerStar"/>
<bean id="topicListenerWell" class="com.itheima.rabbitmq.listener.TopicListenerWell"/>
<bean id="topicListenerWell2" class="com.itheima.rabbitmq.listener.TopicListenerWell2"/>-->
<rabbit:listener-container connection-factory="connectionFactory" auto-declare="true">
<rabbit:listener ref="springQueueListener" queue-names="spring_queue"/>
<!--<rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
<rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
<rabbit:listener ref="topicListenerStar" queue-names="spring_topic_queue_star"/>
<rabbit:listener ref="topicListenerWell" queue-names="spring_topic_queue_well"/>
<rabbit:listener ref="topicListenerWell2" queue-names="spring_topic_queue_well2"/>-->
</rabbit:listener-container>
</beans>
4、监听类SpringQueueListener.java
package com.itheima.rabbitmq.listener;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
public class SpringQueueListener implements MessageListener {
@Override
public void onMessage(Message message) {
//打印消息
System.out.println(new String(message.getBody()));
}
}
2 Spring Boot整合RabbitMQ
生产者
1、创建生产者SpringBoot工程
2、引入依赖坐标
<?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>org.example</groupId>
<artifactId>producer-springboot</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
3、编写yml配置,基本信息配置application.yml
# 配置RabbitMQ的基本信息 ip、端口、username password
spring:
rabbitmq:
host: 172.16.98.133 # ip
port: 5672
username: heima
password: heima
virtual-host: /
4、定义交换机,队列以及绑定关系的配置类
RabbitMQConfig.java
package com.itheima.rabbitmq.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RabbitMQConfig {
public static final String EXCHANGE_NAME = "boot_topic_exchange";
public static final String QUEUE_NAME = "boot_queue";
//1、交换机
@Bean("bootExchange")
public Exchange bootExchange(){
return ExchangeBuilder.topicExchange(EXCHANGE_NAME).durable(true).build();
}
//2、Queue 队列
@Bean("bootQueue")
public Queue bootQueue(){
return QueueBuilder.durable(QUEUE_NAME).build();
}
//3、队列和交换机绑定关系 Binding
/*
1、知道是哪个队列
2、知道哪个交换机
3、routing key
*/
@Bean
public Binding bootQueueExchange(@Qualifier("bootQueue") Queue queue, @Qualifier("bootExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("boot.#").noargs();
}
}
5、注入RabbitTemplate,调用方法,完成消息发送,测试类 ProducerTest.java
import com.itheima.rabbitmq.config.RabbitMQConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@SpringBootTest
@RunWith(SpringRunner.class)
public class ProducerTest {
//1、注入rabbitTemplate
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSend(){
rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME,"boot.haaha","这是一条发给topics模式的消息!");
}
}
消费者
1、创建生产者SpringBoot工程
2、引入依赖坐标
3、编写yml配置,基本信息配置application.yml
4、创建监听类RabbitMQListener.java
package com.example.consumerspringboot;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitMQListener {
@RabbitListener(queues = "boot_queue")
public void ListenerQueue(Message message){
System.out.println(new String(message.getBody()));
}
}
三、 RabbitMQ高级特性
1 消息可靠投递
生产端:
在使用 RabbitMQ的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。
RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式。
- confirm 确认模式
- return 退回模式
rabbitmq整个消息投递的路径为:
- 消从 producer到exchange 则会返回一个confirmCallback
- 消息从 exchange-->queue 投递失败则会返回一个returnCallback
我们将利用这两个callback 控制消息的可靠性投递
confirm
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-producer.xml")
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 确认模式:
* 步骤:
* 1. 确认模式开启:ConnectionFactory中开启publisher-confirms="true"
* 2. 在rabbitTemplate定义ConfirmCallBack回调函数
*/
@Test
public void testConfirm() {
//2. 定义回调 **
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 相关配置信息
* @param ack exchange交换机 是否成功收到了消息。true 成功,false代表失败
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm方法被执行了....");
if (ack) {
//接收成功
System.out.println("接收成功消息" + cause);
} else {
//接收失败
System.out.println("接收失败消息" + cause);
//做一些处理,让消息再次发送。
}
}
});
//3. 发送消息
rabbitTemplate.convertAndSend("test_exchange_confirm111", "confirm", "message confirm....");
}
}
return
/**
* 步骤:
* 1. 开启回退模式:publisher-returns="true"
* 2. 设置ReturnCallBack
* 3. 设置Exchange处理消息的模式:
* 1. 如果消息没有路由到Queue,则丢弃消息(默认)
* 2. 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
*/
@Test
public void testReturn() {
//设置交换机处理失败消息的模式
rabbitTemplate.setMandatory(true);
//2.设置ReturnCallBack
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 消息对象
* @param replyCode 错误码
* @param replyText 错误信息
* @param exchange 交换机
* @param routingKey 路由键
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println("return 执行了....");
System.out.println(message);
System.out.println(replyCode);
System.out.println(replyText);
System.out.println(exchange);
System.out.println(routingKey);
//处理
}
});
//3. 发送消息
rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm....");
}
2 Comsumer ACk
1.2 Consumer ACK ack指 Acknowledge,确认。 表示消费端收到消息后的确认方式。
有三种确认方式:
• 自动确认:acknowledge=“none”
• 手动确认:acknowledge=“manual”
• 根据异常情况确认:acknowledge=“auto”,(这种方式使用麻烦,不作讲解)
其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。
如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。
package com.itheima.listener;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* Consumer ACK机制:
* 1. 设置手动签收。acknowledge="manual"
* 2. 让监听器类实现ChannelAwareMessageListener接口
* 3. 如果消息成功处理,则调用channel的 basicAck()签收
* 4. 如果消息处理失败,则调用channel的basicNack()拒绝签收,broker重新发送给consumer
*/
@Component
public class AckListener implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
try {
//1.接收转换消息
System.out.println(new String(message.getBody()));
//2. 处理业务逻辑
System.out.println("处理业务逻辑...");
int i = 3/0;//出现错误
//3. 手动签收
channel.basicAck(deliveryTag,true);
} catch (Exception e) {
//e.printStackTrace();
//4.拒绝签收
/*
第三个参数:requeue:重回队列。如果设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
*/
channel.basicNack(deliveryTag,true,true);
// 了解
//channel.basicReject(deliveryTag,true);
}
}
}
3. 消费端限流
如上图所示:
第一种情况:
如果在A系统中需要维护相关的业务功能,可能需要将A系统的服务停止,那么这个时候消息的生产者还是一直会向MQ中发送待处理的消息,消费者此时服务已经关闭,导致大量的消息都会在MQ中累积。 如果当A系统成功启动后,消费者会一次性将MQ中累积的大量的消息拉取到自己的服务,导致服务在短时间内会处理大量的业务,可能会导致系统服务的崩溃。 所以消费端限流是非常有必要的。
第二种情况: 当大量用户注册时,高并发请求过来,邮件接口只支持小量并发,这时消费端限流也非常必要;
消费端限流配置:设置监听器容器属性container.setPrefetchCount(1);表示消费端每次从mq拉去1条消息来消费,直到手动确认消费完毕后,才会继续拉去下一条消息。
代码实现
目标:演示消费端限流效果
基于消费端确认的工程进行演示
消费端限流要求, 消费端确认模式必须为手动确认
实现步骤:
- 配置每次拉取消息1条
- 在监听器接收消息方法中,休眠3秒,否则拉取过快,看不出效果
- 重启消费者工程,查看控制面板频道中,每次拉取1消息配置是否生效
- 多次发送请求进行测试 实现过程:
配置每次拉取消息1条
# RabbitMQ 服务host地址
spring.rabbitmq.host=192.168.200.128
# 端口
spring.rabbitmq.port=5672
# 虚拟主机地址
spring.rabbitmq.virtual-host=/kkb
# rabbit服务的用户名
spring.rabbitmq.username=kaikeba
# rabbit服务的密码
spring.rabbitmq.password=kaikeba
# 配置开启手动签收
# 简单模式的开启手动签收
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 路由模式开启手动签收
spring.rabbitmq.listener.direct.acknowledge-mode=manual
# 是否支持重试
spring.rabbitmq.listener.direct.retry.enabled=true
# 设置消费端限流,每次拉取消息多少条,默认是250条
spring.rabbitmq.listener.direct.prefetch=1
在自定义监听器接收消息方法中,休眠3秒,否则拉取过快,看不出效果
/**
- 自定义监听器,监听到消息之后,立即执行onMessage方法 */
@Component
@RabbitListener(queues = "order.A")
public class CustomAckConsumerListener {
/**
* 监听到消息之后执行的方法
*
* @param message 消息内容
* @param channel 消息所在频道
*/
@RabbitHandler
public void queueListenerHandle(String msg, Message message, Channel channel) throws Exception {
//获取消息内容
System.out.println("接收到消息,执行具体业务逻辑{} 消息内容:" + msg);
//获取投递标签
MessageProperties messageProperties =
message.getMessageProperties();
long deliveryTag = messageProperties.getDeliveryTag();
try {
//休眠3秒
Thread.sleep(3000);
if (msg.contains("苹果")) {
throw new RuntimeException("不允许卖苹果手机!!!");
}
/**
* 手动签收消息
* 参数1:消息投递标签
* 参数2:是否批量签收:true一次性签收所有,false,只签收当前消息
*/
channel.basicAck(deliveryTag, false);
System.out.println("手动签收完成:{}");
} catch (Exception ex) {
/**
* 手动拒绝签收
* 参数1:当前消息的投递标签
* 参数2:是否批量签收:true一次性签收所有,false,只签收当前消息
* 参数3:是否重回队列,true为重回队列,false为不重回
*/
channel.basicNack(deliveryTag, false, true);
System.out.println("拒绝签收,重回队列:{}" + ex);
}
}
}
重启消费者工程,查看控制面板频道中,每次拉取1消息配置是否生效
多次发送请求进行测试http://localhost:8080/direct/sendMsg?exchange=order_exchange&routingkey=order.A&msg=购买苹果手机
- 设置每次拉取消息1条spring.rabbitmq.listener.simple.prefetch=2
- 注意,如果想进行消费端限流,那么消息必须手动确认,AcknowledgeMode为MANUAL
4. TTL(消息存活时间)
TTL 全称 Time To Live(存活时间/过期时间)。当消息到达存活时间后,还没有被消费,会被自动清除。
RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。
注意:给单个消息设置过期时间没实际意义。
实现步骤:
在RabbitMQ管理控制台中,新增消息队列 order.B ,并设置消息失效时间为5秒 在RabbitMQ管理控制台中,将消息队列 order.B 绑定到交换机 order_exchange 上 测试发送消息到消息队列order.B中,该队列没有消费者接收消息 等待5秒,消息自动消失 实现过程:
1.在RabbitMQ管理控制台中,新增消息队列 order.B ,并设置消息失效时间为5秒
2. 在RabbitMQ管理控制台中,将消息队列 order.B 绑定到交换机 order_exchange 上
测试发送消息到消息队列order.B中,该队列没有消费者接收消息
等待5秒,消息自动消失
设置消息失效时间:
5. 死信队列
消息丢失,发送失败如何处理?任由消息消失?
死信队列:当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是Dead Letter Exchange(死信交换机 简写:DLX)。
消息成为死信的三种情况:
- 队列消息长度到达限制;
- 消费者拒接消息(basicNack),并且不把消息重新放回源队列,requeue=false;
- 源队列存在消息过期设置,消息到达超时时间未被消费; 设置死信队列绑定死信交换机:
- 给队列设置参数: x-dead-letter-exchange 和 x-dead-letter-routing-key
6 延迟队列
什么是延迟队列?即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。
需求场景:
下单后,30分钟未支付,取消订单,回滚车票。 新用户注册成功7天后,发送短信问好。 实现方法:
定时器 延迟队列
注意:在RabbitMQ中并未提供延迟队列功能。
但是可以使用:TTL+死信队列 组合实现延迟队列的效果。
四、RabbitMQ应用问题
1 消息幂等性处理
幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。
在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。
使用 乐观锁机制 保证消息的幂等操作原理;
乐观锁 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。 乐观锁适用于多读的应用类型,这样可以提高吞吐量。
五、 RabbitMQ 高可用集群搭建
摘要:实际生产应用中都会采用消息队列的集群方案,如果选择RabbitMQ那么有必要了解下它的集群方案原理
一般来说,如果只是为了学习RabbitMQ或者验证业务工程的正确性那么在本地环境或者测试环境上使用其单实例部署就可以了,但是出于MQ中间件本身的可靠性、并发性、吞吐量和消息堆积能力等问题的考虑,在生产环境上一般都会考虑使用RabbitMQ的集群方案。
3.1 集群方案的原理 RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的,这里只需要保证erlang_cookie的参数一致集群即可通信。
主要参考官方文档:www.rabbitmq.com/clustering.…