一、选择合适的队列.
实际上是可以选择三种队列类型的,classic经典队列,Quorum仲裁队列,Stream流式队列。
后面这两种队列也是RabbitMQ在最近的几个大的版本中推出的新的队列类型。3.8.x推出了Quorum仲裁队列,3.9.x推出了Stream流式队列。这些新的队列类型都是RabbitMQ针对现代新的业务场景做出的大的改善。最明显的,以往的RabbitMQ版本,如果消息产生大量积累就会严重影响消息收发的性能。而这两种新的队列可以极大的提升RabbitMQ的消息堆积性能。
1、Classic经典队列
这是RabbitMQ最为经典的队列类型。在单机环境中,拥有比较高的消息可靠性。
在这个图中可以看到,经典队列可以选择是否持久化(Durability)以及是否自动删除(Auto delete)两个属性。
其中,Durability有两个选项,Durable和Transient。 Durable表示队列会将消息保存到硬盘,这样消息的安全性更高。但是同时,由于需要有更多的IO操作,所以生产和消费消息的性能,相比Transient会比较低。
Auto delete属性如果选择为是,那队列将在至少一个消费者已经连接,然后所有的消费者都断开连接后删除自己。
2、Quorum仲裁队列
仲裁队列,是RabbitMQ从3.8.0版本,引入的一个新的队列类型,整个3.8.X版本,也都是在围绕仲裁队列进行完善和优化。仲裁队列相比Classic经典队列,在分布式环境下对消息的可靠性保障更高。官方文档中表示,未来会使用Quorum仲裁队列代替传统Classic队列。
Quorum是基于Raft一致性协议实现的一种新型的分布式消息队列,他实现了持久化,多备份的FIFO队列,主要就是针对RabbitMQ的镜像模式设计的。简单理解就是quorum队列中的消息需要有集群中多半节点同意确认后,才会写入到队列中。
同时,Quorum是以牺牲很多高级队列特性为代价,来进一步保证消息在分布式环境下的高可靠。
从整体功能上来说,Quorum队列是在Classic经典队列的基础上做减法,因此对于RabbitMQ的长期使用者而言,其实是会影响使用体验的。他与普通队列的区别:
官方这个比较图就能看到,Quorum队列大部分功能都是在Classic队列基础上做减法,比如Nondurable queues表示是非持久化的内存队列。Exclusivity表示独占队列,即表示队列只能由声明该队列的Connection连接来进行使用,包括队列创建、删除、收发消息等,并且独占队列会在声明该队列的Connection断开后自动删除。
其中有个特例就是Poison Message handling(处理有毒的消息)。所谓毒消息是指消息一直不能被消费者正常消费(可能是由于消费者失败或者消费逻辑有问题等),就会导致消息不断的重新入队,这样这些消息就成为了毒消息。
这些毒消息应该有保障机制进行标记并及时删除。Quorum队列会持续跟踪消息的失败投递尝试次数,并记录在"x-delivery-count"这样一个头部参数中。然后,就可以通过设置 Delivery limit参数来定制一个毒消息的删除策略。当消息的重复投递次数超过了Delivery limit参数阈值时,RabbitMQ就会删除这些毒消息。当然,如果配置了死信队列的话,就会进入对应的死信队列。
3、Stream流式队列
Stream队列是RabbitMQ自3.9.0版本开始引入的一种新的数据队列类型。这种队列类型的消息是持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景
Stream队列的核心是以append-only只添加的日志来记录消息,整体来说,就是消息将以append-only的方式持久化到日志文件中,然后通过调整每个消费者的消费进度offset,来实现消息的多次分发。下方有几个属性也都是来定义日志文件的大小以及保存时间。如果你熟悉Kafka或者RocketMQ,会对这种日志记录消息的方式非常熟悉。
- large fan-outs 大规模分发
当想要向多个订阅者发送相同的消息时,以往的队列类型必须为每个消费者绑定一个专用的队列。如果消费者的数量很大,这就会导致性能低下。而Stream队列允许任意数量的消费者使用同一个队列的消息,从而消除绑定多个队列的需求。 - Replay/Time-travelling 消息回溯
RabbitMQ已有的这些队列类型,在消费者处理完消息后,消息都会从队列中删除,因此,无法重新读取已经消费过的消息。而Stream队列允许用户在日志的任何一个连接点开始重新读取数据。 - Throughput Performance 高吞吐性能
Strem队列的设计以性能为主要目标,对消息传递吞吐量的提升非常明显。 - Large logs 大日志
RabbitMQ一直以来有一个让人诟病的地方,就是当队列中积累的消息过多时,性能下降会非常明显。但是Stream队列的设计目标就是以最小的内存开销高效地存储大量的数据。使用Stream队列可以比较轻松的在队列中积累百万级别的消息。
4、如何使用不同类型的队列
1、Quorum队列
Quorum队列与Classic队列的使用方式是差不多的。最主要的差别就是在声明队列时有点不同。
如果要声明一个Quorum队列,则只需要在后面的arguments中传入一个参数,x-queue-type,参数值设定为quorum。
Map<String,Object> params = new HashMap<>();
params.put("x-queue-type","quorum");
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
Quorum队列的消息是必须持久化的,所以durable参数必须设定为true,如果声明为false,就会报错。同样,exclusive参数必须设置为false。这些声明,在Producer和Consumer中是要保持一致的。
2、Stream队列
如果要声明一个Stream队列,则 x-queue-type参数要设置为 stream 。
//声明Stream队列的方式。
Map<String,Object> params = new HashMap<>();
params.put("x-queue-type","stream");
params.put("x-max-length-bytes", 20_000_000_000L); // maximum stream size: 20 GB
params.put("x-stream-max-segment-size-bytes", 100_000_000); // size of segment files: 100 MB
channel.queueDeclare(QUEUE_NAME, true, false, false, params);
与Quorum队列类似, Stream队列的durable参数必须声明为true,exclusive参数必须声明为false。
这其中,x-max-length-bytes 表示日志文件的最大字节数。x-stream-max-segment-size-bytes 每一个日志文件的最大大小。这两个是可选参数,通常为了防止stream日志无限制累计,都会配合stream队列一起声明。
然后,当要消费Stream队列时,要重点注意他的三个必要的步骤:
- channel必须设置basicQos属性。 与Spring框架集成使用时,channe对象可以在@RabbitListener声明的消费者方法中直接引用,Spring框架会进行注入。
- 正确声明Stream队列。 在Queue对象中传入声明Stream队列所需要的参数。
- 消费时需要指定offset。 与Spring框架集成时,可以通过注入Channel对象,使用原生API传入offset属性。
例如用原生API创建Stream类型的Consumer时,还必须添加一个参数x-stream-offset,表示从队列的哪个位置开始消费。
Map<String,Object> consumeParam = new HashMap<>();
consumeParam.put("x-stream-offset","last");
channel.basicConsume(QUEUE_NAME, false,consumeParam, myconsumer);
x-stream-offset的可选值有以下几种:
- first: 从日志队列中第一个可消费的消息开始消费
- last: 消费消息日志中最后一个消息
- next: 相当于不指定offset,消费不到消息。
- Offset: 一个数字型的偏移量
- Timestamp:一个代表时间的Data类型变量,表示从这个时间点开始消费。例如 一个小时前 Date timestamp = new Date(System.currentTimeMillis() - 60 * 60 * 1_000)
二、死信队列
死信队列是RabbitMQ中非常重要的一个特性。简单理解,他是RabbitMQ对于未能正常消费的消息进行的一种补救机制。死信队列也是一个普通的队列,同样可以在队列上声明消费者,继续对消息进行消费处理。
对于死信队列,在RabbitMQ中主要涉及到几个参数。
x-dead-letter-exchange: mirror.dlExchange 对应的死信交换机
x-dead-letter-routing-key: mirror.messageExchange1.messageQueue1 死信交换机routingkey
x-message-ttl: 3000 消息过期时间
durable: true 持久化,这个是必须的
在这里,x-dead-letter-exchange指定一个交换机作为死信交换机,然后x-dead-letter-routing-key指定交换机的RoutingKey。而接下来,死信交换机就可以像普通交换机一样,通过RoutingKey将消息转发到对应的死信队列中。
1、何时会产生死信
有以下三种情况,RabbitMQ会将一个正常消息转成死信
- 消息被消费者确认拒绝。消费者把requeue参数设置为true(false),并且在消费后,向RabbitMQ返回拒绝。channel.basicReject或者channel.basicNack。
- 消息达到预设的TTL时限还一直没有被消费。
- 消息由于队列已经达到最长长度限制而被丢掉
设置TTL有两种方式,一是通过配置策略指定,另一种是给队列单独声明TTL
策略配置方式 - Web管理平台配置 或者 使用指令配置 60000为毫秒单位
rabbitmqctl set_policy TTL “.*” ‘{“message-ttl”:60000}’ --apply-to queues
在声明队列时指定 - 同样可以在Web管理平台配置,也可以在代码中配置:
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);
2、死信队列的配置方式
RabbitMQ中有两种方式可以声明死信队列,一种是针对某个单独队列指定对应的死信队列。另一种就是以策略的方式进行批量死信队列的配置。
针对多个队列,可以使用策略方式,配置统一的死信队列。
rabbitmqctl set_policy DLX “.*” ‘{“dead-letter-exchange”:“my-dlx”}’ --apply-to queues
针对队列单独指定死信队列的方式主要是之前提到的三个属性。
channel.exchangeDeclare("some.exchange.name", "direct");
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "some.exchange.name");
channel.queueDeclare("myqueue", false, false, false, args);
这些参数,也可以在RabbitMQ的管理页面进行配置。例如配置策略时:
另外,在对队列进行配置时,只有Classic经典队列和Quorum仲裁队列才能配置死信队列
3、关于参数x-dead-letter-routing-key
死信在转移到死信队列时,他的Routing key 也会保存下来。但是如果配置了x-dead-letter-routing-key这个参数的话,routingkey就会被替换为配置的这个值。
另外,死信在转移到死信队列的过程中,是没有经过消息发送者确认的,所以并不能保证消息的安全性。
4、如何确定一个消息是不是死信
消息被作为死信转移到死信队列后,会在Header当中增加一些消息。在官网的详细介绍中,可以看到很多内容,比如时间、原因(rejected,expired,maxlen)、队列等。然后header中还会加上第一次成为死信的三个属性,并且这三个属性在以后的传递过程中都不会更改。
- x-first-death-reason
- x-first-death-queue
- x-first-death-exchange
5、基于死信队列实现延迟队列
其实从前面的配置过程能够看到,所谓死信交换机或者死信队列,不过是在交换机或者队列之间建立一种死信对应关系,而死信队列可以像正常队列一样被消费。他与普通队列一样具有FIFO的特性。对死信队列的消费逻辑通常是对这些失效消息进行一些业务上的补偿。
RabbitMQ中,是不存在延迟队列的功能的,而通常如果要用到延迟队列,就会采用TTL+死信队列的方式来处理
三、懒队列
懒队列会尽可能早的将消息内容保存到硬盘当中,并且只有在用户请求到时,才临时从硬盘加载到RAM内存当中。
懒队列的设计目标是为了支持非常长的队列(数百万级别)。队列可能会因为一些原因变得非常长-也就是数据堆积。
- 消费者服务宕机了
- 有一个突然的消息高峰,生产者生产消息超过消费者
- 消费者消费太慢了
默认情况下,RabbitMQ接收到消息时,会保存到内存以便使用,同时把消息写到硬盘。但是,消息写入硬盘的过程中,是会阻塞队列的。RabbitMQ虽然针对写入硬盘速度做了很多算法优化,但是在长队列中,依然表现不是很理想,所以就有了懒队列的出现。
在代码中可以通过x-queue-mode参数指定
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
设定一个策略,在策略中指定queue-mode 为 lazy。
rabbitmqctl set_policy Lazy “^lazy-queue$” ‘{“queue-mode”:“default”}’ --apply-toqueues
注意的是,当一个队列被声明为懒队列,那即使队列被设定为不持久化,消息依然会写入到硬盘中。如果是在集群模式中使用,这会给集群资源带来很大的负担。
懒队列适合消息量大且长期有堆积的队列,可以减少内存使用,加快消费速度。但是这是以大量消耗集群的网络及磁盘IO为代价的。
四、联邦插件
1、插件的作用
在企业中有很多大型的分布式场景,在这些业务场景下,希望服务也能够同样进行分布式部署。这样即可以提高数据的安全性,也能够提升消息读取的性能。
例如,某大型企业,可能在北京机房和长沙机房分别搭建RabbitMQ服务,然后希望长沙机房需要同步北京机房的消息,这样可以让长沙的消费者服务可以直接连接长沙本地的RabbitMQ,而不用费尽周折去连接北京机房的RabbitMQ服务。这时要如何进行数据同步呢?
2、使用步骤
- 启动插件
RabbitMQ的官方运行包中已经包含了Federation插件。只需要启动后就可以直接使用。
# 确认联邦插件
rabbitmq-plugins list|grep federation
# 启用联邦插件
rabbitmq-plugins.bat enable rabbitmq_federation
# 启用联邦插件的管理平台支持
rabbitmq-plugins.bat enable rabbitmq_federation_management
插件启用完成后,可以在管理控制台的Admin菜单看到两个新增选项 Federation Status和FederationUpstreams。
Upstream表示是一个外部的服务节点,在RabbitMQ中,可以是一个交换机,也可以是一个队列。他的配置方式是由下游服务主动配置一个与上游服务的链接,然后数据就会从上游服务主动同步到下游服务中。
接下来我们用本地localhost的RabbitMQ服务来模拟DownStream下游服务,去指向一个192.168.65.112服务器上搭建的RabbitMQ服务,搭建一个联邦交换机Federation Exchange。
服务的名字Name属性随意,URI指向远程服务器(配置方式参看页面上的示例):amqp://admin:admin@192.168.65.112:5672/
下面的Federated exchanges parameters和Federated queues parameters分别指定Upstream(也就是远程服务器)的Exchange和Queue。如果不指定,就是用和DownStream中相同的Exchange和Queue。如果UpStream里没有,就创建新的Exchange和Queue。
3、配置Federation策略
接下来需要配置一个指向上游服务的Federation策略。在配置策略时可以选择是针对Exchange交换机还是针对Queue队列。配置策略时,同样有很多参数可以选择配置。最简化的一个配置如下:
4、测试
配置完Upstream和对应的策略后,进入Federation Status菜单就能看到Federation插件的执行情况。状态为running表示启动成功,如果配置出错,则会提示失败原因
并且在fed_exchange的详情页中也能够看到绑定关系。这里要注意一下他给出了一个默认的Routing_key。
接下来就可以尝试在上游服务Worker2的fed_exchange中发送消息,消息会同步到Local本地的联邦交换机中,从而被对应的消费者消费到。
五、消息分片存储插件
1、插件的作用
Lazy Queue懒队列机制提升了消息在RabbitMQ中堆积的能力,但是最终,消息还是需要消费者处理消化。但是如何在消费者的处理能力有限的前提下提升消费者的消费速度呢?RabbitMQ提供的Sharding插件,就提供了一种思路。
上面的懒队列其实就是针对这个问题的一种解决方案。但是很显然,懒队列的方式属于治标不治本。真正要提升RabbitMQ单队列的吞吐量,还是要从数据也就是消息入手,只有将数据真正的分开存储才行。
RabbitMQ提供的Sharding插件,就是一个可选的方案。他会真正将一个队列中的消息分散存储到不同的节点上,并提供多个节点的负载均衡策略实现对等的读与写功能。
2、使用步骤
1、启用Sharding插件
rabbitmq-plugins enable rabbitmq_sharding
2、配置Sharding策略
3、新增带Sharding的Exchange交换机
4、往分片交换机上发送消息
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.56.10");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/mirror");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//发送者只管往exchange里发消息,而不用关心具体发到哪些queue里。
channel.exchangeDeclare(EXCHANGE_NAME, "x-modulus-hash");
String message = "LOG INFO 44444";
for(int i = 0 ; i < 10000 ; i ++){
channel.basicPublish(EXCHANGE_NAME, String.valueOf(i), null, message.getBytes());
}
channel.close();
connection.close();
}
启动后,就会在RabbitMQ上声明一个sharding_exchange。查看这个交换机的详情,可以看到他的分片情况:
并且,一万条消息被平均分配到了三个队列当中
5、消费分片交换机上的消息
数据分片后,还是希望能够像一个普通队列一样消费到完整的数据副本。这时,Sharding插件提供了一种伪队列的消费方式。你可以声明一个名字为 exchangename 的伪队列,然后像消费一个普通队列一样去消费这一系列的碎片队列。
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.56.10");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
factory.setVirtualHost("/mirror");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUENAME,true,false,false,null);
Consumer myconsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body)
throws IOException {
System.out.println("========================");
String routingKey = envelope.getRoutingKey();
System.out.println("routingKey >" + routingKey);
String contentType = properties.getContentType();
System.out.println("contentType >" + contentType);
long deliveryTag = envelope.getDeliveryTag();
System.out.println("deliveryTag >" + deliveryTag);
System.out.println("content:" + new String(body, "UTF-8"));
// (process the message components here ...)
//消息处理完后,进行答复。答复过的消息,服务器就不会再次转发。
//没有答复过的消息,服务器会一直不停转发。
// channel.basicAck(deliveryTag, false);
}
};
//三个分片就需要消费三次。
//sharding插件的实现原理就是将basicConsume方法绑定到分片队列中连接最少的一个队列上。
String consumeerFlag1 = channel.basicConsume(QUEUENAME, true, myconsumer);
System.out.println("c1:"+consumeerFlag1);
String consumeerFlag2 = channel.basicConsume(QUEUENAME, true, myconsumer);
System.out.println("c2:"+consumeerFlag2);
String consumeerFlag3 = channel.basicConsume(QUEUENAME, true, myconsumer);
System.out.println("c3:"+consumeerFlag3);
}
3、注意事项
使用Sharding插件后,Producer发送消息时,只需要指定虚拟Exchange,并不能确定消息最终会发往哪一个分片队列。而Sharding插件在进行消息分散存储时,虽然尽量是按照轮询的方式,均匀的保存消息。但是,这并不能保证消息就一定是均匀的。
首先,这些消息在分片的过程中,是没有考虑消息顺序的,这会让RabbitMQ中原本就不是很严谨的消息顺序变得更加雪上加霜。所以,Sharding插件适合于那些对于消息延迟要求不严格,以及对消费顺序没有任何要求的的场景。
然后,Sharding插件消费伪队列的消息时,会从消费者最少的碎片中选择队列。这时,如果你的这些碎片队列中已经有了很多其他的消息,那么再去消费伪队列消息时,就会受到这些不均匀数据的影响。所以,如果使用Sharding插件,这些碎片队列就尽量不要单独使用了。