SpringBoot与消息(RabbitMQ)
SpringBoot与消息(JMS、AMQP、RabbitMQ)
部分参考:https://blog.csdn.net/qq_35387940/article/details/100514134,这篇帖子写的很不错
1、简介
1.1、大多应用中,可通过消息服务中间件来提升系统异步通信、扩展解耦能力
1.2、消息服务中两个重要概念
消息代理( message broker)和目的地( destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地
1.3。消息队列主要有两种形式的目的地
队列( queue):点对点消息通信( point-to-point)
主题( topIc):发布( publish)们阅( subscribe)消息通信
2、应用场景(老生常谈的场景)
2.1、user注册后,异步处理邮件呀、短息之类的
2.2、应用解耦
2.3、流量削峰(商品的抢购)
3、概述
3.1、大多数应用场景之中,可以通过消息中间件来提升系统异步通信、扩展解耦能力、
3.2、消息服务中的两个重要概念
- 消息代理 ,message broker
- 目的地,destination
- 当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到目的地。
3.3、消息队列主要有两种形式的目的地
- 队列(queue):点对点消息通信(point-to-point)
- 主题(topic):发布(publish)/订阅(subscribe)消息通信
3.4、点对点式(point-to-point)
- 消息者发送消息,消息大力将其放入一个代理之中,消息接收者从队列中获取消息内容,消息读取之后被移除队列
- 消息只能唯一的发送者和接收者,但并不是说只能有一个接收者
3.5、发布订阅式
- 发布者发送消息到主题,多个订阅者监听这个主题,那么就会在消息到达时同时接收到消息。
3.6、JMS(java Message Service) JAVA 消息服务
- 基于消息代理的规范。ActiveMQ
3.7、AMQP(Advanced Message Queuing Protovol)
- 高级消息队列,也是一个消息代理的规范,兼容JMS
- RabbitMQ 时AMQP的实现
4、JMS 和 AMQP 比较
JMS | AMOP | |
---|---|---|
定义 | Java Api | 网络线 级协议 |
跨平台 | 否 | 是 |
跨语言 | 否 | 是 |
Model | 提供两种消息模型:(1)、 Peer-2-Peer(2), Pub/sub | 提供了五种消息模型:(1)、direct exchange(2)、fanout exchange(3)、 topic change(4).headers exchange(5).system exchange本质来讲,后四种和JMS的pub/sub模型没有太大差别,仅是在路由机制上做了更详细的划分; |
支持消息类型 | 多种消息类型:TextMessage、MapMessage、BytesMessage、StreamMessage、ObjectMessage、Message(只有消息头和属性) | byte[] 当实际应用时,有复杂的消息,可以将消息序列化后发送。 |
综合评价 | JMS定义了IAVA API层面的标准;在java体系中,多个client 均可以通过JMS进行交互,不需要应用修改代码,但是其对跨言特性。 | AMQP定义了wire-level层的协议标准;天然具有跨平台、跨语均可以通过JMS进行交互 |
5、Spring 支持
- Spring-Jms提供了对jms的支持
- Spring-rabbit提供了对AMOP的支持
- 需要ConnectionFactory的实现来连接消息代理
- 提供JmsTemplate、RabbitTemplate来发送消息
- @JmsListener(JMS)、@RabbitListener(AMQP)注解在方法上监听消息代理发布的消息
- @EnableJms、@EnableRabbit开启支持
6、SpringBoot自动配置
- JmsAutoConfiguration
- RabbitAutoConfiguration
7、RabbitMQ
7.1、简介
**RabbitMQ是一个由erlang开发的AMQP的开源实现**
7.2、核心概念
- Message:消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则是由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其它消息的优先权)、delivery-mode(指出该消息可能需要永久性存储)等。
- Publisher:消息的生产者,也是一个向交换器发布消息的客户端应用程序。
- Exchange:交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。有4中类型:direct(默认)、fanout,topic和headers,不同的Exchange转发的策略由区别。
- Queue:消息队列,用来保存消息直接发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多喝队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
- Binding:绑定,用于消息队列和交换器之间的关联,一个绑定就是基于路由主键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange和Queue的绑定可以是多对多的关系。
- Connection:网络连接,比如一个tcp连接。
- Channel:信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅消息还是接收消息,这些动过都是通过信道完成,因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入的信道的概念,以服用一条TCP连接。
- Consumer:消费者,表示一个从消息队列中取到的消息的客户端应用程序。
- Virtual Host:虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念基础,必须在连接时指定、RabbitMQ默认的vhost 是 / 。
- Broker:表示消息队列服务器实体
8、RabbitMq的运行机制
8.1、AMQP中的消息路由
- AMQP中消息的路由过程和java开发这熟悉的JMS存在一些差别,AMQP中增加了Exchange和Binding的角色。生产者吧消息发不发哦Exchange上,消息最终到达队列并被消费者接收,而Bing决定交换器的消息应该发送到那个队列。
8.2、Exchange类型
- Exchange分发消息是根据类型的不同分发策略有所区别,目前有四种类型:direct、fanout、topic、headers。headers匹配AMQP消息的headers而不是路由键,headers交换器和direct交换器完全一致,但性能差很多,目前几乎用不到,所以直接看另外三种类型。
9、SpringBoot整合RabbitMQ
9.1、docker安装RabbitMQ
-
记录一个docker 安裝时报的错误
解决办法:
停止然后重新启动docker 服务 $ sudo systemctl stop docker.service $ sudo systemctl start docker.service
- 安装
sudo docker pull rabbitmq:management
-
启动
docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq 263c941f71ea
-
测试 ip:15672
账号密码:guest
9.2、控制台测试
-
添加Exchange
-
添加队列
-
添加绑定关系
-
测试发送消息(direct点对点)
-
给Exchange: 1201test-direct推送消息
-
可以看到对应的队列已经收到消息
-
测试发送消息(fanout模式,不管路由键是什么,所有队列都可以收到)
-
给Exchange: 1201test-fanout推消息
-
可以看到所有的队列已经收到消息
-
测试发送消息(topic模式,根据路由键的匹配规则来发送)
-
给Exchange: 1201testtopic推消息
-
收到消息
-
给Exchange: 1201testtopic推消息
-
收到消息
-
9.3、新建maven工程(消息的发送者-provider)
-
pom
<?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"> <parent> <artifactId>Cloud</artifactId> <groupId>com.riest</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>SpingBoot-RabbitMq</artifactId> <dependencies> <!--消息队列--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!--消息队列连接池--> <dependency> <groupId>org.messaginghub</groupId> <artifactId>pooled-jms</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> </project>
-
yml
server: port: 9099 # 以下都是最简单的配置 spring: rabbitmq: host: xxxxxxx username: guest password: guest port: 5672 # virtual-host: 默认就是 /
-
主启动
package com.riest; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * ClassName:RabbitMqApplication * Describe: * 1、org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration * 配置了连接工厂 * ...... * 2、org.springframework.boot.autoconfigure.amqp.RabbitProperties * 封装了连接参数 * ...... * * 3、RabbitTemplate提供发送接收消息 * 4、AmqpAdmin 系统管理给你组件 * * Author:DGJ * Data:2020/12/1 17:40 */ @SpringBootApplication @EnableRabbit public class RabbitMqApplication { public static void main(String[] args) { SpringApplication.run(RabbitMqApplication.class,args); } }
-
service(当前测试的队列时之前在控制台创建好的)
package com.riest.service; import com.sun.media.jfxmedia.logging.Logger; import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.HashMap; /** * ClassName:RabbitmqService * Describe: * Author:DGJ * Data:2020/12/1 20:00 */ @Service @Slf4j public class RabbitmqService { @Autowired private RabbitTemplate rabbitTemplate; /**rabbitTemplate * 测试 direct 模式 */ public void SendMsg(){ try { HashMap<String, String> stringStringHashMap = new HashMap<>(); stringStringHashMap.put("k1","v1"); stringStringHashMap.put("k2","v2"); rabbitTemplate.convertAndSend("1201test-direct","test.emps",stringStringHashMap); }catch (Exception e){ e.printStackTrace(); } } /** * 测试fanout模式 */ public void sendMsgFanout(){ try { HashMap<String, String> stringStringHashMap = new HashMap<>(); stringStringHashMap.put("k1","v1"); stringStringHashMap.put("k2","v2"); rabbitTemplate.convertAndSend("1201test-fanout","test.emps",stringStringHashMap); }catch (Exception e){ e.printStackTrace(); } } /** * 测试topic模式 */ public void sendMsgTopic(){ try { HashMap<String, String> stringStringHashMap = new HashMap<>(); stringStringHashMap.put("k1","v1"); stringStringHashMap.put("k2","v2"); rabbitTemplate.convertAndSend("1201test-topic","test.emps",stringStringHashMap); }catch (Exception e){ e.printStackTrace(); } } }
-
controller
package com.riest.controller; import com.riest.service.RabbitmqService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; /** * ClassName:RabbitmqController * Describe: * Author:DGJ * Data:2020/12/1 20:06 */ @RestController public class RabbitmqController { @Autowired private RabbitmqService rabbitmqService; //------------------------------------------------------------ @RequestMapping(value = "/test",method = RequestMethod.GET) public String sendmsg(){ rabbitmqService.SendMsg(); return "ok"; } //------------------------------------------------------------------- @RequestMapping(value = "/testfanout",method = RequestMethod.GET) public String sendmsgFanout(){ rabbitmqService.sendMsgFanout(); return "ok"; } //-------------------------------------------------------------------- @RequestMapping(value = "/testtopic",method = RequestMethod.GET) public String sendmsgTopic(){ rabbitmqService.sendMsgTopic(); return "ok"; } }
-
测试前队列
-
测试diect(用已有的队列测试)
-
测试fanout(用已有的队列测试)
-
测试topic(用已有的队列测试)
-
项目中创建队列进行测试diect
config:
package com.riest.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ClassName:RabbitmqConf * Describe: 该配置类就是把我们之前在控制台创建交换机、队列、绑定的操作用代码来实现 * Author:DGJ * Data:2020/12/2 19:09 */ @Configuration public class RabbitmqConfDirect { /** * 创建队列 * durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 * exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable * autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 * return new Queue("TestDirectQueue",true,true,false); * * @return */ @Bean public Queue TestDirectQueue() { //一般设置一下队列的持久化就好,其余两个就是默认false return new Queue("1202-testQueue", true); } /** * Direct交换机 (点对点模式) * * @return */ @Bean public DirectExchange TestDirectExchange() { return new DirectExchange("1202-testDirectExchange", true, false); } /** * 绑定 将队列和交换机绑定, 并设置用于匹配键testDirectExchange * * @return */ @Bean public Binding bindingDirect() { return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("1202-testDirectExchange"); } }
**service**
```java
/**
* 测试创建的Direct
*/
public void sendMsgDirect(){
try {
HashMap<String, String> stringStringHashMap = new HashMap<>();
stringStringHashMap.put("k22","v22");
stringStringHashMap.put("k22","v22");
rabbitTemplate.convertAndSend("1202-testDirectExchange","1202-testDirectExchange",stringStringHashMap);
}catch (Exception e){
e.printStackTrace();
}
}
controller
@GetMapping("/sendDirect")
public String sendDirectMessage() {
rabbitmqService.sendMsgDirect();
return "ok";
}
http://localhost:9099/sendDirect
创建了交换机
创建了队列且收到了消息
** 和队列bind**
-
项目中创建测试fanout(广播模式)
config:
package com.riest.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.FanoutExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ClassName:RabbitmqConfFanout * Describe: 测试fanout模式 * Author:DGJ * Data:2020/12/2 19:42 */ @Configuration public class RabbitmqConfFanout { /** * 创建三个队列 :fanout.A fanout.B fanout.C * 将三个队列都绑定在交换机 fanoutExchange 上 * 因为是扇型交换机, 路由键无需配置,配置也不起作用 */ @Bean public Queue queueA() { return new Queue("fanout.A"); } @Bean public Queue queueB() { return new Queue("fanout.B"); } @Bean public Queue queueC() { return new Queue("fanout.C"); } @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange("fanoutExchange"); } @Bean public Binding bindingExchangeA() { return BindingBuilder.bind(queueA()).to(fanoutExchange()); } @Bean public Binding bindingExchangeB() { return BindingBuilder.bind(queueB()).to(fanoutExchange()); } @Bean public Binding bindingExchangeC() { return BindingBuilder.bind(queueC()).to(fanoutExchange()); } }
service:
/** * 测试创建的fanout */ public void testSendMsgFanout(){ try { HashMap<String, String> stringStringHashMap = new HashMap<>(); stringStringHashMap.put("k22","v22"); stringStringHashMap.put("k22","v22"); rabbitTemplate.convertAndSend("fanoutExchange","null",stringStringHashMap); }catch (Exception e){ e.printStackTrace(); } }
controller:
@GetMapping("/sendFanout") public String sendFanoutMessage() { rabbitmqService.testSendMsgFanout(); return "ok"; }
http://localhost:9099/sendFanout
创建了交换器
建立了绑定
创建了队列且收到消息
-
项目中创建测试topic(topic模式,根据不同的匹配规则来发送消息)
config:
package com.riest.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.TopicExchange; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * ClassName:RabbitmqConfFanout * Describe: 测试topic模式 * Author:DGJ * Data:2020/12/2 19:42 */ @Configuration public class RabbitmqConfTopic { /** * 绑定键 */ public final static String man = "topic.man"; public final static String woman = "topic.woman"; @Bean public Queue firstQueue() { return new Queue(RabbitmqConfTopic.man); } @Bean public Queue secondQueue() { return new Queue(RabbitmqConfTopic.woman); } @Bean TopicExchange exchange() { return new TopicExchange("topicExchange"); } /**topicExchange * 将firstQueue和topicExchange绑定,而且绑定的键值为topic.man * 这样只要是消息携带的路由键是topic.man,才会分发到该队列 * @return */ @Bean Binding bindingExchangeMessage() { return BindingBuilder.bind(firstQueue()).to(exchange()).with(man); } /**man * 将secondQueue和topicExchange绑定,而且绑定的键值为用上通配路由键规则topic.# * 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列 * @return */ @Bean Binding bindingExchangeMessage2() { return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#"); }}
service:
/** * 测试创建的topic */ public void testSendMsgTopic1(){ try { HashMap<String, String> stringStringHashMap = new HashMap<>(); stringStringHashMap.put("k22","v22"); stringStringHashMap.put("k22","v22"); rabbitTemplate.convertAndSend("topicExchange","topic.man",stringStringHashMap); }catch (Exception e){ e.printStackTrace(); } } /** * 测试创建的topic */ public void testSendMsgTopic2(){ try { HashMap<String, String> stringStringHashMap = new HashMap<>(); stringStringHashMap.put("k22","v22"); stringStringHashMap.put("k22","v22"); rabbitTemplate.convertAndSend("topicExchange","topic.woman",stringStringHashMap); }catch (Exception e){ e.printStackTrace(); } }
controller:
@GetMapping("/sendTopic1") public String sendTopic1Message() { rabbitmqService.testSendMsgTopic1(); return "ok"; } @GetMapping("/sendTopic2") public String sendTopic2Message() { rabbitmqService.testSendMsgTopic2(); return "ok"; }
http://localhost:9099/sendTopic1
http://localhost:9099/sendTopic2
创建了队列
建立了绑定
创建了队列且发送了消息
9.4 、新建consumer端(接收消息)
因为首次启动消息太多,所以本次测试时博主的二次测试,之前的消息都已经收到
我们只测试手动创建的队列接收发送消息
-
pom
<?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"> <parent> <artifactId>Cloud</artifactId> <groupId>com.riest</groupId> <version>1.0.0</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>SpringBoot-RabbitMQ-Consumer</artifactId> <dependencies> <!--消息队列--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!--消息队列连接池--> <dependency> <groupId>org.messaginghub</groupId> <artifactId>pooled-jms</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies> </project>
-
yml
server: port: 9100 # 以下都是最简单的配置 spring: rabbitmq: host: xxxxxx username: guest password: guest port: 5672 # virtual-host: 默认就是 /
-
主启动
package com.riest; import org.springframework.amqp.rabbit.annotation.EnableRabbit; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableRabbit public class RabbitMqApplication { public static void main(String[] args) { SpringApplication.run(RabbitMqApplication.class,args); } }
-
service
package com.riest.service; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Service; import sun.awt.SunHints; /** * ClassName:ListenRabbitmqService * Describe: * Author:DGJ * Data:2020/12/2 18:52 */ @Service public class ListenRabbitmqService { @RabbitListener(queues = "1202-testQueue") public void Listen1(Message message){ System.out.println("监听:1202-testQueue"); System.err.println(message.getBody().toString()); System.err.println(message.getMessageProperties()); } @RabbitListener(queues = "fanout.A") public void Listen2(Message message){ System.out.println("监听fanout.A队列"); System.err.println(message.getBody().toString()); System.err.println(message.getMessageProperties()); } @RabbitListener(queues = "fanout.B") public void Listen3(Message message){ System.out.println("监听fanout.B队列"); System.err.println(message.getBody().toString()); System.err.println(message.getMessageProperties()); } @RabbitListener(queues = "fanout.C") public void Listen4(Message message){ System.out.println("监听fanout.C队列"); System.err.println(message.getBody().toString()); System.err.println(message.getMessageProperties()); } @RabbitListener(queues = "topic.man") public void Listen5(Message message){ System.out.println("监听topic.man队列"); System.err.println(message.getBody().toString()); System.err.println(message.getMessageProperties()); } @RabbitListener(queues = "topic.woman") public void Listen6(Message message){ System.out.println("监听topic.woman队列"); System.err.println(message.getBody().toString()); System.err.println(message.getMessageProperties()); } }
-
测试diect类型,点对点
http://localhost:9099/sendDirect
可以看到已经监听到消息
-
测试fanout类型,广播模式,所有何其建立绑定的对俄都可以收到消息
http://localhost:9099/sendFanout
队列收到消息
-
测试topic(订阅模式)
http://localhost:9099/sendTopic1
根据匹配规则,如下队列收到消息
http://localhost:9099/sendTopic2
根据匹配规则,如下队列收到消息
10、消息的回调
后续补充
11、消息的确认
后续补充
12、消息的丢失
后续补充
13、两个工程的结构图
- 生产者端:
- 消费者端