专栏目录
0.docker快速入门
1.初识微服务
2.Gateway网关路由
3.Nacos配置管理
4.Sentinel微服务保护
5.Seata分布式事务管理
6.RabbitMQ消息队列
7.SpringAMQP消息队列
文章目录
SpringAMQP消息队列
学习了RabbitMQ,对于消息队列的整体工作流程已经有了一定的了解,但是在实际开发的时候,肯定不会在控制台操作后再二次搬运到程序上,而是应该基于编程的方式,直接在程序运行时就进行,降低出错的概率。
虽然RabbitMQ提供了各种不同语言的客户端,但提供的Java客户端编码相对复杂。
而Spring的官方刚好基于RabbitMQ提供了这样一套消息收发的模板工具:SpringAMQP。并且还基于SpringBoot对其实现了自动装配,使用起来非常方便。
SpringAMQP提供了三个功能:
- 自动声明队列、交换机及其绑定关系
- 基于注解的监听器模式,异步接收消息
- 封装了RabbitTemplate工具,用于发送消息
1. 相关地址
2. 快速使用
2.1 导入依赖
①在父工程中导入即可
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
②在Rabbit控制台中创建新队列,命名为demo.queue
2.2 消息发送
①在发送者的application.yaml
中配置信息
spring:
rabbitmq:
host: # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /maxbao # 虚拟主机
username: jiabao # 用户名
password: yu123123 # 密码
②编写测试类,实现消息发送:采用消息生产者->队列->消息消费者,这种简易测试
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "demo.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
③运行后就可以在控制台看到发送到队列的消息
2.3 消息接收
①在消费者的application.yaml
中配置信息
spring:
rabbitmq:
host: # 你的虚拟机IP
port: 5672 # 端口
virtual-host: /maxbao # 虚拟主机
username: jiabao # 用户名
password: yu123123 # 密码
②创建消息接收类
@Component
public class SpringRabbitListener {
// 利用RabbitListener来声明要监听的队列信息
// 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。
// 可以看到方法体中接收的就是消息体的内容
@RabbitListener(queues = "demo.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
3. WorkQueues模型
该模型实现多个消费者共享同一个队列中的信息,共同进行消息处理,大大提高消息处理的速度
3.1 模拟大量消息发送
参考3.2.2的消息发送设置
@Test
public void testWorkQueue() throws InterruptedException {
// 队列名称
String queueName = "demo.queue";
// 消息
String message = "hello, message_";
for (int i = 0; i < 50; i++) {
// 发送消息,每20毫秒发送一次,相当于每秒发送50条消息
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
3.2 消息接收
模拟多个消费者绑定同一个队列,我们在消费者服务的SpringRabbitListener中添加2个新的方法:
@RabbitListener(queues = "work.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "work.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(200);
}
该实现方式是将消息平均分配给每个消费者,并没有考虑到消费者的处理能力。
会导致出现部分消费者空闲,部分消费者忙的情况。没有充分利用每一个消费者的能力。
3.3 优化改进
解决上述分配不合理问题,在消费者的application.yaml
中配置信息即可
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
3.4 总结
Work模型的使用:
- 多个消费者绑定到一个队列,同一条消息只会被一个消费者处理
- 通过设置prefetch来控制消费者预取的消息数量
4. 交换机类型
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
交换机的类型有四种:
- Fanout:广播,将消息交给所有绑定到交换机的队列。我们最早在控制台使用的正是Fanout交换机
- Direct:订阅,基于RoutingKey(路由key)发送给订阅了消息的队列
- Topic:通配符订阅,与Direct类似,只不过RoutingKey可以使用通配符
- Headers:头匹配,基于MQ的消息头匹配,用的较少
4.1 Fanout交换机
广播式交换机,允许绑定多个队列,但生产者只能把消息发送到交换机上,交换机把消息发送给绑定过的所有队列,订阅队列的消费者都能拿到消息。
①创建交换机"exchange.fanout"
②创建队列“fanout.queue1”和“fanout.queue2”,并绑定到交换机上
③消息发送
@Test
public void testFanoutExchange() {
// 交换机名称
String exchangeName = "exchange.fanout";
// 消息
String message = "hello, everyone!";
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
④消息接收
@RabbitListener(queues = "fanout.queue1")
public void listenFanoutQueue1(String msg) {
System.out.println("消费者1接收到Fanout消息:【" + msg + "】");
}
@RabbitListener(queues = "fanout.queue2")
public void listenFanoutQueue2(String msg) {
System.out.println("消费者2接收到Fanout消息:【" + msg + "】");
}
4.2 Direct交换机
在Fanout模式中,一条消息,会被所有订阅的队列都消费。如果我们希望不同的消息被不同的队列消费,就要用到Direct类型的Exchange。
Direct类型的Exchange在进行队列绑定时需要指定一个 RoutingKey
;消息的发送方在向 Exchange发送消息时,也必须指定消息的 RoutingKey
;只有队列的Routingkey
与消息的 Routing key
完全一致,才会接收到消息
①创建direct类型exchange
②创建并绑定队列,且指定routingKey
③消息发送
@Test
public void testSendDirectExchange() {
// 交换机名称
String exchangeName = "exchange.direct";
// 消息
String message = "新年快乐!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
④消息接收
@RabbitListener(queues = "direct.queue1")
public void listenDirectQueue1(String msg) {
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "direct.queue2")
public void listenDirectQueue2(String msg) {
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
4.3 Topic交换机
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。
只不过Topic
类型Exchange
可以让队列在绑定BindingKey
的时候使用通配符!
BindingKey:由一个或多个单词组成,之间用.
隔开
- #:匹配一个或多个词语
- *:匹配一个词
示例:
假如此时发送的消息使用的RoutingKey共有四种:
- china.news 代表有中国的新闻消息;
- china.weather 代表中国的天气消息;
- japan.news 则代表日本新闻
- japan.weather 代表日本的天气消息;
topic.queue1:绑定的是china.# ,凡是以 china.开头的routing key 都会被匹配到,包括:
- china.news
- china.weather
topic.queue2:绑定的是#.news ,凡是以 .news结尾的 routing key 都会被匹配。包括:
- china.news
- japan.news
①创建Topic类型的
Exchange
②绑定队列并设置BindingKey
③消息发送
/**
* topicExchange
*/
@Test
public void testSendTopicExchange() {
// 交换机名称
String exchangeName = "exchange.topic";
// 消息
String message = "新年快乐!";
// 发送消息
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
④消息接收
@RabbitListener(queues = "topic.queue1")
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(queues = "topic.queue2")
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
5. 声明式绑定队列和交换机
在实际开发时,队列和交换机是程序员定义的。那么程序员就需要把程序中运行的所有队列和交换机都写下来,在这个过程中是很容易出现错误的。
因此推荐的做法是由程序启动时检查队列和交换机是否存在,如果不存在自动创建。
我们可以使用SpringAMQP提供的Queue接口来创建
5.1 实现Fanout交换机
import ...
@Configuration
public class FanoutConfig {
/**
* 声明交换机
* @return Fanout类型交换机
*/
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("exchange.fanout");
}
/**
* 第1个队列
*/
@Bean
public Queue fanoutQueue1(){
return new Queue("fanout.queue1");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
/**
* 第2个队列
*/
@Bean
public Queue fanoutQueue2(){
return new Queue("fanout.queue2");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
5.2 实现Direct交换机
实现的时候,每个Key都需要编写一个binding
import ...
@Configuration
public class DirectConfig {
/**
* 声明交换机
* @return Direct类型交换机
*/
@Bean
public DirectExchange directExchange(){
return ExchangeBuilder.directExchange("exchange.direct").build();
}
/**
* 第1个队列
*/
@Bean
public Queue directQueue1(){
return new Queue("direct.queue1");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1WithRed(Queue directQueue1, DirectExchange directExchange){
return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1WithBlue(Queue directQueue1, DirectExchange directExchange){
return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
}
/**
* 第2个队列
*/
@Bean
public Queue directQueue2(){
return new Queue("direct.queue2");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2WithRed(Queue directQueue2, DirectExchange directExchange){
return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue2WithYellow(Queue directQueue2, DirectExchange directExchange){
return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
}
}
5.3 注解式绑定
通过注解的方式来创建队列并绑定交换机
5.3.1 注解式Direct
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "exchange.direct", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】");
}
5.3.2 注解式Topic
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "exchange.topic", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String msg){
System.out.println("消费者1接收到topic.queue1的消息:【" + msg + "】");
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "hmall.topic", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String msg){
System.out.println("消费者2接收到topic.queue2的消息:【" + msg + "】");
}
5.4 消息格式转换
Spring的消息发送代码接收的消息体是一个Object,在数据传输时,它会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
但是默认情况下Spring采用的序列化方式是JDK序列化,会存在数据体积过大、有安全漏洞、可读性差的问题
我们可以使用JSON格式来解决这个问题
5.4.1 配置JSON转换器
①生产者和消费者都需要导入依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.10</version>
</dependency>
若已经引入
spring-boot-starter-web
依赖,则无需再次引入Jackson
依赖
②配置消息转换器,在生产者和消费者服务的启动类中都需要添加一个Bean:
@Bean
public MessageConverter messageConverter(){
// 1.定义消息转换器
Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
// 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
jackson2JsonMessageConverter.setCreateMessageIds(true);
return jackson2JsonMessageConverter;
}
5.4.2 消费者接收消息
注意传参是Map<String,Object>
@RabbitListener(queues = "object.queue")
public void listenSimpleQueueMessage(Map<String, Object> msg) throws InterruptedException {
System.out.println("消费者接收到object.queue消息:【" + msg + "】");
}
6. 总结
SpringAMQP可以基于SpringBoot对其实现了自动装配
使用时:
- 引入依赖
- 配置application
- 注解式编写消息监听类
- 编写消息发送逻辑,使用RabbitTemplate提供的接口
- 注意灵活使用交换机的不同种类型,以及bindingKey