RabbitMQ的工作流程
RabbitMQ中的一些角色作用
-
producer:生产者
-
consumer:消费者
-
exchange:交换机,负责消息路由
-
queue:队列,存储消息
-
Bindings:绑定交换机和队列的路由关系
RabbitMQ常用的交换机类型有: fanout 、direct 、topic
-
Fanout:会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中
-
direct:会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中
-
topic:会将消息路由到BindingKey和RoutingKey相匹配的队列中,这里的匹配规则稍微不同,它约定:BindingKey和RoutingKey一样都是由"."分隔的字符串;BindingKey中可以存在两种特殊字符“”和“#”,用于模糊匹配,其中" * "用于匹配一个单词,"#"用于匹配多个单词。
生产者发送消息的流程
-
生产者连接RabbitMQ,建立TCP连接( Connection),开启通信通道(Channel)
-
生产者声明一个Exchange(交换器),并设置相关属性,比如交换器类型、是否持久化等
-
生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等
-
生产者通过routingKey (路由Key)将交换器和队列绑定( binding )起来
-
生产者发送消息至RabbitMQ Broker,其中包含routingKey (路由键)、交换器等信息
-
相应的交换器根据接收到的routingKey 查找相匹配的队列。
-
如果找到,则将从生产者发送过来的消息存入相应的队列中。
-
如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
-
关闭信道,关闭连接
消费者接收消息的过程
-
消费者连接到RabbitMQ Broker ,建立一个连接(Connection ) ,开启通信通道(Channel) 。
-
消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数, 以及 做一些准备工作
-
等待RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。
-
消费者确认( ack) 接收到的消息。
-
RabbitMQ 从队列中删除相应己经被确认的消息。
-
关闭信道,关闭连接
生产者代码实现
public class PublisherTest {
@Test
public void testSendMessage() throws Exception {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("xxx.xxx.xxx.xxx");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("12345");
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
// 第一个参数是队列名称,第二个参数是否持久化,false表示在rabbitmq-server重启后就没有了
// 第三个参数表示该队列不是一个排外队列,否则一旦客户端断开,队列就删除了
// 第四个参数表示该队列是否自动删除,true表示一旦不使用了,系统删除该队列
// 第五个参数表示该队列的参数,该参数是Map集合,用于指定队列的属性
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
//这里使用名称为空的交换机,那么第二个参数路由key默认为队列名称
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
消费者代码实现
public class ConsumerTest {
public static void main(String[] args) throws Exception {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("xxx.xxx.xxx.xxx");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("12345");
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, (consumerTag,message) -> {
System.out.println("接收到消息为:" + new String(message.getBody(),"UTF-8"));
},consumerTag -> {});
System.out.println("等待接收消息。。。。");
}
}
RabbitMQ消息模型-案例
RabbitMQ官方提供了5个不同的Demo示例,对应了不同的消息模型
1.环境搭建
导入maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
配置文件yml
spring:
rabbitmq:
host: xxx.xxx.xxx.xxx # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: root # 用户名
password: 12345 # 密码
2.简单队列模式
HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:生产者,消费者,队列
生产者实现代码
@SpringBootTest
public class PublisherTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, simple.queue!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
消费者实现代码
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) {
log.info("spring 消费者接收到消息:{}",msg);
}
}
测试
启动springboot项目服务,然后执行生产者代码,发送消息,消费者监听到有消息过来输出。
c.dingmb.listener.SpringRabbitListener : spring 消费者接收到消息:hello, simple.queue!
3.WorkQueue
Work queues模型。让多个消费者绑定到一个队列,共同消费队列中的消息。每个消费者仅消费部分信息,可以达到负载均衡的效果。
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。此时就可以使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了。
生产者实现代码
@Test
public void testWorkQueue() throws InterruptedException {
String queueName = "simple.queue";
String message = "hello, message_";
for (int i = 1; i <= 50; i++) {
// 向队列中不停发送消息,模拟消息堆积
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
消费者实现代码
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
log.info("消费者1........接收到消息:{}",msg);
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
log.info("消费者2........接收到消息:{}",msg);
Thread.sleep(200);
}
}
问题与解决
启动boot应用,执行生产者代码发送消息,可以看到两个消费者获取到的消息都是25条,即使listenWorkQueue2消费者消费的时间较长,还是会慢慢消费,这样其实是不好的,因为这时listenWorkQueue1消费者已经消费完了消息,相当于空闲着,如果要实现按消费的能力消费消息,而不是负载均衡的方式平均分配消息的话,需要配置如下
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
再次测试,可以看到大部分都是listenWorkQueue1消费者消费的,不再平均分配消息消费了。
4.发布/订阅
在订阅模型中,多了一个exchange角色,交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
-
Fanout:广播,将消息交给所有绑定到交换机的队列
-
Direct:定向,把消息交给符合指定routing key 的队列
-
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
使用fanout类型交换器,routingKey忽略。每个消费者定义生成一个队列并绑定到同一个
Exchange,每个消费者都可以消费到完整的消息。
生产者实现代码
@Test
public void testFanoutExchange() {
String exchangeName = "fanoutExchange";
String message = "hello, fanout2!";
//这里路由key是空的,与该交换机绑定的所有queue都会收到消息
//控制台输出:消费者FanoutQueue1,消费者FanoutQueue2 都接收到了消息
rabbitTemplate.convertAndSend(exchangeName, "", message);
}
消费者实现代码
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(value = "fanout.queue1"),
exchange = @Exchange(value = "fanoutExchange",type = ExchangeTypes.FANOUT)
)
})
public void listenFanoutQueue1(String msg) {
log.info("消费者FanoutQueue1........接收到消息:{}",msg);
}
@RabbitListener(bindings = {
@QueueBinding(value = @Queue(value = "fanout.queue2"),
exchange = @Exchange(value = "fanoutExchange",type = ExchangeTypes.FANOUT)
)
})
public void listenFanoutQueue2(String msg) {
log.info("消费者FanoutQueue2........接收到消息:{}",msg);
}
}
交换机的作用
-
接收publisher发送的消息
-
将消息按照规则路由到与之绑定的队列
-
不能缓存消息,路由失败,消息丢失
-
FanoutExchange的会将消息路由到每个绑定的队列
5.直连模式(direct)
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
-
队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) -
消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 -
Exchange不再把消息交给每一个绑定的队列,而是根据消息的
Routing Key
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
生产者实现代码
@Test
public void testSendDirectExchange() {
String exchangeName = "directExchange";
//消费者listenDirectQueue1,消费者listenDirectQueue2 都接收到了消息
//String message = "message RoutingKey = red!";
//rabbitTemplate.convertAndSend(exchangeName, "red", message);
//消费者listenDirectQueue1 接收到了消息
//String message = "message RoutingKey = blue!";
//rabbitTemplate.convertAndSend(exchangeName, "blue", message);
//消费者listenDirectQueue2 接收到了消息
String message = "message RoutingKey = yellow!";
rabbitTemplate.convertAndSend(exchangeName, "yellow", message);
}
消费者实现代码
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "directExchange", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}
))
public void listenDirectQueue1(String msg){
log.info("消费者listenDirectQueue1........接收到消息:{}",msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "directExchange", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}
))
public void listenDirectQueue2(String msg){
log.info("消费者listenDirectQueue2........接收到消息:{}",msg);
}
}
Direct交换机与Fanout交换机的差异
-
Fanout交换机将消息路由给每一个与之绑定的队列
-
Direct交换机根据RoutingKey判断路由给哪个队列
-
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
6.Topic模式
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
生产者实现代码
@Test
public void testSendTopicExchange() {
String exchangeName = "topicExchange";
String message = "hello topic exchange!";
//消费者listenTopicQueue1 接收到了消息
//rabbitTemplate.convertAndSend(exchangeName, "name.a.b.c", message);
//消费者listenTopicQueue2,消费者listenTopicQueue1 都接收到了消息
rabbitTemplate.convertAndSend(exchangeName, "name.a", message);
}
消费者实现代码
@Slf4j
@Component
public class SpringRabbitListener {
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "topicExchange", type = ExchangeTypes.TOPIC),
key = "name.#"
))
public void listenTopicQueue1(String msg){
log.info("消费者listenTopicQueue1........接收到消息:{}",msg);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "topicExchange", type = ExchangeTypes.TOPIC),
key = "name.*"
))
public void listenTopicQueue2(String msg){
log.info("消费者listenTopicQueue2........接收到消息:{}",msg);
}
}