目录
简介
下载、安装
RabbitMQ原始API使用
生产者
消费者
基于SpringAMQP的API使用
simpleQueue简单队列
WorkQueue任务队列
发布(Publish)、订阅(Subscribe)
Fanout 广播
Direct定向
Topic 通配饰
消息转换器
MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
比较常见的MQ实现:
ActiveMQ
RabbitMQ
RocketMQ
Kafka
几种常见MQ的对比:
RabbitMQ | ActiveMQ | RocketMQ | Kafka | |
---|---|---|---|---|
公司/社区 | Rabbit | Apache | 阿里 | Apache |
开发语言 | Erlang | Java | Java | Scala&Java |
协议支持 | AMQP,XMPP,SMTP,STOMP | OpenWire,STOMP,REST,XMPP,AMQP | 自定义协议 | 自定义协议 |
可用性 | 高 | 一般 | 高 | 高 |
单机吞吐量 | 一般 | 差 | 高 | 非常高 |
消息延迟 | 微秒级 | 毫秒级 | 毫秒级 | 毫秒以内 |
消息可靠性 | 高 | 一般 | 高 | 一般 |
追求可用性:Kafka、 RocketMQ 、RabbitMQ
追求可靠性:RabbitMQ、RocketMQ
追求吞吐能力:RocketMQ、Kafka
追求消息低延迟:RabbitMQ、Kafka
RabbitMQ中的一些角色:
publisher:生产者
consumer:消费者
exchange:交换机,负责消息路由
queue:队列,存储消息
virtualHost:虚拟主机,隔离不同租户的exchange、queue、消息的隔离
最好是每一个用户拥有不同的virtualHost,这样就保证只能看到属于自己的虚拟主机下的exchange和queue。具有很好的隔离性
RabbitMQ 下载、安装_naki_bb的博客-CSDN博客
依赖
org.springframework.boot
spring-boot-starter-parent
2.3.9.RELEASE
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-test
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.168.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("root");
// 1.2.建立连接
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, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.168.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("root");
factory.setPassword("root");
// 1.2.建立连接
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, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
注意设置VirtuaHost、username、password需要对应,可以在管理后台查询 或者 创建对应的用户和VirtuaHost。
也 可以在管理后台查看连接、channels, exchange, queue的其他信息。
SpringAMQP是基于AMQP协议定义的一套API规范,提供模板发送和接受消息,
其中spring-amqp是基础抽象,spring-rabbit是底层默认实现。
SpringAmqp的官方地址:Spring AMQP
SpringAMQP提供了三个功能:
自动声明队列、交换机及其绑定关系
基于注解的监听器模式,异步接收消息
封装了RabbitTemplate工具,用于发送消息
simpleQueue没有exchange交换机,直接发往queue,消费一经被消息,就没有了。
依赖
org.springframework.boot
spring-boot-starter-parent
2.3.9.RELEASE
org.springframework.boot
spring-boot-starter-amqp
org.springframework.boot
spring-boot-starter-test
配置
spring:
rabbitmq:
host: 192.168.168.128
port: 5672
username: root
password: root
virtual-host: /
生产者
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringRabbitMQPublisherTest {
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void simpleQueueSend(){
String queueName = "simple.queue";
String message = "hello, Spring amqp";
rabbitTemplate.convertAndSend(queueName, message);
}
}
消费者
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitMQListener {
@RabbitListener(queues = "simple.queue")
public void simpleQueueListener(String msg) {
System.out.println("consumer 接受到了simpleQueue : " + msg);
}
}
Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。
当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了。
生产者
和simpleQueue在生产者没有区别
@Test
public void workQueueSend() {
String queueName = "simple.queue";
String message = "hello, Spring amqp__";
for (int i = 0; i < 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
}
}
消费者
@RabbitListener(queues = "simple.queue")
public void workQueueListener1(String msg) throws InterruptedException {
System.out.println("consumer1 接受到了simpleQueue : " + msg);
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void workQueueListener2(String msg) throws InterruptedException {
System.err.println("consumer2 ..... 接受到了simpleQueue : " + msg);
Thread.sleep(200);
}
现在有2个consumer监听同一个队列,但是处理速度不一样,看消息时如何分配的
可以发现consumer1消费的都是偶数消息,cosumer2消费的都是奇数消息,他们是平均分配的,但是由于每个消费者的消费能力不同,造成了,要消费所有的消息时间完全取决于消费能力差的消费者。这时RabbitMQ默认的预分配,在没有消费完上一消息时,就会提前从queue拿回来其他的消息。这样显然有问题,浪费了消费能力好的,最好是能者多劳。减少总体消费全部消息的时间。
所以需要添加以下配置,设置一次能获取一条消息,处理完毕在获取下一个消息,这样就可以完全利用发挥性能好的消费者
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
模型
可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:
Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
Fanout:广播,将消息交给所有绑定到交换机的队列
Direct:定向,把消息交给符合指定routing key 的队列
Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Consumer:消费者,与以前一样,订阅队列,没有变化
Queue:消息队列也与以前一样,接收消息、缓存消息。
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
在广播模式下,消息发送流程是这样的:
1) 可以有多个队列
2) 每个队列都要绑定到Exchange(交换机)
3) 生产者发送的消息,只能发送到交换机,交换机来决定要发给哪个队列,生产者无法决定
4) 交换机把消息发送给绑定过的所有队列
我们的实现步骤:
创建一个交换机 demo.fanout,类型是Fanout
创建两个队列fanout.queue1和fanout.queue2,绑定到交换机demo.fanout
消费者
方式一:声明方式
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("demo.fanout");
}
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
@Bean
public Binding fanoutBinding1(FanoutExchange fanoutExchange, Queue fanoutQueue1) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
@Bean
public Binding fanoutBinding2(FanoutExchange fanoutExchange, Queue fanoutQueue2) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class RabbitMQListener {
@RabbitListener(queues = "fanout.queue1")
public void fanoutQueue1Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了queueQueue1 : " + msg);
}
@RabbitListener(queues = "fanout.queue2")
public void fanoutQueue2Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了queueQueue2 : " + msg);
}
}
方式二:注解方式
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("fanout.queue1"),
exchange = @Exchange(value = "demo.fanout", type = ExchangeTypes.FANOUT)
)})
public void fanoutQueue1Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了fanoutQueue1 : " + msg);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("fanout.queue2"),
exchange = @Exchange(value = "demo.fanout", type = ExchangeTypes.FANOUT)
)})
public void fanoutQueue2Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了fanoutQueue2 : " + msg);
}
生产者
直接发送消息给exchange,具体到达哪些queue,生产者不关心
@Test
public void fanoutQueueSend() {
String exchange = "demo.fanout";
String message = "hello, Spring amqp fanout";
rabbitTemplate.convertAndSend(exchange, "", message);
}
总结
交换机的作用是什么?
接收publisher发送的消息
将消息按照规则路由到与之绑定的队列
不能缓存消息,路由失败,消息丢失
FanoutExchange的会将消息路由到每个绑定的队列
声明队列、交换机、绑定关系的Bean是什么?
Queue
FanoutExchange
Binding
在Fanout模式中,一条消息,会被所有订阅的队列都消费。但是,在某些场景下,我们希望不同的消息被不同的队列消费。这时就要用到Direct类型的Exchange。
在Direct模型下:
队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey
(路由key)
消息的发送方在向 Exchange发送消息时,也必须指定消息的 RoutingKey
。
Exchange不再把消息交给每一个绑定的队列,而是根据消息的Routing Key
进行判断,只有队列的Routingkey
与消息的 Routing key
完全一致,才会接收到消息
案例如下:
利用@RabbitListener声明Exchange、Queue、RoutingKey
在consumer服务中,编写两个消费者方法,分别监听direct.queue1和direct.queue2
在publisher中编写测试方法,向itcast. direct发送消息
消费者
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("direct.queue1"),
exchange = @Exchange(value = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"blue", "red"}
)
})
public void directQueue1Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了directQueue1 : " + msg);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("direct.queue2"),
exchange = @Exchange(value = "itcast.direct", type = ExchangeTypes.DIRECT),
key = {"yellow", "red"}
)
})
public void directQueue2Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了directQueue2: " + msg);
}
发送方
@Test
public void directQueueSend() {
String exchange = "itcast.direct";
String colorKey = "blue";
String message = "hello, " + colorKey;
rabbitTemplate.convertAndSend(exchange, colorKey, message);
colorKey = "yellow";
message = "hello, " + colorKey;
rabbitTemplate.convertAndSend(exchange, colorKey, message);
colorKey = "red";
message = "hello, " + colorKey;
rabbitTemplate.convertAndSend(exchange, colorKey, message);
}
结果如下:
consumer ..... 接受到了directQueue1 : hello, blue
consumer ..... 接受到了directQueue2: hello, yellow
consumer ..... 接受到了directQueue1 : hello, red
consumer ..... 接受到了directQueue2: hello, red
只有订阅相关key的consumer才能收这个key的消息
描述下Direct交换机与Fanout交换机的差异?
Fanout交换机将消息路由给每一个与之绑定的队列
Direct交换机根据RoutingKey判断路由给哪个队列
如果多个队列具有相同的RoutingKey,则与Fanout功能类似
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
举例:
item.#
:能够匹配item.spu.insert
或者 item.spu
item.*
:只能匹配item.spu
解释:
Queue1:绑定的是china.#
,因此凡是以 china.
开头的routing key
都会被匹配到。包括china.news和china.weather
Queue2:绑定的是#.news
,因此凡是以 .news
结尾的 routing key
都会被匹配。包括china.news和japan.news
案例:
消费者
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("topic.queue1"),
exchange = @Exchange(value = "itcast.topic",type = ExchangeTypes.TOPIC),
key = "china.#"
)
})
public void topicQueue1Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了topicQueue1: " + msg);
}
@RabbitListener(bindings = {
@QueueBinding(
value = @Queue("topic.queue2"),
exchange = @Exchange(value = "itcast.topic",type = ExchangeTypes.TOPIC),
key = "#.news"
)
})
public void topicQueue2Listener(String msg) throws InterruptedException {
System.err.println("consumer ..... 接受到了topicQueue2: " + msg);
}
生产者
@Test
public void topicQueueSend() {
String exchange = "itcast.topic";
String key = "china.news";
String message = "hello, " + key;
rabbitTemplate.convertAndSend(exchange, key, message);
key = "china.weather";
message = "hello, " + key;
rabbitTemplate.convertAndSend(exchange, key, message);
}
运行结果:
consumer ..... 接受到了topicQueue2: hello, china.news
consumer ..... 接受到了topicQueue1: hello, china.news
consumer ..... 接受到了topicQueue1: hello, china.weather
总结
Direct交换机与Topic交换机的差异?
Topic交换机接收的消息RoutingKey必须是多个单词,以 **.**
分割
Topic交换机与队列绑定时的bindingKey可以指定通配符
#
:代表0个或多个词
*
:代表1个词
pring会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。
只不过,默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
数据体积过大
有安全漏洞
可读性差
在消息生产者和消费者都加入转json的依赖
com.fasterxml.jackson.dataformat
jackson-dataformat-xml
2.9.10
并且都加入JsonMessageConverter的类
@Bean
public MessageConverter jsonMessageConverter(){
return new Jackson2JsonMessageConverter();
}
生产者
@Test
public void objectQueueSend() {
String queueName = "object.queue";
Map map = new HashMap<>();
map.put("name", "张三");
map.put("age", 22);
rabbitTemplate.convertAndSend(queueName, map);
}
消费者
@RabbitListener(queues = "object.queue")
public void objectQueueListener(Map map) {
System.out.println("consumer 收到object.queue : " + map);
}