RabbitMQ工作模型
典型应用场景
1、跨系统的异步通信 人民银行二代支付系统,使用重量级消息队列 IBM MQ,异步,解耦,削峰都有体现。
2、应用内的同步变成异步
3、基于Pub/Sub模型实现的事件驱动 放款失败通知、提货通知、购买碎屏保 系统间同步数据 摒弃ELT(比如全量 同步商户数据); 摒弃API(比如定时增量获取用户、获取产品,变成增量广播)。
4、利用RabbitMQ实现事务的最终一致性。
基本介绍
AMQP协议
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应 用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户 端/中间件同产品、不同的开发语言等条件的限制。
AMQP的实现有:RabbitMQ、OpenAMQ、Apache Qpid、Redhat Enterprise MRG、AMQP Infrastructure、 ØMQ、Zyre等。
RabbitMQ的特性
RabbitMQ使用Erlang语言编写,使用Mnesia数据库存储消息。
(1)可靠性(Reliability) RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。(2)灵活的路由(Flexible Routing) 在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功 能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在 一起,也通过插件机制实现自己的 Exchange 。
(3)消息集群(Clustering) 多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。 (4)高可用(Highly Available Queues) 队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下 队列仍然可用。
(5)多种协议(Multi-protocol) RabbitMQ 支持多种消息队列协议,比如 AMQP、STOMP、MQTT 等等。
(6)多语言客户端(Many Clients) RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby、PHP、C#、 JavaScript 等等。 (7)管理界面(Management UI) RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息、集群 中的节点。
(8)插件机制(Plugin System) RabbitMQ提供了许多插件,以实现从多方面扩展,当然也可以编写自己的插件。
工作模型
Broker :即RabbitMQ的实体服务器。提供一种传输服务,维护一条从生产者到消费者的传输线路,保证消息数据能按照指定的方式传输
Exchange:消息交换机。指定消息按照什么规则路由到哪个队列Queue。
Queue:消息队列。消息的载体,每条消息都会被投送到一个或多个队列中。
Binding:绑定。作用是将Exchange和Queue按照某种路由规则绑定起来。
Routing Key :路由关键字。Exchange根据Routing Key进行消息传递。定义绑定时指定的关键字称为Binding Key
Vhost:虚拟主机。一个Broker可以有多个虚拟主机,用作不同用户的权限分离。一个虚拟主机持有一组Exchange、Queue和Binding。
Producer:消息生产者。主要将消息投递到对应的Exchange上面。一般是独立的程序。
Consumer:消息消费者。消息的接收者,一般是独立的程序
Connection:Producer和Consumer与Broker之间的TCP长连接。
Channel:消息通道,也称信道。在客户端的每个连接里可以建立多个channel,每个Channel代表一个会话任务。在RabbitMQ Java Client API 中,channel上定义了大量的编程接口
三种主要的交换机
Direct Exchange 直连交换机
定义:直连类型的交换机与一个队列绑定时,需要指定一个明确的binding key。
路由规则:发现消息到直连类型的交换机时,只有routing key和binding key完全匹配时,绑定的队列才能收到消息。
//String exchange,String routingKey,BasicProperties props,byte[] body channel.basicPublish(EXCHANGE_NAME,"key1",null,msg.getBytes());
Topic Exchange 主题交换机
定义:主题类型的交换机与一个队列绑定时,可以指定按模式匹配的routing key。
通配符有两个,*代表匹配一个单词。#代表匹配零个或者多个单词。单词与单词之间用.隔开。
路由规则:发送消息到主题类型的交换机时,routing key符合binding key的模式时,绑定的队列才能收到消息。
例如:
// 只有队列1能收到消息 channel.basicPublish("MY_TOPIC_EXCHANGE", "sh.abc", null, msg.getBytes()); // 队列2和队列3能收到消息 channel.basicPublish("MY_TOPIC_EXCHANGE", "bj.book", null, msg.getBytes()); // 只有队列4能收到消息 channel.basicPublish("MY_TOPIC_EXCHANGE", "abc.def.food", null, msg.getBytes());
Fanout Exchange 广播交换机
定义:广播类型的交换机与一个队列 绑定时,不需要指定binding key。
路由规则:当消息发送到广播类型的交换机时,不需要指定routing key,所有与之绑定的队列都能收到消息。
例如:
// 3个队列都会收到消息 channel.basicPublish("MY_FANOUT_EXCHANGE", "", null, msg.getBytes());
参数说明
//声明交换机 //String exchange,String type,boolean durable,boolean autoDelete,Map<String, Object> arguments channel.exchangeDeclare(EXCHANGE_NAME,"direct",false,false,null);
声明交换机的参数
String type:交换机的类型,direct,topic,fanout中的一种
boolean durable :是否持久化,代表交换机
//声明队列 //String queue,boolean durable,boolean exclusive,boolean autoDelete,Map<String, Object> arguments channel.queueDeclare(QUEUE_NAME,false,false,false,null);
声明队列的参数
boolean durable:是否持久化,代表队列在服务器重启后是否还存在。
boolean exclusive :是否排他性队列。排他性队列只能在声明它的connection中使用,连接断开时自动删除。
boolean autoDelete:是否自动删除。如果为true,至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列自动删除。
Map<String, Object> arguments:队列的其他属性,例如x-message-ttl、x-expires、x-max-length、x-maxlength-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority。
消息属性BasicProperties
消息的全部属性有14个,以下列举了一些主要的参数:
Map<String,Object> headers :消息的其他自定义参数
Integer deliveryMode :2持久化,其他:瞬态
Integer priority :消息的优先级
String correlationId:关联ID,方便RPC相应与请求关联
String replyTo :回调队列
String expiration :TTL,消息过期时间,单位毫秒
进阶知识
1.TTL(Time To Live)
a.消息的过期时间
//对每条消息设置过期时间 AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder() .deliveryMode(2)//持久化消息 .contentEncoding("UTF-8") .expiration("10000") //TTL .build();
channel.basicPublish("","TEST_TTL_QUEUE",properties,msg.getBytes());
b.队列的过期时间
Map<String,Object> argss = new HashMap<>(); argss.put("x-message-ttl",6000); channel.queueDeclare("TEST_TTL_QUEUE",false,false,false,argss);
2.死信队列
有三种情况消息会进入DLX(Dead Letter Exchange)死信交换机
1.(NACK || Reject)&& requeue == false
2.消息过期
3.队列达到最大长度(先入队的消息会被发送到DLX)
可以设置一个死信队列(Dead Letter Queue)与DLX绑定,即可以存储Dead Letter,消费者可以监听这个队列取走消息
//指定队列的死信交换机 Map<String,Object> arguments = new HashMap<>(); arguments.put("x-dead-letter-exchange","DLX_EXCHANGE"); // arguments.put("x-expires","9000"); // 设置队列的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("TEST_DLX_QUEUE",false,false,false, arguments); // 声明死信交换机 channel.exchangeDeclare("DLX_EXCHANGE","topic", false, false, false, null); // 声明死信队列 channel.queueDeclare("DLX_QUEUE", false, false, false, null); // 绑定,此处 Dead letter routing key 设置为 # channel.queueBind("DLX_QUEUE","DLX_EXCHANGE","#");
3.优先级队列
设置一个队列的最大优先级
Map<String, Object> argss = new HashMap<String, Object>();
argss.put("x-max-priority",10); // 队列最大优先级 channel.queueDeclare("ORIGIN_QUEUE", false, false, false, argss);
发送消息时指定消息当前的优先级:
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.priority(5)
.build(); channel.basicPublish("", "ORIGIN_QUEUE", properties, msg.getBytes());
优先级高的消息可以被优先消费,但是:只有消息堆积(消息的发送速度大于消费者的消费速度才有意义)
4.延迟队列
RabbitMQ本身不支持延迟队列。可以使用TTL结合DLX的方式来实现消息的延迟投递,即把DLX跟某个队列绑定, 到了指定时间,消息过期后,就会从DLX路由到这个队列,消费者可以从这个队列取走消息。 另一种方式是使用rabbitmq-delayed-message-exchange插件。
当然,将需要发送的信息保存在数据库,使用任务调度系统扫描然后发送也是可以实现的。
5.RPC
RabbitMQ实现RPC的原理:服务端处理消息后,把响应消息发送到一个响应队列,客户端再从响应队列取到结 果。
其中的问题:Client收到消息后,怎么知道应答消息是回复哪一条消息的?所以必须有一个唯一ID来关联,就是 correlationId。
6.服务端控流(Flow Control)
RabbitMQ 会在启动时检测机器的物理内存数值。默认当 MQ 占用 40% 以上内存时,MQ 会主动抛出一个内存警 告并阻塞所有连接(Connections)。可以通过修改 rabbitmq.config 文件来调整内存阈值,默认值是 0.4,如下 所示: [{rabbit, [{vm_memory_high_watermark, 0.4}]}]. 默认情况,如果剩余磁盘空间在 1GB 以下,RabbitMQ 主动阻塞所有的生产者。这个阈值也是可调的。
注意队列长度只在消息堆积的情况下有意义,而且会删除先入队的消息,不能实现服务端限流。
7.消费端限流
在AutoACK为false的情况下,如果一定数目的消息(通过基于consumer或者channel设置Qos的值)未被确认 前,不进行消费新的消息
channel.basicQos(2); // 如果超过2条消息没有发送ACK,当前消费者不再接受队列消息 channel.basicConsume(QUEUE_NAME, false, consumer);
可靠性投递
效率和可靠性是无法兼得的,如果要保证每一个环节都成功,势必会对消息的收发效率产生影响,如果是一些业务实时一致性要求不高的场合,可以牺牲一些可靠性来换取效率。
① 代表消息从生产者发送到Exchange;
② 代表消息从Exchange路由到Queue;
③ 代表消息在Queue中存储;
④ 代表消费者订阅Queue并消费消息。
1.确保消息发送到RabbitMQ服务器
可能因为网络或者Broker的问题导致①失败,而生产者是无法知道消息是否正确发送到Broker的。
有两种解决方案,第一种是Transaction(事务)模式,第二种Confirm(确认)模式。
在通过channel.txSelect方法开启事务后,我们便可以发布消息到RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。使用事务机制的话会吸干RabbitMQ的性能,一般不建议使用。
生产者通过调用channel.confirmSelect方法(即Confirm.Select命令)将信道设置为confirm模式。一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个确认(Basic.ACK)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地。
2.确保消息路由到正确的队列
可能由于路由关键字错误,或者队列不存在,或者队列名错误导致② 失败。
使用mandatory参数和ReturnListener,可以实现消息无法路由的时候返回给生产者。
另一种方式就是使用备份交换机(alternate-exchange),无法路由的消息会发送到这个交换机上。
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE"); // 指定交换机的备份交换机 channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);
3.确保消息在队列正确地存储
可能因为系统宕机,重启,关闭等情况导致存储在队列的消息丢失,即③ 出现问题
解决方案:
1.队列持久化
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
2.交换机持久化
channel.exchangeDeclare("MY_EXCHANGE","true");
3.消息持久化
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) // 2代表持久化,其他代表瞬态
.build();
channel.basicPublish("", QUEUE_NAME, properties, msg.getBytes());
4.集群,镜像队列
4.确保消息从队列正确地投递到消费者
如果消费者收到消息后未来得及处理即发生异常,或者处理过程中发生异常,会导致④ 失败。
为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement)。消费者在订阅队列时,可以指定autoAck参数,当autoAck等于false时,RabbitMQ会等待消费者显式的回复确认信号后才从队列中移去消息。
如果消费失败,也可以调用Basic.Reject或者Basic.Nack来拒绝当前消息而不是确认。如果requeue参数设置为true,可以把这条消息重新存入队列,以便发给下一个消费者(当然,只有一个消费者的时候,这种方式可能会出现无限重复消费的情况,可以投递到新的队列中,或者只打印异常日志)。
5.消费者回调
消费者处理消息以后,可以再发送一条消息给生产者,或者调用生产者的API,告知消息处理完毕。
6.补偿机制
对于一定时间没有得到响应的消息,可以设置一个定时重发的机制,但要控制次数,比如最多重发三次,否则会造成消息堆积
7.消息的幂等性
服务端是没有这种控制的,只能在消费端控制。
如何避免消息的重复消费?
消息重复可能会有两个原因:
1、生产者的问题,环节①重复发送消息,比如在开启了Confirm模式但未收到确认。 2、环节④出了问题,由于消费者未发送ACK或者其他原因,消息重复投递。 对于重复发送的消息,可以对每一条消息生成一个唯一的业务ID,通过日志或者建表来做重复控制
8.消息的顺序性
消息的顺序性指的是消费者消费的顺序跟生产者产生消息的顺序是一致的。
在RabbitMQ中,一个队列有多个消费者时,由于不同的消费者消费消息的速度是不一样的,顺序无法保证。