目录
RabbitMQ
Windows安装步骤
Linux安装步骤
官网文章中文翻译系列
MQ应用场景
- 跨系统的异步通信。人民银行二代支付系统,使用重量级消息队列IBM MQ,异步,解耦,削峰。
- 应用内的同步编程异步
- 基于Pub/Sub模型实现的事件驱动。放款失败通知、购买碎屏保系统间同步数据,摒弃ETL(比如全量同步商户数据);摒弃API(比如定时增量获取用户、获取产品,变成增量广播)
- 利用RabbitMQ实现事物的最终一致性
基本介绍
AMQP协议
AMQP,即Advanced Message Queuing Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可以传递消息,并不受客户端/中间件同产品、不同的开发语言等条件限制。
AMQP的实现有:RabbitMQ、OpenAMQ、Apache Qpid等等
RabbitMQ的特性
RabbitMQ使用Erlang语言编写,使用Mnesia数据库存储消息。
- 可靠性(Reliability) RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认、发布确认
- 灵活的路由(Flexible Routing)在消息进入队列之前,通过Exchange来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的Exchange来实现。针对更复杂的路由功能,可以将多个Exchange绑定在一起,也通过插件机制实现自己的Exchange。
- 消息集群(Clustering)多个RabbitMQ服务器可以组成一个集群,形成一个逻辑Broker。
- 高可用(Highly Available Queues)队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
- 多种协议(Multi-protocol)RabbitMQ支持多种消息队列协议,比如AMQP、STOMP、MQTT等等。
- 多语言客户端(Many Clients)RabbitMQ几乎支持所有常用语言,比如Java、.NET、Ruby、PHP、C#等等
- 管理界面(Management UI) RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点。
- 插件机制(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完全匹配时,绑定的队列才能收到消息。
例:
//只有队列1能收到消息
channel.basicPublish("MY_DIRECT_EXCHANGE","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());
Java API编程
创建maven工程,pom.xml引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.5.2</version>
</dependency>
生产者
/**
* @author King Chen
* @Date: 2019/3/24 12:46
*/
public class Producer {
private final static String QUEUE_NAME = "ORIGIN_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
//连接IP
factory.setHost("127.0.0.1");
//连接端口
factory.setPort(5672);
//虚拟机
factory.setHost("/");
//用户
factory.setUsername("guest");
factory.setPassword("guest");
//建立连接
Connection connection = factory.newConnection();
//创建消息通道
Channel channel = connection.createChannel();
String msg = "Hello world , Rabbit MQ";
//声明队列
//String queue, boolean durable , boolean exclusive , boolean autoDelete , Map<String , Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
//发送消息(发送到默认交换机AMQP Default , Direct)
//如果有一个队列名称跟Routing Key相等,那么消息会路由到这个队列
//String exchange,String routingKey,BasicProperties props , byte[] body
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
channel.close();
connection.close();
}
}
消费者
/**
* @author King Chen
* @Date: 2019/3/24 15:21
*/
public class MyConsumer {
private final static String QUEUE_NAME = "ORIGIN_QUEUE";
public static void main(String[] args) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
//连接IP
factory.setHost("127.0.0.1");
//默认监听端口
factory.setPort(5672);
//虚拟机
factory.setVirtualHost("/");
//设置访问用户
factory.setUsername("guest");
factory.setPassword("guest");
//建立连接
Connection connection = factory.newConnection();
//创建消息通道
Channel channel = connection.createChannel();
//声明队列
// String queue, boolean durable, boolean exclusive, boolean autoDelete,Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println("Waiting for message...");
//创建消费者
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("Received message : '" + msg + "'");
}
};
//开始获取消息
//String queue, boolean autoAck , Consumer callback
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
参数说明
声明交换机的参数
String type:交换机的类型,direct/topic/fanout中的一种。
boolean durable:是否持久化,代表交换机在服务器重启后是否还存在。
声明队列的参数
boolean durable:是否持久话,代表队列在服务器重启后是否还存在。
boolean exclusive:是否排他性队列,排他性队列只能在声明它的Connection中使用,连接断开时自动删除。
boolean autoDelete:是否自动删除。如果为true,至少有一个消息这连接到这个队列,之后所有与这个队列连接的消费者都断开时,队列会自动删除。
Map<String,Object> argument:队列的其他属性,例如x-message-ttl、x-expires、x-max-length、x-max-length-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority。
消息属性BasicProperties
消息的全部属性有14个,一下列举了一些主要的参数:
参数 | 释义 |
Map<String,Object> headers | 消息的其他自定义参数 |
Integer deliveryMode | 持久化,其他:瞬态 |
Integer priority | 消息的优先级 |
String correlationId | 关联ID,方便RPC相应与请求关联 |
String replyTo | 回调队列 |
String expiration | TTL,消息过期时间,单位毫秒 |
进阶支持
TTL(Time To Live)
消息的过期时间
有两种设置方式:
通过队列属性设置消息过期时间:
Map<String, Object> argss = new HashMap<>(16);
argss.put("x-message-ttl", 6000);
channel.queueDeclare(QUEUE_NAME, false, false, false, argss);
设置单条消息的过期时间:
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.deliveryMode(2)//持久化消息
.contentEncoding("UTF-8")
.expiration("10000")//TTL
.build();
channel.basicPublish("", QUEUE_NAME, properties, msg.getBytes());
注:链式构造,值得借鉴。
队列的过期时间
Map<String, Object> argss = new HashMap<>(16);
argss.put("x-message-ttl", 6000);
channel.queueDeclare(QUEUE_NAME, false, false, false, argss);
队列的过期时间决定了在没有任何消费者后,队列可以存活多久。
死信队列
有三种情况消息会进入DLX(Dead Letter Exchange)私信交换机:
- (NACK || Reject)&& requeue == false
- 消息过期
- 队列达到最大长度(先入队的消息会被先发送到DLX)
可以设置一个死信队列(Dead Letter Queue)与DLX绑定,即可以存储Dead Letter,消费者可以监听这个队列取走消息。
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "DLX_EXCHANGE");
// 指定了这个队列的死信交换机
channel.queueDeclare(QUEUE_NAME, false, false, false, arguments);
// 声明死信交换机
channel.exchangeDeclare("DLX_EXCHANGE", "topic", false, false, false, null);
// 声明死信队列
channel.queueDeclare("DLX_QUEUE", false, false, false, null);
// 绑定
channel.queueBind("DLX_QUEUE", "DLX_EXCHANGE", "#");
优先级队列
设置一个队列的最大优先级:
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());
优先级高的消息可以优先被消费,但是,只有消息堆积(消息的发送速度大于消费者的消费速度)的情况下优先级才有意义。
延迟队列
RabbitMQ本身不支持延迟队列。可以使用TTL结合DLX的方式来实现消息的延迟投递,即把DLX跟某个队列绑定,到了指定时间,消息过期后,就会从DLX路由到这个队列,消费者可以从这个队列取走消息。
另一种方式是使用rabbitmq-delayed-message-exchange插件。
当然,将需要发送的信息保存在数据库,使用任务调度系统扫描然后发送也是可以实现的。
RPC
RabbitMQ实现RPC的原理:服务端处理消息后,把相应消息发送到一个相应队列,客户端再从相应队列取到结果。
其中的问题:Client收到消息后,怎么知道应答消息是回复哪一条消息的?所以必须有一个唯一ID来关联,就是correlationId。
服务端流控(Flow Control)
RabbitMQ会在启动时检测机器的物理内存数值。默认当MQ占用40%以上内存时,MQ会主动抛出一个内存警告并阻塞所有连接(Connections)。可以通过修改rabbitmq.config文件来调整内存阈值,默认值时0.4,如下所示:[{rabbit,[{vm_memory_high_watermark,0.4}]}]。
默认情况,如果剩余磁盘空间在1GB以下,RabbitMQ主动阻塞所有的生产者。这个阈值也是可调的。
注意队列长度只在消息堆积的情况下有意义,而且会删除先入队的消息,不能实现服务端限流。
消费端限流
在AutoACK为false的情况下,如果一定数目的消息(通过基于consumer或者channel设置QoS的值)未被确认前,不进行消费新的消息。
//如果超过2条消息没有发送ACK,当前消费者不再接受队列消息
channel.basicQos(2);
channel.basicConsume(QUEUE_NAME, false, consumer);