该篇文章内容较多,包括有rabbitMq相关的一些简单理论介绍,集成spring Boot,provider消息推送实例,consumer消息消费实例,Direct(直连类型交换机)、Fanout(广播类型交换机)的使用、死信队列等。
MQ使用场景
1、异步:
当一个服务调用链路过长时,而用户只关心前面的流程,例如下订单、减库存、增积分、发短信等等,而用户只关心我的订单是否创建成功,后面的流程可以放到MQ去通知下游系统。
2、解耦:
你一个订单流程,你扣积分,扣优惠券,发短信,扣库存。。。等等这么多业务要调用这么多的接口,每次加一个你要调用一个接口然后还要重新发布系统,写一次两次还好,
3、削峰:
简单,把请求放到队列里面,然后至于每秒消费多少请求,就看自己的服务器处理能力,你能处理5000QPS你就消费这么多,可能会比正常的慢一点,但是不至于打挂服务器,等流量高峰下去了,你的服务也就没压力了。
rabbitMQ工作模型
首先先介绍一个简单的一个消息推送到接收的流程,提供一个简单的图:
图中的Producer就是消息发送者服务,Broker是rabbitMQ服务器包括VHost虚拟主机(可以根据不同业务创建不同的用户分配不同的VHost),Consumer为消息消费之,在rabbitMQ中支持push和pull两种消费方式,在spring集成中之封装了push方式,当消息发送者发送一条消息(直连方式)并指定了Exchange和BindingKey,消息首先会路由到指定交换机,交换机会根据指定的BindingKey发送到相应的队列,有该队列的消费者消费。
交换机类型
常用的交换机有以下三种,因为消费者是从队列获取信息的,队列是绑定交换机的(一般),所以对应的消息推送/接收模式也会有以下几种:
Direct Exchange
直连型交换机,根据消息携带的路由键将消息投递给对应队列。
大致流程,有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。
然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
Fanout Exchange
扇型交换机,这个交换机没有路由键概念,就算你绑了路由键也是无视的。 这个交换机在接收到消息后,会直接转发到绑定到它上面的所有队列。
Topic Exchange
主题交换机,这个交换机其实跟直连交换机流程差不多,但是它的特点就是在它的路由键和绑定键之间是有规则的。
简单地介绍下规则:*和#
*:匹配一个单词,例如ab.bd.lo为三个单词以“.”为分隔符。
#:匹配一个或多个单词。
安装rabbitMQ
以docker-compose为例,下面是docker-compose.yml
version: '2'
services:
rabbitmq:
hostname: myrabbitmq
image: rabbitmq:management
ports:
- 15673:15672
- 5673:5672
restart: always
volumes:
- ./data/rabbitMQ:/var/lib/rabbitmq
environment:
- RABBITMQ_DEFAULT_USER=root
- RABBITMQ_DEFAULT_PASS=root
启动rabbitMQ:docker-compose up -d,浏览器输入http://127.0.0.1:15673
用户:root,密码:root登录即可。
代码部分
集成Spring Boot
rabbitMQ依赖包,amqp(即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
</dependencies>
实现直连交换机模型
配置文件
spring:
application:
name: rabbitmq-provider
rabbitmq:
host: 127.0.0.1
port: 5673
username: root
password: root
virtual-host: /
创建消息生产者配置类
@Configuration
public class RabbitMQConfiguration {
/**
* 声明springQueue队列
* @return
*/
@Bean
public Queue springQueue() {
return new Queue("spring_queue",true);
}
/**
* 声明dubboQueue队列
* @return
*/
@Bean
public Queue dubboQueue() {
return new Queue("dubbo_queue",true);
}
/**
* 声明dubboQueue队列
* @return
*/
@Bean
public Queue vueQueue() {
return new Queue("vue_queue",true);
}
/**
* 声明frame_exchange直连类型交换机
* @return
*/
@Bean
DirectExchange frameDirectExchange() {
return new DirectExchange("frame_exchange",true,false);
}
/**
* 队列springQueue与交换机frame_exchange绑定
* @return
*/
@Bean
Binding bindingDirectSpring() {
return BindingBuilder.bind(springQueue()).to(frameDirectExchange()).with("spring");
}
/**
* 队列dubboQueue与交换机frame_exchange绑定
* @return
*/
@Bean
Binding bindingDirectDubbo() {
return BindingBuilder.bind(dubboQueue()).to(frameDirectExchange()).with("dubbo");
}
/**
* 队列vueQueue与交换机frame_exchange绑定
* @return
*/
@Bean
Binding bindingDirectVue() {
return BindingBuilder.bind(vueQueue()).to(frameDirectExchange()).with("vue");
}
}
消息生产者测试
@SpringBootTest(classes = RabbitMQApplication.class)
public class ProductTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void springMessage(){
rabbitTemplate.convertAndSend("frame_exchange", "spring", "1111");
}
@Test
public void dubboMessage(){
rabbitTemplate.convertAndSend("frame_exchange", "dubbo", "1111");
}
@Test
public void vueMessage(){
rabbitTemplate.convertAndSend("frame_exchange", "vue", "1111");
}
}
消费者配置类
与生产者一样即可
监听队列
分监听对应队列
@Component
@RabbitListener(queues = "spring_queue")
public class SpringQueueConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("SpringQueueConsumer 消费消息:" + msg);
}
}
@Component
@RabbitListener(queues = "dubbo_queue")
public class DubboQueueConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("DubboQueueConsumer 消费消息:" + msg);
}
}
@Component
@RabbitListener(queues = "vue_queue")
public class VueQueueConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("VueQueueConsumer 消费消息:" + msg);
}
}
启动消费者
运行单元测试
@Test
public void springMessage(){
// BindingKey = "spring"
rabbitTemplate.convertAndSend("frame_exchange", "spring", "1111");
}
消费者输出:SpringQueueConsumer 消费消息:1111
@Test
public void dubboMessage(){
// BindingKey = "dubbo"
rabbitTemplate.convertAndSend("frame_exchange", "dubbo", "1111");
}
消费者输出:DubboQueueConsumer 消费消息:1111
@Test
public void vueMessage(){
// BindingKey = "vue"
rabbitTemplate.convertAndSend("frame_exchange", "vue", "1111");
}
消费者输出:VueQueueConsumer 消费消息:1111
实现广播类型交换机模型
配置类
@Configuration
public class FanoutExchangeConfiguration {
/**
* 声明springQueue队列
* @return
*/
@Bean
public Queue springQueue() {
return new Queue("spring_queue",true);
}
/**
* 声明dubboQueue队列
* @return
*/
@Bean
public Queue dubboQueue() {
return new Queue("dubbo_queue",true);
}
/**
* 声明fanout_exchange广播类型交换机
* @return
*/
@Bean
FanoutExchange frameFanoutExchange() {
return new FanoutExchange("fanout_exchange",true,false);
}
/**
* 队列dubboQueue与交换机fanout_exchange绑定
* @return
*/
@Bean
Binding bindingDirectDubbo() {
return BindingBuilder.bind(dubboQueue()).to(frameFanoutExchange());
}
/**
* 队列vueQueue与交换机fanout_exchange绑定
* @return
*/
@Bean
Binding bindingDirectVue() {
return BindingBuilder.bind(springQueue()).to(frameFanoutExchange());
}
}
测试
@Test
public void fanoutMessage(){
// 指定广播类型交换机 广播不需要BindingKey,只要与交换机绑定的所有消费者都会消费
rabbitTemplate.convertAndSend("fanout_exchange", null,"1111");
}
结果
两个消费者都有消费
SpringQueueConsumer 消费消息:1111
DubboQueueConsumer 消费消息:1111
死信队列
死信队列介绍
- 死信队列:DLX,
dead-letter-exchange
- 利用DLX,当消息在一个队列中变成死信
(dead message)
之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX
消息变成死信有以下几种情况
- 消息被拒绝(basic.reject / basic.nack),并且requeue = false
- 消息TTL过期
- 队列达到最大长度
死信处理过程
- DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
- 当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
- 可以监听这个队列中的消息做相应的处理。
引用:作者:若汐缘,链接:https://www.jianshu.com/p/986ee5eb78bc
设置死信队列的Exchange和Queue
@Configuration
public class DLXConfiguration {
/**
* 普通队列
*
* @return
*/
@Bean
public Queue simpleQueue() {
return QueueBuilder
.durable("simpleQueue")
//声明该队列的死信消息发送到的 交换机 (队列添加了这个参数之后会自动与该交换机绑定,并设置路由键,不需要开发者手动设置)
.withArgument("x-dead-letter-exchange", "DXLExchange")
//声明该队列死信消息在交换机的 路由键
.withArgument("x-dead-letter-routing-key", "dead-letter-routing-key")
.build();
}
/**
* 普通交换机
* @return
*/
@Bean
public DirectExchange simpleExchange() {
return new DirectExchange("simpleExchange",true, false);
}
/**
* 普通队列与交换机绑定
*
* @param simpleQueue 普通队列名
* @param simpleExchange 普通交换机名
* @return
*/
@Bean
public Binding binding(Queue simpleQueue, DirectExchange simpleExchange) {
return BindingBuilder.bind(simpleQueue).to(simpleExchange).with("simple");
}
/**
* 死信交换机
*
* @return
*/
@Bean
public Exchange deadLetterExchange() {
return new DirectExchange("DXLExchange", true, false);
}
/**
* 普通队列的死信消息 路由的队列
* 普通队列simpleQueue的死信投递到死信交换机`dead-letter-exchange`后再投递到该队列
* 用这个队列来接收simple-queue的死信消息
*
* @return
*/
@Bean
public Queue deadLetterQueue() {
return QueueBuilder.durable("deadLetterQueue").build();
}
/**
* 死信队列绑定死信交换机
*
* @param deadLetterQueue simple-queue对应的死信队列
* @param deadLetterExchange 通用死信交换机
* @return
*/
@Bean
public Binding deadLetterBinding(Queue deadLetterQueue, Exchange deadLetterExchange) {
return BindingBuilder.bind(deadLetterQueue).to(deadLetterExchange).with("dead-letter-routing-key").noargs();
}
}
定义消费者
@Component
public class Consumer {
@RabbitHandler
@RabbitListener(queues = "simpleQueue")
public void simpleQueue(String msg, Message message, Channel channel) throws IOException {
//拒收消息:消息的DeliveryTag,是否批量拒绝,是否重新入队
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
System.out.println("Consumer.simpleQueue 普通队列拒收消息 内容:" + msg);
}
@RabbitHandler
@RabbitListener(queues = "deadLetterQueue")
public void deadLetterQueue(String msg, Message message, Channel channel) throws IOException {
System.out.println("Consumer.deadLetterQueue 接收到死信消息: " + msg);
System.out.println("Consumer.deadLetterQueue 死信队列收到消息....消息路由键为:" + message.getMessageProperties().getReceivedRoutingKey());
}
}
测试
@Test
public void delMessage(){
// 正常发送消息
rabbitTemplate.convertAndSend("simpleExchange", "simple","死信队列测试!");
}
结果
我们发送正常消息的路由键为“simple”,拒收后发送到死信交换机的路由键为“dead-letter-routing-key”。
死信队列可用场景很多,例如设置队列超时时间,一旦超过该时间,就丢到死信队列处理,可用于下单多少时间内不付款,自动取消订单。
求个关注~~~