RabbitMQ工作模型
基本概念
- Server:接收客户端的连接,实现AMQP实体服务。
- Connection:连接,应用程序与Server的网络连接,TCP连接。
- Channel:信道,消息读写等操作在信道中进行。客户端可以建立多个信道,每个信道代表一个会话任务。
- Message:消息,应用程序和服务器之间传送的数据,消息可以非常简单,也可以很复杂。由Properties和Body组成。Properties为外包装,可以对消息进行修饰,比如消息的优先级、延迟等高级特性;Body就是消息体内容。
- Virtual Host:虚拟主机,用于逻辑隔离。一个虚拟主机里面可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名称的Exchange或Queue。
- Exchange:交换器,接收消息,按照路由规则将消息路由到一个或者多个队列。如果路由不到,或者返回给生产者,或者直接丢弃。RabbitMQ常用的交换器常用类型有direct、topic、fanout、headers四种,后面详细介绍。
- Binding:绑定,交换器和消息队列之间的虚拟连接,绑定中可以包含一个或者多个RoutingKey。
- RoutingKey:路由键,生产者将消息发送给交换器的时候,会发送一个RoutingKey,用来指定路由规则,这样交换器就知道把消息发送到哪个队列。路由键通常为一个“.”分割的字符串,例如“com.rabbitmq”。
- Queue:消息队列,用来保存消息,供消费者消费。
工作原理
AMQP 协议模型由三部分组成:生产者、消费者和服务端,执行流程如下:
- 生产者是连接到 Server,建立一个连接,开启一个信道。
- 生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。
- 消费者也需要进行建立连接,开启信道等操作,便于接收消息。
- 生产者发送消息,发送到服务端中的虚拟主机。
- 虚拟主机中的交换器根据路由键选择路由规则,发送到不同的消息队列中。
- 订阅了消息队列的消费者就可以获取到消息,进行消费。
RabbitMQ交换机
Fanout Exchange(扇形交换机)
Fanout Exchange不处理路由键。你只需要简单的将队列绑定到交换机上。一个发送到交换机的消息都会被转发到与该交换机绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。Fanout交换机转发消息是最快的。
"key1" 能匹配到所有绑定的队列
"key2" 能匹配到所有绑定的队列
"" 能匹配到所有绑定的队列
任何发送到Fanout Exchange的消息都会被转发到与该Exchange绑定(Binding)的所有Queue上。
- 可以理解为路由表的模式
- 这种模式不需要RouteKey
- 这种模式需要提前将Exchange与Queue进行绑定,一个Exchange可以绑定多个Queue,一个Queue可以同多个Exchange进行绑定。
- 如果接受到消息的Exchange没有与任何Queue绑定,则消息会被抛弃。
FanoutExchange消息发送服务
RabbitMQconfig配置类:
@Configuration
// 配置属性
@ConfigurationProperties(prefix = "my")
// 类中的属性没有set方法时,使用@ConfigurationProperties进行配置注入会注入为null
@Setter
public class RabbitConfig {
/** 扇形交换区名称 */
private String fanoutExchange;
/** 队列A名称 */
private String queueAName;
/** 队列B名称 */
private String queueBName;
/**
* 1. 定义交换机
*/
@Bean
public FanoutExchange fanoutExchange() {
return ExchangeBuilder.fanoutExchange(fanoutExchange).build();
}
/**
* 2. 定义队列
*/
@Bean
public Queue queueA() {
return QueueBuilder.durable(queueAName).build();
}
@Bean
public Queue queueB() {
return QueueBuilder.durable(queueBName).build();
}
/**
* 3. 绑定交换机与队列
*/
@Bean
public Binding bindingA(FanoutExchange fanoutExchange, Queue queueA) {
return BindingBuilder
// 绑定队列A
.bind(queueA)
// 绑定到扇形交换区
.to(fanoutExchange);
}
@Bean
public Binding bindingB(FanoutExchange fanoutExchange, Queue queueB) {
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到扇形交换区
.to(fanoutExchange);
}
}
消息发送服务:
@Component
@Slf4j
public class MessageServer {
@Resource
private RabbitTemplate rabbitTemplate;
/**
* 扇形交换机名称
*/
private static final String FONOUT_EXCHANGE = "fonout.exchange";
public void sendMsg() {
// 定义消息
String msg = "扇形交换机发送消息,没有路由key";
Message message = new Message(msg.getBytes());
// 交换机名称,路由key(扇形交换机不需要),消息体
rabbitTemplate.convertAndSend(FONOUT_EXCHANGE, "", message);
log.info("消息发送完毕,发送时间:{}", LocalDateTime.now());
}
}
YML配置:
server:
port: 8081
spring:
application:
name: rabbitMQ-fanout
rabbitmq:
host: 192.168.233.130 #rabbitMQ主机地址
port: 5672 # 默认端口
username: user # 用户信息
password: 123456
virtual-host: powernode #虚拟主机
my:
fanoutExchange: fonout.exchange #扇形交换机名称
queueAName: queue.fonout.a #队列A名称
queueBName: queue.fonout.b #队列B名称
启动服务类:
@SpringBootApplication
public class RabbitMQFanoutApplication implements ApplicationRunner {
@Resource
private MessageServer messageService;
public static void main(String[] args)
{
SpringApplication.run(RabbitMQFanoutApplication.class, args);
}
/**
* 系统一启动就执行,发送消息
*/
@Override
public void run(ApplicationArguments args) throws Exception {
messageService.sendMsg();
}
}
测试
运行程序后,我们到RabbitMQ的web管理界面去看看。
在浏览器地址栏中输入:localhost:15672
输入用户名和密码进入后,在Exchange栏可以看到如下画面:
可以看见成功创建了一个fanout类型的名为fonout.exchange的交换机Exchange。点击去看看!
可以看见该交换机绑定了两个队列。
接下来点开Queue标签看看:
可以看见成功创建了两个队列,每个队列中都有一条消息。因为我们只发送了一次,而且是群发,因此两个队列中都有一条相同的消息。
Direct Exchange(直连交换机)
Direct Exchange处理路由键。一对一完全匹配,将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。
"key1" 只能匹配到 key1
"key2" 只能匹配到 key2
任何发送到Direct Exchange的消息都会被转发到RouteKey中指定的Queue。
- 一般情况可以使用rabbitMQ自带的Exchange:”"(该Exchange的名字为空字符串,下文称其为default Exchange)。
- 这种模式下不需要将Exchange进行任何绑定(binding)操作
- 消息传递时需要一个“RouteKey”,可以简单的理解为要发送到的队列名字。
- 如果vhost中不存在RouteKey中指定的队列名,则该消息会被抛弃。
DirectExchange消息发送服务
RabbitMQconfig配置类:
@Configuration
// 配置属性
@ConfigurationProperties(prefix = "my")
// 类中的属性没有set方法时,使用@ConfigurationProperties进行配置注入会注入为null
@Setter
public class RabbitConfig {
/** 直连交换机名称 */
private String directExchange;
/** 队列A名称 */
private String queueAName;
/** 队列B名称 */
private String queueBName;
/**
* 1. 定义交换机
*/
@Bean
public DirectExchange directExchange() {
return ExchangeBuilder.directExchange(directExchange).build();
}
/**
* 2. 定义队列
*/
@Bean
public Queue queueA() {
return QueueBuilder.durable(queueAName).build();
}
@Bean
public Queue queueB() {
return QueueBuilder.durable(queueBName).build();
}
/**
* 3. 绑定交换机与队列
*/
@Bean
public Binding bindingA(DirectExchange directExchange, Queue queueA) {
return BindingBuilder
// 绑定队列A
.bind(queueA)
// 绑定到直连交换区
.to(directExchange)
// 绑定路由key1
.with("key1");
}
@Bean
public Binding bindingB1(DirectExchange directExchange, Queue queueB) {
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到直连交换区
.to(directExchange)
// 绑定路由key1
.with("key1");
}
@Bean
public Binding bindingB2(DirectExchange directExchange, Queue queueB) {
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到直连交换区
.to(directExchange)
// 绑定路由key2
.with("key2");
}
@Bean
public Binding binding3(DirectExchange directExchange, Queue queueB) {
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到直连交换区
.to(directExchange)
// 绑定路由key3
.with("key3");
}
}
消息发送服务:
@Component
@Slf4j
public class MessageServer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendMsg(){
// 使用建造者模式创建消息
Message message1 = MessageBuilder.withBody("直连交换机发送key1消息".getBytes()).build();
Message message2 = MessageBuilder.withBody("直连交换机发送key2消息".getBytes()).build();
Message message3 = MessageBuilder.withBody("直连交换机发送key3消息".getBytes()).build();
// 交换机名称,路由key(直连交换机必须有),消息体
rabbitTemplate.convertAndSend("direct.exchange","key1",message1);
rabbitTemplate.convertAndSend("direct.exchange","key2",message2);
rabbitTemplate.convertAndSend("direct.exchange","key3",message3);
log.info("直连交换机消息发送完毕,发送时间:{}", LocalDateTime.now());
}
}
YML配置:
server:
port: 8082
spring:
application:
name: rabbitMQ-direct
rabbitmq:
host: 192.168.233.130 #rabbitMQ主机地址
port: 5672 # 默认端口
username: user # 用户信息
password: 123456
virtual-host: powernode #虚拟主机
my:
directExchange: direct.exchange #直连交换机名称
queueAName: queue.direct.a #队列A名称
queueBName: queue.direct.b #队列B名称
启动服务类:
@SpringBootApplication
public class RabbitMQADirectApplication implements ApplicationRunner {
@Resource
private MessageServer messageService;
public static void main(String[] args)
{
SpringApplication.run(RabbitMQADirectApplication.class, args);
}
/**
* 系统一启动就执行,发送消息
*/
@Override
public void run(ApplicationArguments args) throws Exception {
messageService.sendMsg();
}
}
测试
运行程序后,我们到RabbitMQ的web管理界面去看看。
在浏览器地址栏中输入:localhost:15672
输入用户名和密码进入后,在Exchange栏可以看到如下画面:
可以看见成功创建了一个fanout类型的名为direct.exchange的交换机Exchange。点击去看看!
可以看见该交换机绑定了两个队列,但向queue.direct.b的队列绑定了3条不同路由key,queue.direct.a绑定了一个路由key;
接下来点开Queue标签看看:
可以看见成功创建了两个队列:
queue.direct.b队列中有3条消息,因为3个不同的路由key。queue.direct.a队列中有1条消息,因为1个不同的路由key。
我们只发送了一次,并且队列a,b中有一个路由key相同的消息,所以队列a,b中路由key等于‘key1’的消息是相同的。
Topic Exchange(主题交换机)
Topic Exchange将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。符号“#”匹配 一个 或 多个 词,符号“*”匹配 不多不少一个词 。
"user.#" 可以匹配到 user.add user.add.batch
"user.*" 只能匹配到 user.add ,不能匹配到 user.add.batch
任何发送到Topic Exchange的消息都会被转发到所有关心RouteKey中指定话题的Queue上
- 这种模式较为复杂,简单来说,就是每个队列都有其关心的主题,所有的消息都带有一个“标题”(RouteKey),Exchange会将消息转发到所有关注主题能与RouteKey模糊匹配的队列。
- 这种模式需要RouteKey,也许要提前绑定Exchange与Queue。
- 在进行绑定时,要提供一个该队列关心的主题,如“#.log.#”表示该队列关心所有涉及log的消息(一个RouteKey为”MQ.log.error”的消息会被转发到该队列)。
- “#”表示0个或若干个关键字,“”表示一个关键字。如“log.”能与“log.warn”匹配,无法与“log.warn.timeout”匹配;但是“log.#”能与上述两者匹配。
- 同样,如果Exchange没有发现能够与RouteKey匹配的Queue,则会抛弃此消息。
TopicExchange消息发送服务
RabbitMQconfig配置类:
/**
* @Descripton TODO
* @Version 1.0.0
* @Author lll
* @Date 2023/5/10 22:22:52
*/
@Configuration
// 配置属性
@ConfigurationProperties(prefix = "my")
// 类中的属性没有set方法时,使用@ConfigurationProperties进行配置注入会注入为null
@Setter
public class RabbitConfig {
/** 主题交换机名称 */
private String topicExchange;
/** 队列A名称 */
private String queueAName;
/** 队列B名称 */
private String queueBName;
/**
* 1. 定义交换机
*/
@Bean
public TopicExchange topicExchange() {
return ExchangeBuilder.topicExchange(topicExchange).build();
}
/**
* 2. 定义队列
*/
@Bean
public Queue queueA() {
return QueueBuilder.durable(queueAName).build();
}
@Bean
public Queue queueB() {
return QueueBuilder.durable(queueBName).build();
}
/**
* 3. 绑定交换机与队列
*/
@Bean
public Binding bindingA(TopicExchange topicExchange, Queue queueA) {
return BindingBuilder
// 绑定队列A
.bind(queueA)
// 绑定到主题交换区
.to(topicExchange)
// 绑定路由key1
.with("*.orange.*");
}
@Bean
public Binding bindingB1(TopicExchange topicExchange, Queue queueB) {
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到主题交换区
.to(topicExchange)
// 绑定路由key1
.with("*.*.rabbit");
}
@Bean
public Binding bindingB2(TopicExchange topicExchange, Queue queueB) {
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到主题交换区
.to(topicExchange)
// 绑定路由key2
.with("lazy.#");
}
}
消息发送服务:
/**
* @Descripton TODO
* @Version 1.0.0
* @Author lll
* @Date 2023/5/10 23:23:07
*/
@Component
@Slf4j
public class MessageServer {
@Resource
private AmqpTemplate amqpTemplate;
public void sendMsg(){
// 使用建造者模式创建消息
Message message = MessageBuilder.withBody("主题交换机发送消息".getBytes()).build();
// 交换机名称,路由key,消息体
amqpTemplate.convertAndSend("topic.exchange","lazy.orange.rabbit",message);
log.info("主题交换机消息发送完毕,发送时间:{}", LocalDateTime.now());
}
}
YML配置:
server:
port: 8083
spring:
application:
name: rabbitMQ-topic
rabbitmq:
host: 192.168.233.130 #rabbitMQ主机地址
port: 5672 # 默认端口
username: user # 用户信息
password: 123456
virtual-host: powernode #虚拟主机
my:
topicExchange: topic.exchange #主题交换机名称
queueAName: queue.topic.a #队列A名称
queueBName: queue.topic.b #队列B名称
启动服务类:
@SpringBootApplication
public class RabbitMQATopicApplication implements ApplicationRunner {
@Resource
private MessageServer messageService;
public static void main(String[] args)
{
SpringApplication.run(RabbitMQATopicApplication.class, args);
}
/**
* 系统一启动就执行,发送消息
*/
@Override
public void run(ApplicationArguments args) throws Exception {
messageService.sendMsg();
}
}
测试
运行程序后,我们到RabbitMQ的web管理界面去看看。
在浏览器地址栏中输入:localhost:15672
输入用户名和密码进入后,在Exchange栏可以看到如下画面:
可以看见成功创建了一个fanout类型的名为topic.exchange的交换机Exchange。点击去看看!
可以看见该交换机绑定了两个队列,queue.topic.a的队列绑定了一个路由key,queue.topic.b的队列绑定了两个路由key;
接下来点开Queue标签看看:
可以看见成功创建了两个队列:
queue.topic.a队列与queue.topic.b队列都有1条消息;- 我们生产者只发送了一条消息,但路由key被两个队列都匹配上了,所以两个队列都有一条消息;
图例:
生产者发送消息时指定了路由key为:lazy.orange.rabbit,把上图中的路由都匹配上了;
详解:
// 路由key = lazy.orange.rabbit
// 规则 = 符号 # 匹配一个或多个词,符号 * 匹配不多不少一个词。
// 队列1绑定的路由key = *.orange.* 等于 lazy(*).orange.rabbit(*)
// 队列2绑定的路由key = lazy.# 等于 lazy.orange(#).rabbit(#)
// 队列2绑定的路由key = *.*.rabbit 等于 lazy(*).orange(*).rabbit
Headers Exchange( 头部交换机)
消息头订阅:
消息发布前,为消息定义一个或多个键值对的消息头,然后消费者接收消息同时需要定义类似的键值对请求头:(如:x-mactch=all或者x_match=any),只有请求头与消息头匹配,才能接收消息,忽略RoutingKey.
HeadersExchange消息发送服务
RabbitMQconfig配置类:
@Configuration
// 配置属性
@ConfigurationProperties(prefix = "my")
// 类中的属性没有set方法时,使用@ConfigurationProperties进行配置注入会注入为null
@Setter
public class RabbitConfig {
/** 头部交换机名称 */
private String headersExchange;
/** 队列A名称 */
private String queueAName;
/** 队列B名称 */
private String queueBName;
/**
* 1. 定义交换机
*/
@Bean
public HeadersExchange headersExchange() {
return ExchangeBuilder.headersExchange(headersExchange).build();
}
/**
* 2. 定义队列
*/
@Bean
public Queue queueA() {
return QueueBuilder.durable(queueAName).build();
}
@Bean
public Queue queueB() {
return QueueBuilder.durable(queueBName).build();
}
/**
* 3. 绑定交换机与队列
*/
@Bean
public Binding bindingA(HeadersExchange headersExchange, Queue queueA) {
HashMap<String,Object> map = Maps.newHashMap();
map.put("type","m");
map.put("status",1);
return BindingBuilder
// 绑定队列A
.bind(queueA)
// 绑定到头部交换区
.to(headersExchange)
// 绑定队列参数
.whereAll(map).match();
}
@Bean
public Binding bindingB(HeadersExchange headersExchange, Queue queueB) {
HashMap<String,Object> map = Maps.newHashMap();
map.put("type","s");
map.put("status",0);
return BindingBuilder
// 绑定队列B
.bind(queueB)
// 绑定到头部交换区
.to(headersExchange)
// 绑定队列参数
.whereAll(map).match();
}
}
消息发送服务:
@Component
@Slf4j
public class MessageServer {
@Resource
private RabbitTemplate rabbitTemplate;
public void sendMsg(){
//消息属性
MessageProperties messageProperties = new MessageProperties();
messageProperties.setHeader("type","s");
messageProperties.setHeader("status",0);
// 使用建造者模式创建消息
Message message = MessageBuilder
// 设置消息体
.withBody("头部交换机发送消息".getBytes())
//设置消息属性
.andProperties(messageProperties)
.build();
// 交换机名称,路由key,消息体
rabbitTemplate.convertAndSend("headers.exchange","",message);
log.info("头部交换机消息发送完毕,发送时间:{}", LocalDateTime.now());
}
}
YML配置:
server:
port: 8084
spring:
application:
name: rabbitMQ-headers
rabbitmq:
host: 192.168.233.130 #rabbitMQ主机地址
port: 5672 # 默认端口
username: user # 用户信息
password: 123456
virtual-host: powernode #虚拟主机
my:
headersExchange: headers.exchange #头部交换机名称
queueAName: queue.headers.a #队列A名称
queueBName: queue.headers.b #队列B名称
启动服务类:
@SpringBootApplication
public class RabbitMQAHeadersApplication implements ApplicationRunner {
@Resource
private MessageServer messageService;
public static void main(String[] args)
{
SpringApplication.run(RabbitMQAHeadersApplication.class, args);
}
/**
* 系统一启动就执行,发送消息
*/
@Override
public void run(ApplicationArguments args) throws Exception {
messageService.sendMsg();
}
}
测试
运行程序后,我们到RabbitMQ的web管理界面去看看。
在浏览器地址栏中输入:localhost:15672
输入用户名和密码进入后,在Exchange栏可以看到如下画面:
可以看见成功创建了一个headers类型的名为headers.exchange的交换机Exchange。点击去看看!
可以看见该交换机绑定了两个队列,queue.headers.a队列与queue.headers.b队列都没有路由key,但在Arguments(参数) 中,有我们刚刚自定义的头部信息,分别绑定在两个队列中;
接下来点开Queue标签看看:
可以看见成功创建了两个队列:
queue.headers.a队列中没有消息;queue.headers.b队列中有1条消息;- 上面的代码中生产者发送了一条消息,在Message消息中设置了消息属性。
- Message消息属性与
queue.headers.b队列中的参数匹配上了,所以B队列有一条消息;