1、RabbitMQ安装
2、RabbitMQ介绍
2007年,Rabbit技术公司基于AMQP开发了RabbitMQ 1.0。为什么要用Erlang语言呢?因为Erlang是作者Matthias擅长的开发语言。第二个就是Erlang是为电话交换机编写的语言,天生适合分布式和高并发。
为什么要取Rabbit Technologies这个名字呢?因为兔子跑得很快,而且繁殖起来很疯狂。
从最开始用在金融行业里面,现在RabbitMQ已经在世界各地的公司中遍地开花。国内的绝大部分大厂都在用RabbitMQ,包括头条,美团,滴滴(TMD),去哪儿,艺龙,淘宝也有用。
RabbitMQ和Spring家族属于同一家公司:Pivotal。
当然,除了AMQP之外,RabbitMQ支持多种协议,STOMP、MQTT、HTTP、WebSockets。
2.1、工作模型及组件
我们先从下面这张图开始学习
2.1.1、Broker
Broker中文件翻译是代理/中介,这里我们就可以看做是RabbitMQ服务器,默认端口5672。
2.1.2、Connection
无论是生产者发送消息,还是消费者接收消息,都必须要跟Broker之间建立一个连接,这个连接是一个TCP的长连接。
2.1.3、Channel
如果所有的生产者发送消息和消费者接收消息,都直接创建和释放TCP长连接的话,对于Broker来说肯定会造成很大的性能损耗,也会浪费时间。
所以在AMQP里面引入了Channel的概念,它是一个虚拟的连接。我们把它翻译成通道,或者消息信道。这样我们就可以在保持的TCP长连接里面去创建和释放Channel,大大了减少了资源消耗。
不同的Channel是相互隔离的,每个Channel都有自己的编号。对于每个客户端线程来说,Channel就没必要共享了,各自用自己的Channel。
另外一个需要注意的是,Channel是RabbitMQ原生API里面的最重要的编程接口,也就是说我们定义交换机、队列、绑定关系,发送消息,消费消息,调用的都是Channel接口上的方法。
2.1.4、Queue
Queue是RabbitMQ用来存储消息的对象。实际上RabbitMQ是用数据库来存储消息的,这个数据库跟RabbitMQ一样是用Erlang开发的,名字叫Mnesia。我们可以在磁盘上找到 Mnesia的存储路径。
C:\Users\用户名\AppData\RoamingRabbitMQ\db\rabbit@用户名-mnesia
队列也是生产者和消费者的纽带,生产者发送的消息到达队列,在队列中存储。消费者从队列消费消息。
2.1.5、Consumer
就是消费者的意思。RabbitMQ中提供了两种消费的模式
- Pull模式,对用方法BasicGet
- Push模式,对应方法BasicConsume
Pull模式,对应的方法是 basicGet。消息存放在服务端,只有消费者主动获取才能拿到消息。如果每隔一段时间获取一次消息,消息的实时性会降低。但是好处是可以根据自己的消费能力决定获取消息的频率。
Push模式,对应的方法是basicConsume,只要生产者发消息到服务器,就马上推送给消费者,消息保存在客户端,实时性很高,如果消费不过来有可能会造成消息积压。Spring AMQP是 push方式,通过事件机制对队列进行监听,只要有消息到达队列,就会触发消费消息的方法。
2.1.6、Exchange
Exchange是交换机的意思。其作用是将消息按照规则分发到Queue中。所以,Exchange和这些需要接收消息的队列必须建立一个绑定关系,并且为每个队列指定一个特殊的标识。
Exchange和队列是多对多的绑定关系,也就说,一个交换机的消息一个路由给多个队列,一个队列也可以接收来自多个交换机的消息。
绑定关系建立好之后,生产者发送消息到Exchange,也会携带一个特殊的标识。当这个标识跟绑定的标识匹配的时候,消息就会发给一个或者多个符合规则的队列。
2.1.7、Vhost
Vhost可以理解为虚拟主机,它和rabbitMQ的关系类似于,虚拟机和物理主机。同一个RabbitMQ服务器,可以创建多个Vhost,它们彼此是独立的,拥有自己的交换机、队列、绑定等,拥有自己的权限机制。我们安装RabbitMQ的时候会自带一个默认的VHOST,名字是“/”。
2.2、路由方式
RabbitMQ中一共有四种类型的交换机,Direct、Topic、Fanout、Headers。其中Headers 不常用。交换机的类型可以在创建的时候指定,网页或者代码中。
2.2.1、Direct直连
消息的routing key
和 binding key
完全匹配,才能路由到某一个队列。
eg: channel.basicPublish("MY_DIRECT_EXCHANGE”," spring” ," msg1”)
只有第一个队列能收到消息。
2.2.2、Topic主题
消息的routing key
和 binding key
进行规则匹配,并且可以路由到多个队列。
这里需要说明的路由规则如下
*
代表不多不少一个单词#
代表0个或者多个单词
eg.根据一下填写可以路由到的队列。
routing key | queue |
---|---|
junior.abc.jvm | JUNIOR_QUEUE |
senior.netty | NETTY_QUEUE、SENIOR_QUEUE |
2.2.3、Fanout广播
广播类型的交换机与队列绑定时,不需要指定绑定键。因此生产者发送消息到广播类型的交换机上,也不需要携带路由键。消息达到交换机时,所有与之绑定了的队列,都会收到相同的消息的副本。
eg:channel.basicPublish(" MY_FANOUT_EXCHANGE","", "msg 4")
三个队列都会收到msg 4。
2.3、延迟消息实现
2.3.1、场景
假设有一个业务场景:超过30分钟未付款的订单自动关闭,这个功能应该怎么实现?
方案有如下两种:
- 利用RabbitMQ的**死信队列(Dead Letter Queue)**来实现。
- 利用rabbitmq-delayed-message-exchange实现
2.3.2、利用RabbitMQ的死信队列来实现
利用消息的过期时间,过期之后投递到DLX(死信交换机),路由到DLQ(死信队列),监听DLQ,实现了延迟队列。
2.3.2.1、消息的流转流程:
生产者——>原交换机——>原队列(超过TTL之后)——>死信交换机——>死信队列——>最终消费者
2.3.2.2、消息过期设置
实现消息过期的设置有两种
1、设置队列属性x-message-ttl
设置了该属性,队列中所有的消息超过时间未被消费时,都会过期。不管是谁的包裹都一视同仁。
@Bean("ttlQueue")
public Queue queue() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("x-message-ttl", 11000); // 队列中的消息未被消费11秒后过期
return new Queue("TTL_QUEUE", true, false, false, map);
}
2、设置消息属性
通过MessageProperties.setExpiration("4000")
方法设置消息的过期时间。
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("4000"); // 消息的过期属性,单位ms
Message message = new Message("这条消息4秒后过期".getBytes(), messageProperties);
rabbitTemplate.send("TTL_EXCHANGE", "test.ttl", message);
注意:如果两者都设置了过期时间,先到期,先生效。
2.3.2.3、死信会去哪里?
队列在创建的时候可以指定一个死信交换机 DLX (Dead Letter Exchange)。死信交换机绑定的队列被称为死信队列DLQ (Dead Letter Queue),DLX实际上也是普通的交换机,DLQ也是普通的队列(例如替补球员也是普通球员)。
也就是说,如果消息过期了,队列指定了DLX,就会发送到DLX。如果DLX绑定了DLQ,就会路由到DLQ。路由到DLQ之后,我们就可以消费了。
2.3.2.4、死信队列如何使用?
下面我们通过一个例子来演示死信队列的使用。
- 声明原交换机(ORI_USE_EXCHANGE ) 、原队列(ORI_uSE_QUEUE ),相互绑定。指定原队列的死信交换机(DEAD_LETTER_EXCHANGE)。
- 声明死信交换机 (DEAD_LETTER_EXCHANGE)、死信队列(DEAD_LETTER_QUEUE),并且通过"#"绑定,代表无条件路由。
- 最终消费者监听死信队列,在这里面实现检查订单状态逻辑。
- 生产者发送消息测试,设置消息10秒过期。
// 指定队列的死信交换机
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("x-dead-letter-exchange","GP_DEAD_LETTER_EXCHANGE");
// arguments.put("x-expires",9000L); // 设置队列的TTL
// arguments.put("x-max-length", 4); // 如果设置了队列的最大长度,超过长度时,先入队的消息会被发送到DLX
// 声明队列(默认交换机AMQP default,Direct)
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare("GP_ORI_USE_QUEUE", false, false, false, arguments);
// 声明死信交换机
channel.exchangeDeclare("GP_DEAD_LETTER_EXCHANGE","topic", false, false, false, null);
// 声明死信队列
channel.queueDeclare("GP_DEAD_LETTER_QUEUE", false, false, false, null);
// 绑定,此处 Dead letter routing key 设置为 #
channel.queueBind("GP_DEAD_LETTER_QUEUE","GP_DEAD_LETTER_EXCHANGE","#");
System.out.println(" Waiting for message....");
2.3.3、利用rabbitmq-delayed-message-exchange实现
使用死信队列实现延时消息的缺点:
- 如果统一用队列来设置消息的TTL,当梯度非常多的情况下,比如1分钟,2分钟,5分钟,10分钟,20分钟,30分钟…….需要创建很多交换机和队列来路由消息。
- 如果单独设置消息的TTL,则可能会造成队列中的消息阻塞——前一条消息没有出队(没有被消费),后面的消息无法投递(比如第一条消息过期TTL是30min,第二条消息TTL是10min。10分钟后,即使第二条消息应该投递了,但是由于第一条消息还未出队,所以无法投递)。
- 可能存在一定的时间误差。
在RabbitMQ 3.5.7︰及以后的版本提供了一个插件(rabbitmq-delayed-message-exchange)来实现延时队列功能(Linux和 Windows都可用)。同时插件依赖Erlang/OPT 18.0及以上。
2.3.3.1、插件安装
1、进入插件目录
whereis rabbitmq
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.12/plugins
2、将下载的插件rabbitmq_delayed_message_exchange-3.8.0.ez
上传到这个
3、启用插件
# 启动插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
# 停用插件
rabbitmq-plugins disable rabbitmq_delayed_message_exchange
4、使用插件
通过声明一个x-delayed-message类型的Exchange来使用delayed-messaging特性。x-delayed-message是插件提供的类型,并不是rabbitmq本身的(区别于direct、topic、fanout、headers)。
2.3.3.2、使用代码示例
示例:声明延迟Exchange
@Bean("delayExchange")
public TopicExchange exchange() {
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-delayed-type", "direct");
return new TopicExchange("DELAY_EXCHANGE", true, false, argss);
}
生产者:消息属性中指定x-delay参数。
MessageProperties messageProperties = new MessageProperties();
// 延迟的间隔时间,目标时刻减去当前时刻
messageProperties.setHeader("x-delay", delayTime.getTime() - now.getTime());
Message message = new Message(msg.getBytes(), messageProperties);
// 不能在本地测试,必须发送消息到安装了插件的服务端
rabbitTemplate.send("DELAY_EXCHANGE", "#", message);
补充:消息除了过期,还有什么情况下会变成死信消息?
- 消息被消费者拒绝并且未设置重回队列:(NACK || Reject ) && requeue ==false
- 队列达到最大长度,超过了Max length (消息数)或者Max length bytes (字节数),最先入队的消息会被发送到DLX。
2.4、消息存储限制
场景:当RabbitMQ生产MQ消息的速度远大于消费消息的速度时,会产生大量的消息堆积,占用系统资源,导致机器的性能下降。我们想要控制服务端接收的消息的数量,应该怎么做呢?
流量控制我们可以从几方面来控制,一个是服务端,一个是消费端。
2.4.1、服务端控制
2.4.1.1、队列长度
队列有两个控制长度的属性:
x-max-length
:队列中存储消息的最大数量,超过这个数量,队头的消息会被丢弃。x-max-length-bytes
:队列中存储的最大消息容量(单位bytes),超过这个容量,队头的消息会被丢弃。
需要注意的是,设置队列长度只在消息堆积的情况下有意义,而且会删除先入队的消息,不能真正地实现服务端限流。
2.4.1.2、内存控制
RabbitMQ会在启动时检测机器的物理内存数值。默认当MQ占用40%以上内存时,MQ会主动抛出一个内存警告并阻塞所有连接(Connections)。可以通过修改rabbitmq.config
文件来调整内存阈值,默认值是0.4,如下所示:
Windows默认配置文件: advanced.config
,在此路径下 C:\Users\用户名\AppData\RoamingRabbitMQ\
[{rabbit,[{vm_memory_high_watermark,0.4}]}].
也可以用命令动态设置,如果设置成0,则所有的消息都不能发布。
rabbitmqctl set_vm_memory_high_watermark 0.3
2.4.1.3、磁盘控制
相关配置参数,参考文档 https://www.rabbitmq.com/configure.html#config-items
另一种方式是通过磁盘来控制消息的发布。当磁盘剩余可用空间低于指定的值时(默认50MB),触发流控措施。
# 指定磁盘的30%,存储消息
disk_free_limit.relative = 3.0
# 指定磁盘的2GB,用来存储消息
disk_free_limit.absolute = 2GB
还有一种情况,虽然 Broker消息存储得过来,但是在push模型下(consume,有消息就消费),消费者消费不过来了,这个时候也要对流量进行控制。
2.4.2、消费端控制
https://www.rabbitmq.com/consumer-prefetch.html
默认情况下,如果不进行配置,RabbitMQ会尽可能快速地把队列中的消息发送到消费者。因为消费者会在本地缓存消息,如果消息数量过多,可能会导致OOM或者影响其他进程的正常运行。
在消费者处理消息的能力有限,例如消费者数量太少,或者单条消息的处理时间过长的情况下,如果我们希望在一定数量的消息消费完之前,不再推送消息过来,就要用到消费端的流量限制措施。
可以基于Consumer或者channel设置prefetch count的值,含义为Consumer端的最大的 unacked messages数目。当超过这个数值的消息未被确认,RabbitMQ会停止投递新的消息给该消费者。
channel.basicQos(2); //如果超过2条消息没有发送ACK,当前消费者不再接受队列消息
channel.basicConsume(QUEUE_NAME, false, consumer); // 这里设置为手动提交消息
代码示例:
启动两个消费者,其中一个Consumer2消费很慢,qos设置为2,最多一次给它发两条消息,其他的消息都被Consumer1接收了。这个叫能者多劳。
3、RabbitMQ使用
3.1、Springboot整合RabbitMQ
参考代码:https://gitee.com/fanger8848/study/tree/master/rabbitMQ/springboot-demo
maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
RabbitConfig.java
@Configuration
public class RabbitConfig {
// 两个交换机
@Bean("topicExchange")
public TopicExchange getTopicExchange(){
return new TopicExchange("TOPIC_EXCHANGE");
}
@Bean("fanoutExchange")
public FanoutExchange getFanoutExchange(){
return new FanoutExchange("FANOUT_EXCHANGE");
}
// 三个队列
@Bean("firstQueue")
public Queue getFirstQueue(){
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl",6000);
Queue queue = new Queue("FIRST_QUEUE", false, false, true, args);
return queue;
}
@Bean("secondQueue")
public Queue getSecondQueue(){
return new Queue("SECOND_QUEUE");
}
@Bean("thirdQueue")
public Queue getThirdQueue(){
return new Queue("THIRD_QUEUE");
}
// 两个绑定
@Bean
public Binding bindSecond(@Qualifier("secondQueue") Queue queue,@Qualifier("topicExchange") TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("#.gupao.#");
}
@Bean
public Binding bindThird(@Qualifier("thirdQueue") Queue queue,@Qualifier("fanoutExchange") FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
}
消费者
@Component
@RabbitListener(queues = "FIRST_QUEUE")
public class FirstConsumer {
@RabbitHandler
public void process(String msg){
System.out.println(" first queue received msg : " + msg);
}
}
消息发送
@Component
public class MyProvider {
@Autowired
AmqpTemplate amqpTemplate;
public void send(){
// 发送4条消息
amqpTemplate.convertAndSend("","FIRST_QUEUE","-------- a direct msg");
amqpTemplate.convertAndSend("TOPIC_EXCHANGE","shanghai.teacher","-------- a topic msg : shanghai.teacher");
amqpTemplate.convertAndSend("TOPIC_EXCHANGE","changsha.student","-------- a topic msg : changsha.student");
amqpTemplate.convertAndSend("FANOUT_EXCHANGE","","-------- a fanout msg");
}
}
3.2、Springboot参数配置
Springboot相关配置查询地址 https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html
4、特性总结
- 支持多客户端:对主流开发语言(Python、Java、Ruby、PHP、C#、JavaScript、Go、Elixir、Objective-C、 Swift等)都有客户端实现。
- 灵活的路由:通过交换机 (Exchange)实现消息的灵活路由。
- 权限管理:通过用户与虚拟机实现权限管理。
- 插件系统:支持各种丰富的插件扩展,同时也支持自定义插件。
- 与Spring集成: Spring对 AMQP进行了封装。
- 高可靠: RabbitMQ提供了多种多样的特性让你在可靠性和性能之间做出权衡,包括持久化、发送应答、发布确认以及高可用性。
- 集群与扩展性:多个节点组成一个逻辑的服务器,支持负载。
- 高可用队列:通过镜像队列实现队列中数据的复制。