SpringCloud(六.2)SpringAMQP
基本介绍
AMQP:高级消息队列协议,在应用程序之间传递业务消息的开放标准,该协议与语言和平台无关,更符合微服务中独立性的要求
Spring AMQP:是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
Spring AMQP 特征:
监听器容器,用于异步处理入站消息
用于发送和接收消息的RabbitTemplate
RabbitAdmin,用于自动声明队列,交换和绑定
SpringAMQP大大简化了消息发送和接收的Api
SpringAMQP -- 入门案例Basic Queue简单队列模型
使用步骤
1、在消息发送者和消费者的父工程中引入依赖
<!--AMQP依赖,包含RabbitMQ--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
2、在消息发送者和消费者的yml文件中配置MQ地址
spring: rabbitmq: host: 192.168.150.101 # rabbitMQ的ip地址 port: 5672 # 端口 username: itcast #用户名 password: 123321 #密码 virtual-host: / #虚拟主机
-消息发送
rabbitTemplate.convertAndSend 前提!!必须存在 simple.queue 此队列,如果不存在是不行的。
在消息发送类服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2SimpleQueue() { //队列名称 String queueName = "simple.queue"; //消息 String message = "hello, spring amqp!"; //发送消息 rabbitTemplate.convertAndSend(queueName, message); } }
-消息接收
在yml文件中配置RabbitMQ地址
在消息消费服务中新建一个类SpringRabbitListener,代码如下:
@Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenSimpleQueue(String msg) { System.out.println("消费者接收到simple.queue的消息:【" + msg + "】"); } }
注意:消息一旦消费就会从队列中删除,RabbitMQ没有消息回溯功能。
总结Basic Queue发送和接收消息的步骤
发送消息
父工程引入amqp的starter依赖
配置RabbitMQ地址
引入RabbitTemplate对象,并使用方法convertAndSend()发送消息
接收消息(监听)
父工程引入amqp的starter依赖
配置RabbitMQ地址
定义类,添加@Component注解
类中声明方法,添加@RabbitListener注解,方法参数就是消息
SpringAMQP -- Work Queue工作队列案例
一个发送者,多个消费者。
有些人会想,为什么要挂在两个消费者?
因为队列中存放的消息是有上限的,假设queue中只能存储60条,publisher每秒钟发送50条,一个consumer每秒钟只能处理40条,这样随着时间的推移,queue中存储的数据就会堆满。 如果是两个consumer后,加大了消息的处理速度,有效的避免了堆积问题。
模拟Work Queue,实现一个队列绑定多个消费者
实现思路
在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
在consumer服务中定义两个消息监听者,都监听simple.queue队列
consumer1每秒处理50条消息,consumer2每秒处理10条消息
-发送者(消息发送)
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2WorkQueue() throws InterruptedException { String queueName = "simple.queue"; String message = "hello, message__"; //向simple.queue消息队列发送50条消息 for (int i = 1; i <= 50; i++) { rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } } }
-消费者(消息接收)
@Component public class SpringRabbitListener { @RabbitListener(queues = "simple.queue") public void listenWorkQueue1(String msg) throws InterruptedException { System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = "simple.queue") public void listenWorkQueue2(String msg) throws InterruptedException { System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(200); } }
效果是consumer1和consumer2各自拿了25条平分了所有的消息。
备注知识点:消息预取
当有大量的消息到达队列时,队列queue就会把消息进行投递,consumer1和consumer2的通道会先把消息拿过来(不管能不能处理的了,先拿过来),于是两边平均拿了所有的消息。
但是consumer1处理的速度快,很快就把消息处理了,consumer2处理的慢,处理需要一段时间
因为我们设想的是consumer1处理速度快多处理一些,consumer2处理速度慢少处理一些。这样才能避免处理总时间过长问题,这时我们就需要控制consumer的预取上限。
控制 consumer 预取上限
spring: rabbitmq: host: 192.168.150.101 # rabbitMQ的ip地址 port: 5672 # 端口 username: itcast #用户名 password: 123321 #密码 virtual-host: / #虚拟主机 listener: simple: prefetch: 1 #每次只能获取一条消息,处理完成后才能获取下一条消息
效果展示:
SpringAMQP -- 发布(Publish)、订阅(Subscribe)
发布订阅模式与之前案例的区别就是允许将同一消息发送给多个消费者。实现方式是加入exchange(交换机)
消费者和队列之间依然会有一个绑定。
之前是publisher直接发送给queue,但是现在publisher先把消息发送给exchange(交换机),再有exchange将消息发送给queue,那这样来说,publisher不用知道queue是否存在。
SpringAMQP -- 发布、订阅模型 -- Fanout(广播)
Fanout Exchange 会将接收到的消息路由到每一个跟其绑定的消息队列queue
1、在consumer服务声明Exchange(交换机)、Queue(队列)、Binding(绑定关系)
在consumer服务中添加一个类,添加@Configuration注解,并声明FanoutExchange、Queue和绑定关系对象Binding 代码如下:
@Configuration public class FanoutConfig { //创建交换机 itcast.fanout @Bean public FanoutExchange fanoutExchange(){ //创建交换机,交换机的名字叫itcast.fanout return new FanoutExchange("itcast.fanout"); } //创建第一个队列 @Bean public Queue fanoutQueue1(){ //创建消息队列1 队列名称 fanout.queue1 return new Queue("fanout.queue1"); } // 绑定队列1到交换机 @Bean public Binding fanoutBinding1(Queue fanoutQueue1, FanoutExchange fanoutExchange){ return BindingBuilder .bind(fanoutQueue1) .to(fanoutExchange); } //创建第二个队列 @Bean public Queue fanoutQueue2(){ //创建消息队列2 队列名称 fanout.queue2 return new Queue("fanout.queue2"); } // 绑定队列2到交换机 // 将队列1和交换机当作参数传递进来了 @Bean public Binding fanoutBinding2(Queue fanoutQueue2, FanoutExchange fanoutExchange){ return BindingBuilder .bind(fanoutQueue2) .to(fanoutExchange); } }
我们在页面上也确实能看到交换机icast.fanout
也可以点进去查看一下这个交换器,可以看到有两个队列和交换机进行了绑定。
-发送者(消息发送)
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendFanoutExchange() { // 交换机名称 String exchangeName = "itcast.fanout"; // 消息 String message = "hello, every one!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "", message); } }
-消费者(消息收接)
@Component public class SpringRabbitListener { @RabbitListener(queues = "fanout.queue1") public void listenFanoutQueue1(String msg) { System.out.println("消费者接收到fanout.queue1的消息:【" + msg + "】"); } @RabbitListener(queues = "fanout.queue2") public void listenFanoutQueue2(String msg) { System.out.println("消费者接收到fanout.queue2的消息:【" + msg + "】"); } }
效果展示:
总结:
SpringAMQP -- 发布、订阅模型 -- Direct(路由)
DirectExchange会将接收到的消息根据规则路由到指定的Queue,因此称为路由模式(routes)
-
发布者发送消息时,指定消息的RoutingKey
-
每一个Queue都与Exchange设置一个BindingKey(可以理解为约定的暗号)
-
Exchange将消息路由到BingingKey与消息RoutingKey一致的队列
队列在和交换机绑定的时候可以指定多个key,如下图所示
那这样看来DirectExchange比FanoutExchange更灵活一点,DirectExchange也可以模拟FanoutExchange
案例:利用SpringAMQP演示DirectExchange的使用
实现思路
利用@RabbitListener声明Exchange、Queue、RoutingKey(之前的交换机和队列都是@Bean的方式创建出来的)
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
在publisher中编写测试方法,向itcast.direct发送消息
-发送者(消息发送)
发送给RoutingKey为blue的队列
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendDirectExchange() { // 交换机名称 String exchangeName = "itcast.direct"; // 消息 String message = "hello, blue!"; // 发送消息 "blue"指定的是BindingKey 当与RoutingKey一样时,消费者consumer便可以收到消息 rabbitTemplate.convertAndSend(exchangeName, "blue", message); } }
-消费者(消息接收)
@Component public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue1"),//指定队列 exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT),//指定交换机名称和交换机类型 key = {"red", "blue"} //RoutingKey 可以单个 可以多个 多个用数组 )) public void listenDirectQueue1(String msg) { System.out.println("消费者接收到direct.queue1的消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "direct.queue2"), exchange = @Exchange(name = "itcast.direct", type = ExchangeTypes.DIRECT), key = {"red", "yellow"} )) public void listenDirectQueue2(String msg) { System.out.println("消费者接收到direct.queue2的消息:【" + msg + "】"); } }
效果展示:
启动消费者:
启动发送者:
如果发送的key改为yellow
如果key改为red
总结:
SpringAMQP -- 发布、订阅模型 -- Topic(话题)
案例:利用SpringAMQP演示TopicExchange的使用
实现思路
利用@RabbitListener声明Exchange、Queue、RoutingKey
在consumer服务中,编写两个消费者方法,分别监听topic.queue1和topic.queue2
在publisher中编写测试方法,向itcast.topic发送消息
-发送者(消息发送)
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendTopicExchange() { // 交换机名称 String exchangeName = "itcast.topic"; // 消息 String message = "今天天气不错,我的心情好极了!"; // 发送消息 rabbitTemplate.convertAndSend(exchangeName, "china.weather", message); } }
-消费者(消息接收)
@Component public class SpringRabbitListener { @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue1"),//队列名称 exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC),//交换机名称 和 交换机类型topic key = "china.#" //key )) public void listenTopicQueue1(String msg){ System.out.println("消费者接收到topic.queue1的消息:【" + msg + "】"); } @RabbitListener(bindings = @QueueBinding( value = @Queue(name = "topic.queue2"), exchange = @Exchange(name = "itcast.topic", type = ExchangeTypes.TOPIC), key = "#.news" )) public void listenTopicQueue2(String msg){ System.out.println("消费者接收到topic.queue2的消息:【" + msg + "】"); } }
效果展示:
启动消费者:
启动发送者:
SpringAMQP -- 消息转换器
在SpringAMQP的发送方法中,接收消息的类型是Object,也就是说我们可以发送任意对象类型的消息,SpringAMQP会帮我们序列化为字节后发送
声明队列
@Configuration public class FanoutConfig { @Bean public Queue objectQueue(){ //声明队列 return new Queue("object.queue"); } }
向队列发布消息
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendObjectQueue(){ Map<String,Object> msg = new HashMap<>(); msg.put("name","柳岩"); msg.put("age",21); rabbitTemplate.convertAndSend("object.queue",msg); } }
效果:
我们查看一下消息的内容,没有发现柳岩,而且类型是
content_type: application/x-java-serialized-object,将我们的对象做序列化,也就是jdk的序列化方式(性能比较差,安全性有问题,数据长度过长)
所以我们非常不推荐我们用这种默认的方式
修改JDK序列化使得数据是JSON形式发送
Spring对消息对象的处理是由org.springframework.amqp.support.converter.MessageConverter来处理的
而默认实现是SimpleMessageConverter,基于JDK的ObjectOutputStream完成序列化。
如果修改只需要定义一个MessageConverter类型的Bean即可。推荐使用JSON方式序列化。
实现方式
-发送者(消息发送)
1、引依赖(由于发送方publisher和接收方consumer必须使用相同的MessageConverter,所以这里的依赖直接放在父工程的pom文件即可。)
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
2、在publisher服务声明MessageConverter的Bean,我们一旦声明。就会把默认的给覆盖掉,这是spring自动装配的原理
注意!!! 一定是下面的两个类,不要覆盖错了
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
@Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); }
清空队列中的内容,重新运行,在页面的队列中查看,发现这次是汉字了,效果如图:
-消费者(消息接收)
1、引依赖(由于发送方publisher和接收方consumer必须使用相同的MessageConverter,所以这里的依赖直接放在父工程的pom文件即可。)
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency>
2、在consumer服务声明MessageConverter的Bean,我们一旦声明。就会把默认的给覆盖掉,这是spring自动装配的原理
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
3、消息监听
@Component public class SpringRabbitListener { @RabbitListener(queues = "object.queue") public void listenObjectQueue(Map<String,Object> msg){ System.out.println("接收到object.queue的消息:" + msg); } }
效果如图:
总结: