介绍
消息队列
消息:指的是应用间传递的数据。
消息队列是一种应用程序对应用程序直接的通信方法,应用程序通过读出队列的消息来通信,是一种跨进程的、异步的通信机制,用于上下游传递信息,消息包括文本字符和json等。
消息中间件,利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,在分布式环境下扩展进程间的通信。
消息队列的传递模式分为两种方式:点对点,这种方式基于队列实现;还有一种是发布订阅模式(广播模式)。
RabbitMQ
采用Erlang语言实现AMQP协议的消息中间件,用于分布式系统中存储转发消息。轻量级,易于在本地和云端部署。支持多种消息传递协议。RabbitMQ可以部署在分布式和联合配置中,以满足大规模、高可用性的要求。
RabbitMQ整体上是一个生产者和消费者模型,主要负责接收、存储和转发消息。
RabbitMQ安装
使用docker拉取RabbitMQ镜像
docker search rabbitmq:查找镜像
docker pull rabbitmq :3.8.3-management:一般拉取-management结尾的版本(有图形化界面)
docker run -d --name myrabbitmq -p 15672:15672 -p 5672:5672 rabbitmq:3.8.3-management:启动镜像服务
-d :后台运行;
--name :设置镜像的名称;
-p 15672:15672 :rabbitmq的后台管理端口映射;【这里需要做端口映射才能正常访问】
-p 5672:5672 :RabbitMQ服务端口映射;
登录:虚拟ip+管理端ip15672,用户名:guest;密码:guest。如果正常访问,登录成功,即为安装成功。
整体架构
架构图
运转流程
内部大致的运转流程如下:
生产者发送消息:
生产者连接到RabbitMQ Broker,建立了一个连接(Connection),开启信道(Channel);
生产者声明一个交换器,并设置交换机类型、是否持久化等相关属性;
生产者声明一个队列,并设置相关属性:是否排他、是否持久化、是否自动删除等;
生产者通过路由键将交换器和队列进行绑定;
生产者发送消息到RabbitMQ Broker,包含路由键、交换器等信息;
交换器根据收到的路由键查询匹配的队列,如果找到了队列,将消息存入相应的队列中,否则,根据生产者配置的属性选择丢弃还是回退给生产者;
最后关闭信道、关闭连接。
消费者接收消息:
消费者连接到RabbitMQ Broker,建立一个连接,开启一个信道(Channel);
向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及一些准备工作。
等待RabbitMQ Broker回应并投递相应队列中的消息,消费者接收消息;
消费者确认(ack)接收到的消息;
RabbitMQ从队列中删除相应已经被确认的消息;
最后关闭信道,关闭连接。
相关概念解释
Producer:生产者,消息的投递方;
生产者创建信息,发布到RabbitMQ,消息一般是两个部分:一个是消息体,是一个带有业务逻辑结构的数据;另一部分是消息的标签,用来表述这条消息,比如一个交换器的名称和一个路由键。
Consumer:消费者,消息的接收方;
消费者连接到RabbitMQ服务器,订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体而已。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,不会知道消息的生产者是谁。
Broker:消息中间件的服务节点,相当于消息的中转站。
RabbitMQ的服务节点或者是RabbitMQ的服务实例。
Queue:队列,RabbitMQ的内部对象,用于存储消息。
RabbitMQ的消息只能存储在队列中,最终将消息投递到队列中,消费者可以从队列中获取消息来消费;
多个消费者可以订阅同一个队列,这时队列中的消息会被平摊(轮询)分给多个消费者进行处理,不是每个消费者都收到所有的消息进行消费。
Exchange:交换器。
生产者将消息发送到Exchange,由交换器将消息路由到队列中,若是路由不到,对消息进行丢弃或者是返回给生产者。
fanout交换器:把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中;
direct交换器:把消息路由到那些BindingKey和RoutingKey完全匹配的队列中;
topic交换器:按照匹配规则进行消息传递,基本也是按照BindingKey和RoutingKey进行匹配的,但是可以规定匹配的规则:
RoutingKey:生产者将消息发给交换器的时候,一般会指定一个RoutingKey,用来指定这个消息的路由规则,而这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才生效;
BindingKey:绑定。通过绑定将交换器和队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确将消息路由到队列了。
RoutingKey为一个“.”分隔的字符串,如“com.rabbitmq.client”,被“.”分隔开的每一段独立的字符串称为一个单词;
BindingKey同上,也是“.”分隔的字符串;但是BindingKey可以存在两种特殊字符串"*"和"#",用于做模糊匹配。"*"用来匹配一个单词,"#"用来匹配多规格单词(允许匹配0个)。
配置路由键为“com.rabbitmq.client”的消息会路由到Queue1和Queue2中;
路由键“com.hidden.client”消息只会路由到Queue2中;
路由键“com.hidden.demo”的消息只会路由到Queue2中;
路由键“java.rabbitmq.demo”的消息只会路由到Queue1中;
路由键为“java.util.concurrent”消息会丢失或者是返回给生产者。
headers交换器:不依赖于路由键的匹配规则来路由消息,是根据发送的消息内容中的headers属性进行匹配。heaers类型的交换器性能会很差,不实用,基本不会使用。
工作模式
生产者将消息放入队列中,消费者进行监听(while)队列,若有消息则将其消费掉,队列中会自动清除消息体;
这种模式很容易消息丢失,可以设置手动ack,设置后,在消息处理完毕后需要及时发送ack消息给队列,否则会内存溢出。
生产者将消息放入队列中,可以有多个消费者,这些消费者同时监听同一个队列,这些消费者是竞争关系,谁先拿到谁就负责消费消息。
在 高并发情况下,默认会产生一个消息被多个消费者共同使用,可以设置一个开关Syncronize,保证一条消息只能被一个消费者使用。
生产者将消息发给Broker,由交换机将消息转发到绑定在该交换器的每个队列中,所有绑定这个交换器的队列都会接收到消息。消费者可以有多个,它们可以根据自己的要求监听所需的队列。
消息生产者将消息发送给交换机,按照路由匹配对应的消息队列。这个路由为字符串,当前产生的消息携带路由字符(对象的方法)。交换机根据路由的key匹配对应的消息队列,当消费者监听对应的队列才能消费此消息;
路由字符串根据业务功能定义;
从系统的代码逻辑中获取对应的功能字符串,将消息任务扔到对应的队列中。
*和#代表通配符,*表示多个单词,#表示一个单词;
路由功能添加模糊匹配;
消息生产者产生消息,把消息发送给交换机;
交换机根据key的规则进行模糊匹配到对应的队列,由队列的监听消费者接收消息并消费。
举个例子【topic主题模式】
利用topic主题模式,实现消息的发送和匹配接收。
yaml配置
spring:
rabbitmq:
host: 192.168.209.200
port: 5672
username: guest
password: guest
#连接超时,单位毫秒,0表示无穷大,不超时
connection-timeout: 10000s
# 是否启用【发布返回】
publisher-returns: true
# correlated(发布确认模式),simple(使用waitForConfirms()或waitForConfirmsOrDie()发布时),none(发布非确认模式)
publisher-confirm-type: correlated
#虚拟主机,用于不同的工作组
virtual-host: /
server:
port: 8083
配置类
@Configuration
public class TopicRabbitConfig {
/**
* 队列msg
*/
@Bean("msg")
public Queue getQueueMsg() {
return new Queue("msg.queue", true, false, false);
}
/**
* 队列notes
*/
@Bean("notes")
public Queue getQueueNotes() {
return new Queue("notes.queue", true, false, false);
}
/**
* 交换机
*/
@Bean
public TopicExchange getTopicExchange() {
return new TopicExchange("topic-exchange", true, false);
}
/**
* 绑定队列msg到交换器
* 绑定键:#.topic.*
* 表示:能匹配到topic前面出现的所有单词,能匹配到topic后面出现的一个单词
*
*/
@Bean
public Binding bindingMsg() {
return BindingBuilder.bind(getQueueMsg()).to(getTopicExchange()).with("#.topic.*");
}
/**
* 绑定队列notes到交换器
* 绑定键为:#.topic.#
* 表示能匹配到topic前面的所有出现的单词,也能匹配到topic后面出现的所有单词
*/
@Bean
public Binding bindingNotes() {
return BindingBuilder.bind(getQueueNotes()).to(getTopicExchange()).with("#.topic.#");
}
}
生产者
@Component
@Slf4j
public class TopicProducer {
@Resource
private AmqpTemplate amqpTemplate;
public void send(String msg) {
log.info("发送的消息为:" + msg);
// 将消息发送到tpic-exchange交换器,根据路由键:rabbit.test.topic.msg.notes进行转发[该路由键决定着把消息存入哪些队列中]
amqpTemplate.convertAndSend("topic-exchange", "rabbit.test.topic.msg.notes", msg);
}
}
消费者
@Component
@Slf4j
public class TopicConsumer {
/**
* msg.queue队列监听
*/
@RabbitListener(queues = "msg.queue")
public void receiveMsg(String msg) {
log.info("监听msg.queue队列中的消息.....收到的消息为:{}", msg);
}
/**
* notes.queue队列监听
*/
@RabbitListener(queues = "notes.queue")
public void receiveNotes(String msg) {
log.info("监听notes.queue队列中的消息.....收到的消息为:{}", msg);
}
}
测试
@SpringBootTest
class RabbitTopicDemoApplicationTests {
@Autowired
TopicProducer topicProducer;
@Test
void contextLoads() {
topicProducer.send("test---topic交换器");
}
}
结果:只有notes.queue队列中有消息,该队列在绑定交换器的时候按照#.topic.#进行匹配,可以成功匹配到路由键rabbit.test.topic.msg.notes
2022-01-12 15:47:36.083 INFO 48252 --- [ntContainer#0-1] com.xlh.topic.consumer.TopicConsumer : 监听notes.queue队列中的消息.....收到的消息为:test---topic交换器
若是把路由键改为:rabbit.test.topic.msg,两个队列都可以收到消息
2022-01-12 16:42:52.539 INFO 19280 --- [ntContainer#1-1] com.xlh.topic.consumer.TopicConsumer : 监听notes.queue队列中的消息.....收到的消息为:test---topic交换器
2022-01-12 16:42:52.540 INFO 19280 --- [ntContainer#0-1] com.xlh.topic.consumer.TopicConsumer : 监听msg.queue队列中的消息.....收到的消息为:test---topic交换器
总结
这些都是RabbitMQ的基础内容,还有好多好多知识,关于进阶的下次再进行学习了。
请关注公众号:喵喵伸懒腰
专注java学习,共同成长!