RabbitMQ 是一个开源的AMQP实现、服务器端用Erlang语言编写,支持多种客户端。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。
AMQP
在了解RabbitMQ之前,有必要认识一下AMQP协议。
AMQP(Advanced Message Queuing Protoclol),是一种进程间传递异步消息的协议。是跨语言跨平台的, 不管什么样的MQ服务器,只要遵循AMQP协议,就可以实现消息的交互。
和HTTP一样,AMQP是一个工作于应用层的协议,是由几个大商业公司于2006年制定发布的进程间传递异步消息的网络协议,它是跨平台的,不管是什么样的MQ服务器、什么语言的客户端,只要遵循AMQP协议,就可以实现消息的交互。
基本概念
工作模型
以上即RabbitMQ的整体架构。
Broker
即MQ服务端,功能就是存储和转发消息。
Producer
即生产者,是一个向Exchange发送消息的客户端。
创建Message,并将Message发布到Broker Server。
Message分为两个部分:Payload(被传输的内容)和Label(包含交换机名、绑定键等)。
Consumer
即消费者,是一个从Queue中接收消息的客户端。
消费者消费消息有两种模式:
- pull 拉,消费者主动从服务端拉取消息
- push 推,服务端主动推送消息给消费者
RabbitMQ对这两种模式都做了实现,在API中,pull对应的方法就是basicGet
,push对应的方法就是basicConsumer
。但是如果使用spring AMQP,那么采用的就只有push模式。
Connection
服务端和客户端之间的一个TCP长连接,当然这个客户端指的是消费者。
Channel
通信通道,多路复用连接中的一条双向数据流通道,为会话提供物理传输介质,为了减少创建和断开TCP连接的消耗,一个TCP连接中有多个通道。
不同的通道之间是相互隔离的,每个通道都有自己的编号。
channel是rabbitMQ中一个重要的编程接口,定义交换机和队列、绑定关系、发送消息 、消费消息,这些操作都是调用channel上的方法。
Queue
即消息队列,是消息的容器,用来保存生产者发送来的消息。
每个消息会被投送到一个或多个队列中。
rabbitMQ中,Queue的数据结构是数据库,这个数据库叫做Mnesia,是用erlang语言开发的。
过期属性
x-message-ttl
队列有一个消息过期属性,设置了这个属性后,超过指定时间的消息将会被丢弃。
Exchange
即交换机,接收生产者发送来的消息,并通过设置的规则路由给Queue。
当一条消息需要发送给多个队列时,不是多次调用basicPublish
方法,而是将消息交给交换机,由交换机路由到多个队列。
Binding
即绑定器,可以理解为路由规则,就是把交换机和队列按照规则绑定在一起,绑定之后,交换机就只路由消息到绑定了的队列中。
Binding Key
绑定关键字
Rounting Key
路由关键字
VHost
即virtual host,虚拟主机,指一批相关的Exchange、Queue、Binding。
虚拟主机是共享相同的身份认证和加密环境的独立服务器域,一个broker可以配置多个VHost,用做资源的隔离和权限的控制。
路由方式
RabbitMQ中共有四种交换机。
Direct
直连类型
直连类型交换机与队列绑定时,需要指定一个明确的绑定键(Binding Key)。
生产者发送消息时,会携带一个路由键(Rounting Key),当路由键与绑定键完全匹配时,这条消息才会从这个交换机发送到这个队列上。
直连类型的交换机适合一些业务用途明确的消息。
Topic
主题类型
主题类型交换机与队列绑定时,可以在绑定键中使用通配符,支持的通配符有两种:
*
代表一个或者多个单词(单词指的是用.
分割的字符)#
代表一个单词
主题类型的交换机,适用于一些根据业务主题或者消息等级过滤消息的场景。
Fanout
广播类型
广播类型的交换机与队列绑定时,不需要指定绑定键,因此生产者发送消息时也不需要携带路由键。
消息到达交换机后,所有与之绑定的队列都会受到相同的消息副本。
Headers
不依赖于Routing Key与Binding Key的匹配规则来路由消息,而是根据消息内容中的headers属性进行匹配。在绑定Exchange和Queue时指定一组键值对,当消息发送到Exchange时,会取得消息的headers,也是键值对的形式,将两组键值对进行匹配,如果完全匹配,则将消息发送到对应的Queue中。
Headers类型不常用。
RabbitMQ安装
RabbitMQ是使用erlang语言实现的,所以安装前需要具备erlang环境。
#安装必要依赖
yum -y install gcc glibc-devel make ncurses-devel openssl-devel xmlto perl wget
#下载erlang
wget http://erlang.org/download/otp_src_21.3.tar.gz
tar -xvf otp_src_21.3.tar.gz
cd otp_src_21.3
./configure --prefix=/usr/local/erlang
#编译
make && make install
# 配置erlang环境变量,在配置文件尾部加入
vim /etc/profile
export PATH=$PATH:/usr/local/erlang/bin
source /etc/profile
#验证erlang安装是否成功
erl
#安装rabbitmq
wget https://dl.bintray.com/rabbitmq/all/rabbitmq-server/3.8.4/rabbitmq-server-generic-unix-3.8.4.tar.xz
xz -d rabbitmq-server-generic-unix-3.8.4.tar.xz
tar -xvf rabbitmq-server-generic-unix-3.8.4.tar
#配置环境变量
#后台启动 默认端口5672
cd /usr/local/soft/rabbitmq_server-3.8.4/sbin
./rabbitmq-server -detached
#前台启动
./rabbitmq-server start
#因为guest用户只能在本机访问,添加一个admin用户,密码也是admin
./rabbitmqctl add_user admin admin
./rabbitmqctl set_user_tags admin administrator
./rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
#启用管理插件 页面默认端口15672
./rabbitmq-plugins enable rabbitmq_management
可靠性投递
在使用MQ实现异步通信时,消息丢失了怎么办?消息重复了怎么办?
这个问题便是RabbitMQ的可靠性投递问题,RabbitMQ在设计的时候就考虑到了这些问题,也提供了很多保证消息可靠的机制。
消息从发送到最终消费者消费,有四个主要的环节:
- 生产者发送消息到broker
- 消息从exchange路由到queue
- 消息存储在queue中
- 消费者消费queue中的消息
接下来从这四个步骤来看RabbitMQ可靠性投递的实现。
生产者发送消息到broker
什么情况下会出现发送消息失败?
可能是网络问题或者Broker的问题(比如磁盘满了或者磁盘故障了),导致消息发送失败,生产者不能确定Broker有没有正确的接收到消息。
因此,在生产者发送消息到服务端后,需要服务端给生产者一个应答,以保证消息有被接收到。
在RabbitMQ里提供了两种消息确认机制:
- Transaction 事务模式
- Confirm 确认模式
事务模式
try{
//开启事务模式
channel.txSelect();
channel.basicPublish(exchange, routingKey, null, (msg + " " + routingKey).getBytes());
channel.txCommit();
} catch (IOException e) {
//回滚
channel.txRollback();
}
在创建channel时可以设置为事务模式,如果提交成功则表明一定发送成功。
事务模式 有一个严重的缺点:它是阻塞的,一条消息没有发送完成,不能发送下一条消息。
确认模式
除了事务模式外,rabbitMq还提供了另外一种方法来确认消息是否发送成功,者就是确认模式。
确认模式有三种:
- 普通确认模式
- 批量确认模式
- 异步确认模式
普通确认模式
//开启确认模式
channel.confirmSelect();
channel.basicPublish(exchange, routingKey, null, (msg + " " + routingKey).getBytes());
if (channel.waitForConfirms()) {
System.out.println("发送成功");
}
普通确认模式就是发送一条,确认一条。
批量确认模式
try {
channel.confirmSelect();
//批量发送消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("exchange", "routingKey", null, msg.getBytes());
}
//等待确认消息
channel.waitForConfirmsOrDie();
System.out.println("批量确认成功");
} catch (Exception e) {
e.printStackTrace();
System.out.println("批量确认失败");
}
批量确认,只要有一条消息未被broker确认,就会抛出异常。没有异常就表示都发送成功。
批量确认效率高,但是也带来问题:批量的数量多少合适? 太大的话,如果有一条未被确认,则需要全部重发;太少的话,则效率提升不明显。
异步确认模式
//异步确认
//用来维护未确认消息的deliveryTag
SortedSet<Long> unConfirmSet = Collections.synchronizedSortedSet(new TreeSet<>());
//异步监听
channel.addConfirmListener(new ConfirmListener() {
//broker确认处理成功的
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
//如果是批量,则表示返回的tag前面的消息都被确认
unConfirmSet.headSet(deliveryTag + 1L).clear();
} else {
unConfirmSet.remove(deliveryTag);
}
}
//被broker丢失的消息,被丢失的消息也有可能被消费者消费,但是broker不保证这一点
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
//如果是批量
unConfirmSet.headSet(deliveryTag + 1L).clear();
}else {
unConfirmSet.remove(deliveryTag);
}
//TODO 重发或者其他操作
}
});
channel.confirmSelect();
for (int i = 0; i < 100; i++) {
//发送消息后将发送的消息的序列号记录下来
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("exchange", "routingKey", null, msg.getBytes());
unConfirmSet.add(nextPublishSeqNo);
}
异步确认这是通broker过回调返回确认信息。
消息从交换机路由到队列
服务端接收到消息后,接下来就是交换机按照规则将消息路由到队列中。这个过程可能会出现根据rounting key找不到队列等情况。
如果发生了异常情况,该如何处理?
通常,会有两种方式来处理:
- 消息回发
- 路由到备份交换机
消息回发
消息回发就是当交换机无法根据routingKey将消息路由到队列中时,服务端将消息发回给生产者
//消息回发监听
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.printf("收到无法路由的消息,回发:\n" +
"replyCode:%d replyText:%s exchange:%s
routingKey:%s\nmsg:%s", replyCode, replyText,
exchange, routingKey, new String(body));
}
});
//将mandatory标志位置为true
channel.basicPublish("direct_exchange", "1.1simple.test", true,null, msg.getBytes());
mandatory:是basicPublish
方法中的一个标志位,用来控制路由失败后是否回发。置为true时则会回发,置为false时,在无法路由时,broker会将消息丢失。
basicPublish
方法其实还有另一个标志位–immediate,它表示交换机路到队列后,根据是否有消费者来消费消息来判断是否回发。置为true时,如果没有消费者连接交换机绑定的队列,那么就会触发回发。在RabbitMQ3.0以后的版本里,去掉了immediate参数的支持。
路由到备份交换机
当交换机无法路由消息时 ,将消息路由到指定的路由消息。
通过
alternate-exchange
参数指定备份交换机。
队列存储消息
队列接收消息后,就会一直储存着消息,直到有消费者来消费消息。
如果RabbitMQ发生服务异常或者硬件故障,这个会导致内存中消息的丢失,所以需要将队列中的消息持久化,以便重启后消息任可被消费。
持久化也有多种方案:
- 队列持久化
- 交换机持久化
- 消息持久化
队列持久化
/**
* 声明队列
* @param durable 是否持久化,没有持久化的队列,队列中的消息只存在内存中
* @param exclusive 是否排他,
排他队列:1.只对首次声明它的连接(connection)可见
2.会在连接断开时自动删除
* @param autoDelete 是否自动删除,自动删除意味着队列在没有消费者连接时,会自动删除
*/
Queue.DeclareOk queueDeclare(String queue,
boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments) throws IOException;
交换机持久化
/**
声明交换机
* @param durable 交换机持久化意味服务端重启后,交换机会复活
*/
Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable);
消息持久化
//发送消息时可以为消息配置一些属性
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.deliveryMode(2)
.contentEncoding("UTF-8")
.expiration("10000")
.build();
channel.basicPublish("exchange", "1.1simple.test", true,basicProperties, msg.getBytes());
deliveryMode:表示消息是否持久化:1-不持久化,2-持久化
消息投递到消费者
队列中的消息最终会投递到响应的消费端,如果消费者在接收消息后还未处理就发生异常或者处理过程中发生异常,那么这次消费就是失败的。
服务端应该需要知道消费者对消息的处理情况,并决定是否重新投递这条消息。
RabbitMQ提供了消费者的消息确认机制,消费端可以自动或者手动的发送ACK给服务端。
自动ACK
自动ACK是默认的方式,在消费者受到消息时,就自动ACK,而不关心消息是否被处理。
//设置autoAck为true
channel.basicConsume(firstQueue, true, consumerA);
手动ACK
即什么时候发送ack由客户端自己控制。
Consumer consumerA = 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("consumerA " +LocalDateTime.now() + " :received msg :" + msg);
//消费完后发送ack
channel.basicAck(envelope.getDeliveryTag(),true);
}
};
//设置手动ack
channel.basicConsume(firstQueue, false, consumerA);
集群与高可用
为了保证高可用,中间件一般都需要做集群。
RabbitMQ集群方式
RabbitMQ集群通过.erlang.cookie
文件来验证身份,需要在所有节点上都保持一致。
服务端的端口是5672,web端的端口是15672,集群的端口是25672,集群中的节点就是通过25672端口来两两通信。
集群节点
RabbitMQ集群中有两种类型的节点:
- 内存节点
- 磁盘节点
磁盘节点
将元数据(包括队列名字属性、交换机的类型名字属性、绑定、vhost等)放在磁盘中。
未指定类型的情况下,默认为磁盘节点。
集群中至少要有一个磁盘节点,用来持久化数据,否则当全部内存节点崩溃时数据就会被丢失。
内存节点
将原数据放在内存中,如果是持久化的消息,会同时放在内存和磁盘中。
内存节点会将磁盘节点的地址存放在磁盘中,否则崩溃重启后就没法同步数据了。
通常会 把应用连接到内存节点,这样会比较快。而磁盘节点则是用作备份。
集群模式
RabbitMQ有两种集群模式:
- 普通模式
- 镜像队列模式
普通模式
普通集群模式下,节点间只会同步元数据而不会同步消息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLAY5kWQ-1690249919714)(img\common_cluster.png)]
如果队列1的消息之存储在节点A上,
- 如果生产者连接的是节点C,那么要将消息通过交换机1路由到队列1中,最终消息会被转发到节点A上存储。
- 如果消费者连接的是节点B,要从队列1中取消息,消息会从节点A转发到节点B。
普通模式的设计思想就是分片存储,相同的消息不需要在每台机器都存储,这样就可以线性的增加性能和存储容量。
但是这种模式无法保证高可用,如果其中一台机器挂了,那其上的队列中的消息就全部丢失了。
镜像队列模式
镜像队列模式就是在普通模式的基础上,增加同步队列消息的镜像节点。
镜像队列模式下,队列中的消息会在集群节点中相互同步,这样可用性更高,但是系统性能自然就降低了,节点过多时,同步的代价也会变得很大。
那么同步镜像就会存在两个关键问题或者配置:
- 同步到哪些节点作为镜像? 全部节点还是部分节点?
- 如何同步?自动同步还是手动同步?
RabbitMQ中提供了两种配置: ha-mode
和ha-sync-mode
。
ha-mode
指明镜像队列的模式,有效值有三个
ha-mode | params | 说明 |
---|---|---|
all | 空 | 队列镜像到所有节点 |
exactly | 数量 | 队列镜像到指定数目的节点。如果集群内节点数小于该数字,则同步到所有节点。 |
nodes | 节点名 | 队列镜像到指定名称的节点上。 |
ha-sync-mode
队列消息同步方式有两个有效值:automatic(自动)和manual(手动),默认是automatic