RabbitMq笔记
文章正文:
MQ介绍
消息队列是一中FIFO的数据结构。主要作用有三种:
- 异步
例子:快递员发快递,直接到客户家效率会很低。引入菜鸟驿站后,快递员只需要把快递放到菜鸟驿站,就可以继续发其他快递去了。客户再按自己的时间安排去菜鸟驿站取快递。
作用:异步能提高系统的响应速度、吞吐量。 - 解耦
1、服务之间进行解耦,才可以减少服务之间的影响。提高系统整体的稳定性以及可扩展性。
2、另外,解耦后可以实现数据分发。生产者发送一个消息后,可以由一个或者多个消费者进行消费,并且消费者的增加或者减少对生产者没有影响。 - 削峰
例子:长江每年都会涨水,但是下游出水口的速度是基本稳定的,所以会涨水。引入三峡大坝后,可以把水储存起来,下游慢慢排水。
作用:以稳定的系统资源应对突发的流量冲击。
优缺点
上面MQ的所用也就是使用MQ的优点。 但是引入MQ也是有他的缺点的:
- 系统可用性降低
系统引入的外部依赖增多,系统的稳定性就会变差。一旦MQ宕机,对业务会产生影响。这就需要考虑如何保证MQ的高可用。 - 系统复杂度提高
引入MQ后系统的复杂度会大大提高。以前服务之间可以进行同步的服务调用,引入MQ后,会变为异步调用,数据的链路就会变得更复杂。并且还会带来其他一些问题。比如:如何保证消费不会丢失?不会被重复调用?怎么保证消息的顺序性等问题。 - 消息一致性问题
A系统处理完业务,通过MQ发送消息给B、C系统进行后续的业务处理。如果B系统处理成功,C系统处理失败怎么办?这就需要考虑如何保证消息数据处理的一致性。
RabbitMq两种集群模式:
1、普通集群:访问的节点没有需要的数据的话就从其他节点同步数据过来
2、镜像集群:每个节点同步成相同数据
RabbitMQ的集群节点分为disk和ram。
添加服务到worker2集群
rabbitmqctl join_cluster --ram rabbit@worker2
--ram 表示以Ram节点加入集群。disk节点会将元数据保存到硬盘当中,而ram节点只是在内存中保存元数据。
1、由于ram节点减少了很多与硬盘的交互,所以,ram节点的元数据使用性能会比较高。但是,同时,这也意味着元数据的安全性是不如disk节点的。在我们这个集群中,worker1和worker3都以ram节点的身份加入到worker2集群里,因此,是存在单点故障的。如果worker2节点服务崩溃,那么元数据就有可能丢失。在企业进行部署时,性能与安全性需要自己进行平衡。
2、这里说的元数据仅仅只包含交换机、队列等的定义,而不包含具体的消息。因此,ram节点的性能提升,仅仅体现在对元数据进行管理时,比如修改队列queue,交换机exchange,虚拟机vhosts等时,与消息的生产和消费速度无关。
3、如果一个集群中,全部都是ram节点,那么元数据就有可能丢失。这会造成集群停止之后就启动不起来了。RabbitMQ会尽量阻止创建一个全是ram节点的集群,但是并不能彻底阻止。所以,综合考虑,官方其实并不建议使用ram节点,更推荐保证集群中节点的资源投入,使用disk节点。
RabbitMq基础概念
RabbitMQ是基于AMQP协议开发的一个MQ产品
虚拟主机virtual host
RabbitMQ出于服务器复用的想法,可以在一个RabbitMQ集群中划分出多个虚拟主机,每一个虚拟主机都有AMQP的全套基础组件,并且可以针对每个虚拟主机进行权限以及数据分配,并且不同虚拟主机之间是完全隔离的。
连接 Connection
客户端与RabbitMQ进行交互,首先就需要建立一个TCP连接,这个连接就是Connection。
信道 Channel
一旦客户端与RabbitMQ建立了连接,就会分配一个AMQP信道 Channel。每个信道都会被分配一个唯一的ID。也可以理解为是客户端与RabbitMQ实际进行数据交互的通道,我们后续的大多数的数据操作都是在信道 Channel 这个层面展开的。
RabbitMQ为了减少性能开销,也会在一个Connection中建立多个Channel,这样便于客户端进行多线程连接,这些连接会复用同一个Connection的TCP通道,所
以在实际业务中,对于Connection和Channel的分配也需要根据实际情况进行考量。
交换机 Exchange
这是RabbitMQ中进行数据路由的重要组件。消息发送到RabbitMQ中后,会首先进入一个交换机,然后由交换机负责将数据转发到不同的队列中。RabbitMQ中有多种不同类型的交换机来支持不同的路由策略。从Web管理界面就能看到,在每个虚拟主机中,RabbitMQ都会默认创建几个不同类型的交换机来。
交换机多用来与生产者打交道。生产者发送的消息通过Exchange交换机分配到各个不同的Queue队列上,而对于消息消费者来说,通常只需要关注自己感兴趣的队列就可以了。
队列 Queue
队列是实际保存数据的最小单位。队列结构天生就具有FIFO的顺序,消息最终都
会被分发到不同的队列当中,然后才被消费者进行消费处理。
Classic 经典队列
经典队列可以选择是否持久化(Durability)以及是否自动删除(Auto delete)两个属性。
其中,Durability有两个选项,Durable和Transient。 Durable表示队列会将消息保存到硬盘,这样消息的安全性更高。但是同时,由于需要有更多的IO操作,所以生产和消费消息的性能,相比Transient会比较低。
Auto delete属性如果选择为是,那队列将在至少一个消费者已经连接,然后所有的消费者都断开连接后删除自己。
Quorum 仲裁队列
Quorum是基于Raft一致性协议实现的一种新型的分布式消息队列,他实现了持久化,多备份的FIFO队列,主要就是针对RabbitMQ的镜像模式设计的。简单理解就是quorum队列中的消息需要有集群中多半节点同意确认后,才会写入到队列中。
从官方这个比较图就能看到,Quorum队列大部分功能都是在Classic队列基础上做减法,比如Non-durable queues表示是非持久化的内存队列。Exclusivity表示独占队列,即表示队列只能由声明该队列的Connection连接来进行使用,包括队列创建、删除、收发消息等,并且独占队列会在声明该队列的Connection断开后自动删除。
其中有个特例就是这个Poison Message(有毒的消息)。所谓毒消息是指消息一直不能被消费者正常消费(可能是由于消费者失败或者消费逻辑有问题等),就会导致消息不断的重新入队,这样这些消息就成为了毒消息。这些读消息应该有保障机制进行标记并及时删除。Quorum队列会持续跟踪消息的失败投递尝试次数,并记录在"x-delivery-count"这样一个头部参数中。然后,就可以通过设置 Delivery limit 参数来定制一个毒消息的删除策略。当消息的重复投递次数超过了Delivery limit参数阈值时,RabbitMQ就会删除这些毒消息。当然,如果配置了死信队列的话,就会进入对应的死信队列。
Quorum队列更适合于 队列长期存在,并且对容错、数据安全方面的要求比低延迟、不持久等高级队列更能要求更严格的场景。例如 电商系统的订单,引入MQ 后,处理速度可以慢一点,但是订单不能丢失。
也对应以下一些不适合使用的场景:
1、 一些临时使用的队列:比如transient临时队列,exclusive独占队列,或者经常会修改和删除的队列。
2、 对消息低延迟要求高: 一致性算法会影响消息的延迟。
3、 对数据安全性要求不高:Quorum队列需要消费者手动通知或者生产者手动确认。
4、 队列消息积压严重 : 如果队列中的消息很大,或者积压的消息很多,就不要使用Quorum队列。Quorum队列当前会将所有消息始终保存在内存中,直到达到内存使用极限。
Stream队列
这种队列类型的消息是持久化到磁盘并且具备分布式备份的,更适合于消费者多,读消息非常频繁的场景。
Stream队列的核心是以append-only只添加的日志来记录消息,整体来说,就是消息将以append-only的方式持久化到日志文件中,然后通过调整每个消费者的消费进度offset,来实现消息的多次分发。下方有几个属性也都是来定义日志文件的大小以及保存时间。这种队列提供了RabbitMQ已有的其他队列类型不太好实现的四个特点:
1、 large fan-outs 大规模分发
当想要向多个订阅者发送相同的消息时,以往的队列类型必须为每个消费者绑定一个专用的队列。如果消费者的数量很大,这就会导致性能低下。而Stream队列允许任意数量的消费者使用同一个队列的消息,从而消除绑定多个队列的需求。
2、 Replay/Time-travelling 消息回溯
RabbitMQ已有的这些队列类型,在消费者处理完消息后,消息都会从队列中删除,因此,无法重新读取已经消费过的消息。而Stream队列允许用户在日志的任何一个连接点开始重新读取数据。
3、 Throughput Performance 高吞吐性能
Strem队列的设计以性能为主要目标,对消息传递吞吐量的提升非常明显。
4、 Large logs 大日志
RabbitMQ一直以来有一个让人诟病的地方,就是当队列中积累的消息过多时,性能下降会非常明显。但是Stream队列的设计目标就是以 小的内存开销高效地存储大量的数据。
RabbitMQ编程模型
原生API
maven依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.9.0</version>
</dependency>
基础编程模型
step1、首先创建连接,获取Channel
点击查看代码
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
step2、声明queue队列
点击查看代码
channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments);
// 声明Quorum队列的方式就是添加一个x-queue-type参数,指定为quorum。默认是classic
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 如果要声明一个Stream队列,则 x-queue-type参数要设置为 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);
//注意:1、同样,durable参数必须是true,exclusive必须是false。 -你应该会想到,对于这两种队列,这两个参数就是多余的了,未来可以直接删除。
//2、x-max-length-bytes 表示日志文件的 大字节数。x-stream-maxsegment-size-bytes 每一个日志文件的 大大小。这两个是可选参数,通常为了防止stream日志无限制累计,都会配合stream队列一起声明。
//3、 声明的队列,如果服务端没有,那么会自动创建。但是如果服务端有了这个队列,那么声明的队列属性必须和服务端的队列属性一致才行。
step3、Producer根据应用场景发送消息到queue
点击查看代码
// 其中exchange是一个Producer与queue的中间交互机制。可以让Producer把消息按一定的规则发送到不同的queue,不需要的话就传空字符串
channel.basicPublish(String exchange, String routingKey, BasicProperties props,message.getBytes("UTF-8")) ;
step4、Consumer消费消息
定义消费者,消费消息进行处理,并向RabbitMQ进行消息确认。确认了之后就表明这个消息已经消费完了,否则RabbitMQ还会继续让别的消费者实例来处理。
主要收集了两种消费方式
1、被动消费模式,Consumer等待rabbitMQ 服务器将message推送过来再消费。一般是启一个一直挂起的线程来等待。
channel.basicConsume(String queue, boolean autoAck, Consumer callback);
其中autoAck是个关键。autoAck为true则表示消息发送到该Consumer后就被 Consumer消费掉了,不需要再往其他Consumer转发。为false则会继续往其他 Consumer转发。要注意如果每个Consumer一直为false,会导致消息不停的被转发,不停的吞噬系统资源, 终造成宕机。
2、另一种是主动消费模式。Comsumer主动到rabbitMQ服务器上去获取指定的 messge进行消费。
GetResponse response = channel.basicGet(QUEUE_NAME, boolean autoAck);
3.Stream队列消费 在当前版本下,消费Stream队列时,需要注意三板斧的设置。
- channel必须设置basicQos属性。
- 正确声明Stream队列。
- 消费时需要指定offset。
点击查看代码
Connection connection = RabbitMQUtil.getConnection();
Channel channel = connection.createChannel();
//1、这个属性必须设置。
channel.basicQos(100);
//2、声明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);
//Consumer接口还一个实现QueueConsuemr 但是代码注释过期了。
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);
}
};
//3、消费时,必须指定offset。 可选的值:
// first: 从日志队列中第一个可消费的消息开始消费
// last: 消费消息日志中最后一个消息
// next: 相当于不指定offset,消费不到消息。
// Offset: 一个数字型的偏移量
// Timestamp:一个代表时间的Data类型变量,表示从这个时间点开始消费。例如 一个
小时前 Date timestamp = new Date(System.currentTimeMillis() - 60 * 60 * 1_000)
Map<String,Object> consumeParam = new HashMap<>();
consumeParam.put("x-stream-offset","next");
channel.basicConsume(QUEUE_NAME, false,consumeParam, myconsumer);
channel.close();
step5、完成以后关闭连接,释放资源
channel.close();
消息场景
RabbitMQ官方提供了总共七种消息模型,这其中,6 RPC部分是使用RabbitMQ来实现RPC远程调用,这个场景通常不需要使用MQ来实现,所以也就不当作重点来学习。而7 Publisher Confirms是当前版本新引进来的一种消息模型,对保护消息可靠性有很重要的意义。
1:hello world体验
直接的方式,P端发送一个消息到一个指定的queue,中间不需要任何exchange 规则。C端按queue方式进行消费。
关键代码:(其实关键的区别也就是几个声明上的不同。) producer:
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
channel.basicPublish("", QUEUE_NAME, null, message.getBytes("UTF-8"));
consumer:
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
2: Work queues 工作序列
Producer消息发送给queue,服务器根据负载方案决定把消息发给一个指定的Consumer处理。工作任务模式,领导部署一个任务,由下面的一个员工来处理。
producer:
点击查看代码
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null); //任务一般是不能因为消息中间件的服务而被耽误的,所以durable设置成了true,这样,即使rabbitMQ服务断了,这个消息也不会消失
channel.basicPublish("",TASK_QUEUE_NAME,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes("UTF-8"));
点击查看代码
channel.queueDeclare(TASK_QUEUE_NAME, true, false, false, null);
channel.basicQos(1);
channel.basicConsume(TASK_QUEUE_NAME, false, consumer);
中间件最为关键的分发方式。这里,RabbitMQ默认是采用的fairdispatch,也叫round-robin模式,就是把消息轮询,在所有consumer中轮流发送。这种方式,没有考虑消息处理的复杂度以及consumer的处理能力。而他们改进后的方案,是consumer可以向服务器声明一个prefetchCount,我把他叫做预处理能力值。channel.basicQos(prefetchCount);表示当前这个consumer可以同时处理几个message。这样服务器在进行消息发送前,会检查这个consumer当前正在处理中的message(message已经发送,但是未收到 consumer的basicAck)有几个,如果超过了这个consumer节点的能力值,就不再往这个consumer发布。
这种模式,官方也指出还是有问题的,消息有可能全部阻塞,所有consumer节点都超过了能力值,那消息就阻塞在服务器上,这时需要自己及时发现这个问题,采取措施,比如增加consumer节点或者其他策略
3:Publish/Subscribe 订阅 发布 机制
type为fanout 的exchange:
这个机制是对上面的一种补充。也就是把preducer与Consumer进行进一步的解耦。producer只负责发送消息,至于消息进入哪个queue,由exchange来分配。如上图,就是把producer发送的消息,交由exchange同时发送到两个queue里,然后由不同的Consumer去进行消费。
关键代码 ===》 producer: //只负责往exchange里发消息,后面的事情不管。
点击查看代码
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
点击查看代码
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, EXCHANGE_NAME, "");
4:Routing 基于内容的路由
type为”direct” 的exchange
这种模式一看图就清晰了。 在上一章 exchange 往所有队列发送消息的基础上,增
加一个路由配置,指定exchange如何将不同类别的消息分发到不同的queue上。
关键代码===> Producer:
点击查看代码
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
点击查看代码
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);
5:Topics 话题
type为"topic" 的exchange
这个模式也就在上一个模式的基础上,对routingKey进行了模糊匹配,单词之间用,隔开,* 代表一个具体的单词。# 代表0个或多个单词。
关键代码===> Producer:
点击查看代码
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.basicPublish(EXCHANGE_NAME, routingKey, null, message.getBytes("UTF-8"));
点击查看代码
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
channel.queueBind(queueName, EXCHANGE_NAME, routingKey1);
channel.queueBind(queueName, EXCHANGE_NAME, routingKey2);
channel.basicConsume(queueName, true, consumer);
6:RPC 远程调用
7:Publisher Confirms 发送者消息确认
RabbitMQ的消息可靠性是非常高的,但是他以往的机制都是保证消息发送到了MQ之后,可以推送到消费者消费,不会丢失消息。但是发送者发送消息是否成功是没有保证的。我们可以回顾下,发送者发送消息的基础API:Producer.basicPublish方法是没有返回值的,也就是说,一次发送消息是否成功,应用是不知道的,这在业务上就容易造成消息丢失。而这个模块就是通过给发送者提供一些确认机制,来保证这个消息发送的过程是成功的。
发送者确认模式默认是不开启的,所以如果需要开启发送者确认模式,需要手动在channel中进行声明。
channel.confirmSelect();
在官网的示例中,重点解释了三种策略:
1、发布单条消息 即发布一条消息就确认一条消息。核心代码:
点击查看代码
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
channel.basicPublish("", queue, null, body.getBytes());
channel.waitForConfirmsOrDie(5_000);
}
官方说明了,其实channel底层是异步工作的,会将channel阻塞住,然后异步等待服务端发送一个确认消息,才解除阻塞。但是我们在使用时,可以把他当作一个同步工具来看待。
然后如果到了超时时间,还没有收到服务端的确认机制,那就会抛出异常。然后通常处理这个异常的方式是记录错误日志或者尝试重发消息,但是尝试重发时一定要注意不要使程序陷入死循环。
2、发送批量消息
之前单条确认的机制会对系统的吞吐量造成很大的影响,所以稍微中和一点的方式就是发送一批消息后,再一起确认。
核心代码:
点击查看代码
int batchSize = 100;
int outstandingMessageCount = 0;
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
ch.basicPublish("", queue, null, body.getBytes());
outstandingMessageCount++; 9
if (outstandingMessageCount == batchSize) {
ch.waitForConfirmsOrDie(5_000);
outstandingMessageCount = 0;
}
}
if (outstandingMessageCount > 0) {
ch.waitForConfirmsOrDie(5_000);
}
3、 异步确认消息
实现的方式也比较简单,Producer在channel中注册监听器来对消息进行确认。
核心代码就是一个:
channel.addConfirmListener(ConfirmCallback var1, ConfirmCallback var2);
按说监听只要注册一个就可以了,那为什么这里要注册两个呢?发送者在发送完消息后,就会执行第一个监听器callback1,然后等服务端发过来的反馈后,再执行第二个监听器 callback2。
然后关于这个ConfirmCallback,这是个监听器接口,里面只有一个方法: void handle(long sequenceNumber, boolean multiple) throws IOException; 这方法中的两个参数:
- sequenceNumer:这个是一个唯一的序列号,代表一个唯一的消息。在RabbitMQ中,他的消息体只是一个二进制数组,所以默认消息是没有序列号的。而RabbitMQ提供了一个方法int sequenceNumber = channel.getNextPublishSeqNo();来生成一个全局递增的序列号。然后应用程序需要自己来将这个序列号与消息对应起来。
- multiple:这个是一个Boolean型的参数。如果是true,就表示这一次只确认了当前一条消息。如果是false,就表示RabbitMQ这一次确认了一批消息,在 sequenceNumber之前的所有消息都已经确认完成了。
SpringBoot集成
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
要特别注意下版本。我们这里采用的是SpringBoot的2.6.7版本的依赖发布包。不同版本下的配置方式会有变化。
然后所有的基础运行环境都在application.properties中进行配置。所有配置以 spring.rabbitmq开头。通常按照示例进行一些基础的必要配置就可以跑了。
配置生产者
声明队列
使用RabbitmqTemplate对象发送消息
生产者的所有属性都已经在application.properties配置文件中进行配置。项目启动时,就会在Spring容器中初始化一个RabbitmqTemplate对象,然后所有的发送消息操作都通过这个对象来进行。
使用@RabbitListener注解声明消费者
当要消费Stream队列时,还是要重点注意他的三个必要的步骤:
- channel必须设置basicQos属性。 channel对象可以在@RabbitListener声明的消费者方法中直接引用,Spring框架会进行注入。
- 正确声明Stream队列。 通过往Spring容器中注入Queue对象的方式声明队列。在Queue对象中传入声明Stream队列所需要的参数。
- 消费时需要指定offset。 可以通过注入Channel对象,使用原生API传入offset属性。
关于Stream队列
在目前版本下,使用RabbitMQ的SpringBoot框架集成,可以正常声明Stream 队列,往Stream队列发送消息,但是无法直接消费Stream队列了。关于这个问题,还是需要从Stream队列的三个重点操作入手。SpringBoot框架集成RabbitMQ后,为了简化编程模型,就把channel,connection等这些关键对象给隐藏了,目前框架下,无法直接接入这些对象的注入过程,所以无法直接使用。
如果非要使用Stream队列,那么有两种方式,一种是使用原生API的方式,在SpringBoot框架下自行封装。另一种是使用RabbitMQ的Stream 插件。在服务端通过Strem插件打开TCP连接接口,并配合单独提供的Stream客户端使用。这种方式对应用端的影响太重了,并且并没有提供与SpringBoot框架的集成,还需要自行完善,因此选择使用的企业还比较少。
SpringCloudStream集成
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
// <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
<artifactId>spring-cloud-stream-binder-rabbit</artifactId>
</dependency>
基础使用方法
1、 声明Sink 消息消费者
点击查看代码
@Component
@EnableBinding(Sink.class)
public class MessageReceiver {
private Logger logger = LoggerFactory.getLogger(MessageReceiver.class);
@EventListener
@StreamListener(Sink.INPUT)
public void process(Object message) {
System.out.println("received message : " + message);
logger.info("received message : {}", message);
}
}
2、 使用Source 消息生产者 发送消息
点击查看代码
@Component
@EnableBinding(Source.class)
public class MessageSender {
@Autowired
private Source source;
public void sendMessage(Object message) {
MessageBuilder<Object> builder = MessageBuilder.withPayload(message);
source.output().send(builder.build());
}
}
3、 在SpringBoot的配置文件中,增加配置
点击查看代码
server.port=8080
spring.cloud.stream.bindings.output.destination=streamExchange
spring.cloud.stream.bindings.input.destination=streamExchange
spring.cloud.stream.bindings.input.group=stream
spring.cloud.stream.bindings.input.content-type=text/plain
这样三个步骤,就完成了与本地RabbitMQ的对接。 接下来增加SpringBoot启动类,以及测试用的Controller就可以启动测试了。
点击查看代码
@SpringBootApplication
public class SCApplication {
public static void main(String[] args) {
SpringApplication.run(SCApplication.class, args);
}
}
@RestController
//@EnableBinding(Source.class)
public class SendMessageController {
@Autowired
private Source source;
@GetMapping("/send")
public Object send(String message) {
MessageBuilder<String> messageBuilder = MessageBuilder.withPayload(message); source.output().send(messageBuilder.build());
return "message sended : "+message;
}
}
理解SpringCloudStream干了些什么
1、 配置RabbitMQ服务器
在SpringBoot的autoconfigure包当中,有个 RabbitProperties类,这个类就会解析application.properties中以spring.rabbitmq开头的配置。里面配置了跟RabbitMQ相关的主要参数,包含服务器地址等。里面对每个参数也都提供了默认值。默认就是访问本地的RabbitMQ服务。
点击查看代码
#这几个是默认配置。
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/
2、 在RabbitMQ中声明Exchange和Queue
SCStream帮我们在RabbitMQ的根虚拟机上创建了一个topic类型的scstreamExchange交换机,然后在这个交换机上绑定了一个 scstreamExchange.stream队列,绑定的RoutingKey是#。 而程序中的消息发送者是将消息发送到scstreamExchange交换机,然后RabbitMQ将消息转发到scstreamExchange.stream队列,消息接收者从队列接收到消息。这个流程,就是 SpringCloudStream在背后为我们做的事情。
深入玩转SCStream
1、配置Binder
SCStream是通过Binder来定义一个外部消息服务器。具体对于RabbitMQ来说,Binder就是一个Exchange的抽象。默认情况下,RabbitMQ的binder使用了SpringBoot的ConnectionFactory,所以,他也支持spring-boot-starter-amqp组件中提供的对RabbitMQ的所有配置信息。这些配置信息在 application.properties里都以spring.rabbitmq开头。而在SCStream框架中,也支持配置多个Binder访问不同的外部消息服务器(例如同时访问kafka和rabbitmq,或者同时访问rabbitmq的多个virtual-host),就可以通过Binder进行定义。配置的方式都是通过spring.cloud.stream.binders. [bindername].environment.[props]=[value]的格式来进行配置。另外,如果配置了多个binder,也可以通过spring.cloud.stream.default-binder属性指定默认的 binder。例如
点击查看代码
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.host=localhost
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.port=5672
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.username= guest
spring.cloud.stream.binders.testbinder.environment.spring.rabbitmq.password= guest
这个配置方式就配置了一个名为testbinder的Binder。
2、Binding配置
Binding是SCStream中实际进行消息交互的桥梁。在RabbitMQ中,一个 binding可以对应消费者的一个queue,在发送消息时,也可以直接对应一个 exchange。在SCStream中,就是通过将Binding和Binder建立绑定关系,然后客户端就只需要通过Binding来进行实际的消息收发。 在SCStream框架中,配置Binding首先需要进行声明。声明Binding的方式是在应用中通过@EnableBinding注解,向Spring容器中注入一个Binding接口对象。在这个接口中,增加@Input注解指定接收消息的Binding,而通过@Output注解指定发送消息的Binding。在SCStream中,默认提供了Source、Sink、Processor三个接口对象,这三个对象都是简单的接口,可以直接拿来使用,当然也可以配置自己的Binding接口对象。比如Source,他的定义就是这样的:
点击查看代码
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
MessageChannel output();
}
通过这个@Output直接,就声明出了一个Binding对象,他的名字就叫做 output。对于RabbitMQ,就对应一个queue。SCStream就会将这个output声明为一个消息发送队列。
接下来就可以在应用中使用@EnableBinding(Source.class),声明这个Binding 对象。接下来,就可以在Spring应用中使用@Autowired注入,然后通过 source.output()方法获取到MesasgeChannel对象,进而发送消息了。
这时,如果不对output这个Binding做任何配置,应用启动后,会在RabbitMQ 中声明出一个默认的exchange和queue。但是默认的名字都太奇怪,而且很多细节功能不够好用。所以,通常都会对这个Binding进行配置。配置的方式都是在application.properties中配置。所有配置都是按照spring.cloud.stream.binding.[bindingname].[props]=[value]的格式进行指定。例如:
点击查看代码
spring.cloud.stream.bindings.output.destination=scstreamExchange
spring.cloud.stream.bindings.output.group=myoutput
spring.cloud.stream.bindings.output.binder=testbinder
这样就指定了output这个Binding对应的Exchange。
注意:1、如果不希望每个Binding都单独指定Binder,就可以配置默认的Binder。
2、对于binding,可以指定group所属组的属性。Group这个概念在RabbitMQ中是不存在的,但是SCStream会按照这个group属性,声明一个名为scstreamExchange.myoutput的queue队列,并与 scstreamExchange建立绑定关系。
3、SCStream的分组消费策略
SCStream中的消费者分组策略,即不同group的消费者,都会消费到所有的message消息。而在同一个goup
中,每个message消息,只会被消费一次。
这种分组消费的策略,严格来说,在RabbitMQ中是不存在的,RabbitMQ是通过不同类型的Exchange来实现不同的消费策略。而使用SCStream框架,就可以直接在RabbitMQ中实现这种分组消费的策略。
点击查看代码
spring.cloud.stream.bindings.consumer1.group=stream
spring.cloud.stream.bindings.consuemr2.group=stream
spring.cloud.stream.bindings.consumer3.group=stream
spring.cloud.stream.bindings.consumer4.group=stream2
例如这样,就声明了两个消费者组。consumer1,consumer2,consumer3是一个组,consuemr4是另一个组。接下来,可以自行验证一下消息的分发过程。对于这种分组消费的策略,SCStream框架不光提供了实现,同时还提供了扩展。可以对每个组进行分区(partition), 例如做这样的配置:
点击查看代码
#消息生产者端配置
#启动发送者分区
spring.cloud.stream.bindings.output.producer.partitioned=true
#指定参与消息分区的消费端节点数量
spring.cloud.stream.bindings.output.producer.partition-count=2
#只有消费端分区ID为1的消费端能接收到消息
spring.cloud.stream.bindings.output.producer.partition-key-expression=1
#消息消费者端配置
#启动消费分区
spring.cloud.stream.bindings.input.consumer.partitioned=true
#参与分区的消费端节点个数
spring.cloud.stream.bindings.input.consumer.instance-count=2
#设置该实例的消费端分区ID
spring.cloud.stream.bindings.input.consumer.instance-index=1
通过这样的分组策略,当前这个消费者实例就只会消费奇数编号的消息,而偶数编号的消息则不会发送到这个消费者中。注意:这并不是说偶数编号的消息就不会被消费,只是不会被当前这个实例消费而已。
SCStream框架虽然实现了这种分组策略机制,但是其实是不太严谨的,当把分区数量和分区ID不按套路分配时,并没有太多的检查和日志信息,但是就是收不到消息。
另外,在@StreamListener注解中还有condition属性也可以配置消费者的分配逻辑,该属性支持一个SPELl表达式,只接收满足条件的消息。
4、使用原生消息转发机制
SCStream其实自身实现了一套事件驱动的流程。这种流程,对于各种不同的MQ 产品都是一样的。但是,毕竟每个MQ产品的实现机制和功能特性是不一样的,所以,SCStream还是提供了一套针对各个MQ产品的兼容机制。
在RabbitMQ的实现中,所有个性化的属性配置实现都是以spring.cloud.stream.rabbit开头,支持对binder、producer、consumer进行单独配置。
点击查看代码
#绑定exchange
spring.cloud.stream.binding.<bindingName>.destination=fanoutExchange
#绑定queue
spring.cloud.stream.binding.<bindingName>.group=myQueue
#不自动创建queue
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.bindQueue=false
#不自动声明exchange(自动声明的exchange都是topic)
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.declareExchange=false
#队列名只声明组名(前面不带destination前缀)
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.queueNameGroupOnly=true
#绑定rouytingKey
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.bindingRoutingKey=myRoutingKey
#绑定exchange类型
spring.cloud.stream.rabbit.bindings.<bindingName>.consumer.exchangeType= <type>
#绑定routingKey
spring.cloud.stream.rabbit.bindings.<bindingName>.producer.routingKeyExpression='myRoutingKey'
通过这些配置可以按照RabbitMQ原生的方式进行声明。例如,SCStream自动创建的Exchange都是Topic类型的,如果想要用其他类型的Exchange交换机,就可以手动创建交换机,然后在应用中声明不自动创建交换机。
所有可配置的属性,参见github仓库中的说明。例如,如果需要声明一个Quorum仲裁队列,那么只要给这个Binding配置quorum.enabled属性,值为true 就可以了。
5、使用SCStream配置死信队列
死信(Dead letter)队列是RabbitMQ中的一个高级功能,所谓死信,就是长期没有人消费的消息。RabbitMQ中有以下几种情况会产生死信:
- 消息被拒绝(basic.reject/baskc.nack)并且设置消息不重新返回队列 (配置 spring.rabbitmq.listener.default-requeue-rejected=true=false 。这个属性默认是true,就是消息处理失败后,就会重新返回队列,后续重新投递。但是这里需要注意,如果队列已经满了,那就会循环不断的报错,这时候就要考虑死信
了) - 队列达到最大长度消息
- TTL过期
在RabbitMQ中,有一类专门处理死信的Exchange交换机和Queue队列。通过 RabbitMQ的死信队列功能,可以很好的用来实现延迟队列或者消息补发之类的功能。
RabbitMQ的死信队列实现机制,是在正常队列上声明一个死信交换机dlExchange,然后这个死信交换机dlExchange可以像正常交换机Exchange一样,去绑定队列,分发消息等。其配置方式,就是在队列中增加声明几个属性来指定死信交换机。而这几个队列属性,即可以在服务器上直接配置,也可以用原生API配置,还可以用SpringBoot的方式声明Queue队列来实现,并且在SCStream框架中也支持定制。主要就是这几个属性:
点击查看代码
x-dead-letter-exchange: mirror.dlExchange 对应的死信交换机
x-dead-letter-routing-key: mirror.messageExchange1.messageQueue1 死信交换机 routing-key
x-message-ttl: 3000 消息过期时间
durable: true 持久化,这个是必须的。
配置完成后,在管理页面也能看到队列信息:
这样配置完成后,在当前队列中的消息,经过3秒无人消费,就会通过指定的死信交换机mirror.dlExchange,分发到对应的死信队列中。
关于如何配置这些属性,在之前声明Quorum仲裁队列和Stream队列时,都有说明。
而在SCStream框架中,就可以通过以下的方式进行配置:
点击查看代码
spring.cloud.stream.rabbit.bindings.input.destination=DlqExchange
spring.cloud.stream.rabbit.bindings.input.group=dlQueue
spring.cloud.stream.rabbit.bindings.output.destination=messageExchange1
spring.cloud.stream.rabbit.bindings.output.producer.requiredgroups=messageQueue1
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.autoBindDlq=true
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.ttl=3000
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.deadLetterExchang e=DlqExchange
spring.cloud.stream.rabbit.rabbit.bindings.output.producer.deadLetterQueueNa me=DlqExchange.dlQueue
通过这样的一组配置,从output这个Binding发送的消息,经过3秒后,就会被 input这个Binding对应的消费者消费到了。
6、扩展的事件监听机制
另外,在SCStream框架的Sink消费者端,还可以添加@EventListener注解。加入这个注解后,这个Sink消费者,不光可以消费MQ消息,还能监控很多Spring内的事件,像 AsyncConsumerStartedEvent、ApplicationReadyEvent(springBoot 启动事件)、ServletRequestHandledEvent(请求响应事件)等等。而使用这些功能,我们可以将Spring的应用事件作为业务事件一样处理,这对于构建统一的Spring应用监控体系是非常有用的。
7、SCStream框架总结
对于事件驱动这个应用场景来说,SCStream框架绝对是一个举足轻重的产品。
一方面,他极大的简化的事件驱动的开发过程,让技术人员可以减少对于不同MQ产品的适应过程,更多的关注业务逻辑。另一方面,SCStream框架对各种五花八门的 MQ产品提供了一种统一的实现流程,从而可以极大的减少应用对于具体MQ产品的依赖,极大提高应用的灵活性。例如如果应用某一天想要从RabbitMQ切换成Kafka 或者RocketMQ等其他的MQ产品,如果采用其他框架,需要对应用程序做非常大的改动。但是,如果使用SCStream框架,那么基本上就是换Maven依赖,调整相关配置就可以了。应用代码基本不需要做任何改动。
Headers路由
官网示例中的集中路由策略, direct,fanout,topic等这些Exchange,都是以 routingkey为关键字来进行消息路由的,但是这些Exchange有一个普遍的局限就是都是只支持一个字符串的形式,而不支持其他形式。Headers类型的Exchange就是一种忽略routingKey的路由方式。他通过Headers来进行消息路由。这个headers 是一个键值对,发送者可以在发送的时候定义一些键值对,接受者也可以在绑定时定义自己的键值对。当键值对匹配时,对应的消费者就能接收到消息。匹配的方式有两种,一种是all,表示需要所有的键值对都满足才行。另一种是any,表示只要满足其中一个键值就可以了。而这个值,可以是List、Boolean等多个类型。SpringBoot代码示例:
点击查看代码
@ApiOperation(value="header发送接口",notes="发送到headerExchange。exchange转发消息时,不再管routingKey,而是根据header条件进行转发。")
@GetMapping(value="/headerSend")
public Object headerSend(String txTyp,String busTyp,String message) throws AmqpException, UnsupportedEncodingException {
if(null == txTyp) {
txTyp="0";
}
if(null == busTyp) {
busTyp="0";
}
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentType(MessageProperties.CONTENT_TYPE_TEXT_PLAIN);
messageProperties.setHeader("txTyp", txTyp);
messageProperties.setHeader("busTyp", busTyp);
//fanout模式只往exchange里发送消息。分发到exchange下的所有queue
rabbitTemplate.send("headerExchange", "uselessRoutingKey", new Message(message.getBytes("UTF-8"),messageProperties));
return "message sended : txTyp >"+txTyp+";busTyp > "+busTyp;
}
@Configuration
public class HeaderConfig {
//声明queue
@Bean
public Queue headQueueTxTyp1() {
return new Queue(MyConstants.QUEUE_TXTYP1);
}
@Bean
public Queue headQueueBusTyp1() {
return new Queue(MyConstants.QUEUE_BUSTYP1);
}
@Bean
public Queue headQueueTxBusTyp() {
return new Queue(MyConstants.QUEUE_TXBUSTYP1);
}
//声明exchange
@Bean
public HeadersExchange setHeaderExchange() {
return new HeadersExchange(MyConstants.EXCHANGE_HEADER);
}
//声明Binding
//绑定header中txtyp=1的队列。header的队列匹配可以用mathces和exisits
@Bean
public Binding bindHeaderTxTyp1() {
return BindingBuilder.bind(headQueueTxTyp1()).to(setHeaderExchange()).where("txTyp").matches("1");
}
//绑定Header中busTyp=1的队列。
@Bean
public Binding bindHeaderBusTyp1() {
return BindingBuilder.bind(headQueueBusTyp1()).to(setHeaderExchange()).where("busTyp").matches("1");
}
//绑定Header中txtyp=1或者busTyp=1的队列。
@Bean
public Binding bindHeaderTxBusTyp1() {
Map<String,Object> condMap = new HashMap<>();
condMap.put("txTyp", "1");
condMap.put("busTyp", "1");
// return BindingBuilder.bind(headQueueTxBusTyp()).to(setHeaderExchange()).whereAny(new String[] {"txTyp","busTyp"}).exist();
return BindingBuilder.bind(headQueueTxBusTyp()).to(setHeaderExchange()).whereAny(condMap).match();
}
}
分组消费模式
实例测试
要使用分组消费策略,需要在生产者和消费者两端都进行分组配置。
生产者端 核心配置
点击查看代码
#指定参与消息分区的消费端节点数量
spring.cloud.stream.bindings.output.producer.partition-count=2
#只有消费端分区ID为1的消费端能接收到消息
spring.cloud.stream.bindings.output.producer.partition-key-expression=1
消费者端启动两个实例,组成一个消费者组
消费者1 核心配置
点击查看代码
#启动消费分区
spring.cloud.stream.bindings.input.consumer.partitioned=true
#参与分区的消费端节点个数
spring.cloud.stream.bindings.input.consumer.instance-count=2
#设置该实例的消费端分区ID
spring.cloud.stream.bindings.input.consumer.instance-index=1
消费者2 核心配置
点击查看代码
#启动消费分区
spring.cloud.stream.bindings.input.consumer.partitioned=true
#参与分区的消费端节点个数
spring.cloud.stream.bindings.input.consumer.instance-count=2
#设置该实例的消费端分区ID
spring.cloud.stream.bindings.input.consumer.instance-index=0
这样就完成了一个分组消费的配置。两个消费者实例会组成一个消费者组。而生产者发送的消息,只会被消费者1 消费到(生产者的partition-key-expression 和 消费者的 instance-index 匹配)。
实现原理
实际上,在跟踪查看RabbitMQ的实现时,就会发现,Spring Cloud Stream在增加了消费者端的分区设置后,会对每个有效的分区创建一个单独的queue,这个队列的队列名是在原有队列名后面加上一个索引值。而发送者端的消息,会 终发送到这个带索引值的队列上,而不是原队列上。这样就完成了分区消费。
我们的示例中,分组表达式是直接指定的,这样其实是丧失了灵活性的。实际开发中,可以将这个分组表达式放到消息的header当中,在发送消息时指定,这样就更有灵活性了。
例如:将生产者端的分组表达式配置为header['partitonkey']
点击查看代码
#生产者端设置
spring.cloud.stream.bindings.output.producer.partition-keyexpression=header['partitionkey']
这样,就可以在发送消息时,给消息指定一个header属性,来控制控制分组消费的结果。
点击查看代码
Message message = MessageBuilder.withPayload(str).setHeader("partitionKey", 0).build();
source.output().send(message);
死信队列
对于死信队列,在RabbitMQ中主要涉及到几个参数。
点击查看代码
x-dead-letter-exchange: mirror.dlExchange 对应的死信交换机
x-dead-letter-routing-key: mirror.messageExchange1.messageQueue1 死信交换机 routing-key
x-message-ttl: 3000 消息过期时间
durable: true 持久化,这个是必须的。
在这里,x-dead-letter-exchange指定一个交换机作为死信交换机,然后x-dead-letter-routing-key指定交换机的RoutingKey。而接下来,死信交换机就可以像普通交换机一样,通过RoutingKey将消息转发到对应的死信队列中。
何时会产生死信
有以下三种情况,RabbitMQ会将一个正常消息转成死信
- 消息被消费者确认拒绝。消费者把requeue参数设置为true(false),并且在消费后,向RabbitMQ返回拒绝。channel.basicReject或者channel.basicNack。
- 消息达到预设的TTL时限还一直没有被消费。
- 消息由于队列已经达到最长长度限制而被丢掉
TTL即 长存活时间 Time-To-Live 。消息在队列中保存时间超过这个TTL,即会被认为死亡。死亡的消息会被丢入死信队列,如果没有配置死信队列的话,RabbitMQ会保证死了的消息不会再次被投递,并且在未来版本中,会主动删除掉这些死掉的消息。
设置TTL有两种方式,一是通过配置策略指定,另一种是给队列单独声明TTL策略配置方式 - Web管理平台配置 或者 使用指令配置 60000为毫秒单位
点击查看代码
rabbitmqctl set_policy TTL ".*" '{"message-ttl":60000}' --applyto queues
在声明队列时指定 - 同样可以在Web管理平台配置,也可以在代码中配置:
点击查看代码
Map<String, Object> args = new HashMap<String, Object>(); args.put("x-message-ttl", 60000); channel.queueDeclare("myqueue", false, false, false, args);
RabbitMQ中,是不存在延迟队列的功能的,而通常如果要用到延迟队列,就会采用TTL+死信队列的方式来处理。
RabbitMQ提供了一个rabbitmq_delayed_message_exchange插件,可以实现延迟队列的功能,但是并没有集成到官方的发布包当中,需要单独去下载。
死信队列的配置方式
RabbitMQ中有两种方式可以声明死信队列,一种是针对某个单独队列指定对应的死信队列。另一种就是以策略的方式进行批量死信队列的配置。
针对多个队列,可以使用策略方式,配置统一的死信队列。
rabbitmqctl set_policy DLX ".*" '{"dead-letter-exchange":"my-dlx"}' --applyto 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仲裁队列才能配置死信队列,而目前Stream流式队列,并不支持配置死信队列。
关于参数x-dead-letter-routing-key
死信在转移到死信队列时,他的Routingkey也会保存下来。但是如果配置了xdead-letter-routing-key这个参数的话,routingkey就会被替换为配置的这个值。
另外,死信在转移到死信队列的过程中,是没有经过消息发送者确认的,所以并不能保证消息的安全性。
如何确定一个消息是不是死信
消息被作为死信转移到死信队列后,会在Header当中增加一些消息。在官网的详细介绍中,可以看到很多内容,比如时间、原因(rejected,expired,maxlen)、队列等。然后header中还会加上第一次成为死信的三个属性,并且这三个属性在以后的传递过程中都不会更改。
- x-first-death-reason
- x-first-death-queue
- x-first-death-exchange
消费优先级与流量控制
关于消费队列的优先级,关键是x-priority 这个参数,可以指定队列的优先级。
默认情况下,RabbitMQ会根据round-robin策略,把消息均匀的给不同的消费者进行处理。但是有了优先级之后,RabbitMQ会保证优先级高的队列先进行消费,而同一优先级的队列,还是会使用round-robin轮询策略来进行分配。
与之对应的是RabbitMQ的流量控制配置,关键是channel.basicQos(int prefetch_size, int prefetch_count, boolean global)。 这个方法中,prefetch_count设置了当前消费者节点最多保持的未答复的消息个数,prefetch_size设置了当前消费节点最多保持的未答复的消息大小,然后global参数为true则表示该配置针对当前channel的所有队列,而默认情况下是false,表示该配置只针对当前消费者队列。最常用的方式就是只设定一个prefetch_count参数。
这两个参数实际上都是为了配置当前消费节点的消息吞吐量。当消费者集群中的业务处理能力或者消息配置不一样时,可以通过给不同的消费节点配置不同的 prefetch_count,再结合消费优先级的配置来实现流量控制策略。
远程数据分发插件- Federation Plugin
如果我们需要在多个RabbitMQ服务之间进行消息同步,那么,首选的方案自然是通过RabbitMQ集群来进行。但是,在某些网络状况比较差的场景下,搭建集群会不太方便。例如,某大型企业,可能在北京机房和长沙机房分别搭建RabbitMQ 服务,然后希望长沙机房需要同步北京机房的消息,这样可以让长沙的消费者服务可以直接连接长沙本地的RabbitMQ,而不用费尽周折去连接北京机房的RabbitMQ服务。这时要如何进行数据同步呢?搭建一个网络跨度这么大的集群显然就不太划算。这时就可以考虑使用RabbitMQ的Federation插件,搭建联邦队列Federation。通过Federation可以搭建一个单向的数据同步通道(当然,要搭建双相同步也是可以的)。
启动插件
RabbitMQ的官方运行包中已经包含了Federation插件。只需要启动后就可以直接使用。
点击查看代码
# 确认联邦插件
rabbitmq-plugins list|grep federation
[ ] rabbitmq_federation 3.9.15
[ ] rabbitmq_federation_management 3.9.15
# 启用联邦插件
rabbitmq-plugins.bat enable rabbitmq_federation
# 启用联邦插件的管理平台支持
rabbitmq-plugins.bat enable rabbitmq_federation_management
插件启用完成后,可以在管理控制台的Admin菜单看到两个新增选项 Federation Status和Federation Upstreams。
配置Upstream
Upstream表示是一个外部的服务节点,在RabbitMQ中,可以是一个交换机,也可以是一个队列。他的配置方式是由下游服务主动配置一个与上游服务的链接,然后数据就会从上游服务主动同步到下游服务中。
接下来我们用本地localhost的RabbitMQ服务来模拟DownStream下游服务,去指向一个worker2机器上搭建的RabbitMQ服务,搭建一个联邦交换机FederationExchange。
先在本地用程序的方式,声明一个交换机和交换队列。
点击查看代码
public class DownStreamConsumer {
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/mirror");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare("fed_exchange","direct");
channel.queueDeclare("fed_queue",true,false,false,null);
channel.queueBind("fed_queue","fed_exchange","routKey");
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);
}
};
channel.basicConsume("fed_queue", true, myconsumer);
}
}
然后在本地RabbitMQ中配置一个上游服务,服务的名字Name属性随意,URI指向Worker2上的/mirror虚拟机(配置方式参看页面上的示例): amqp://admin:admin@worker2:5672/ (注意,在添加时,如果指定了Upstream 的Virtual Host是mirror,那么在URI中就不能再添加Virtual host配置了,默认会在上游服务中使用相同的VirtualHost。)
注意:
1、其他的相关参数,可以在页面上查看帮助。例如,默认情况下,Upstream会使用和Downstream同名的Exchange,但是也可以通过Upstream配置中的Exchange参数指定不同的。
2、关于Virtual Host虚拟机配置,如果在配置Upstream时指定了Virtual Host属性,那么在URI中就不能再添加Virtaul Host配置了,默认会在Upstream上使用相同的Virtual Host。
配置Federation策略
接下来需要配置一个指向上游服务的Federation策略。在配置策略时可以选择是针对Exchange交换机还是针对Queue队列。配置策略时,同样有很多参数可以选择配置。最简化的一个配置如下:
注意:每个策略的Definition部分,至少需要指定一个Federation目标。federation-upstream-set参数表示是以set集合的方式针对多个Upstream生效,all表示是全部Upstream。而federation-upstream参数表示只对某一个Upstream生效。
测试
配置完Upstream和对应的策略后,进入Federation Status菜单就能看到Federation插件的执行情况。状态为running表示启动成功,如果配置出错,则会提示失败原因这个提示非常非常简单
然后,在远程服务Worker2的RabbitMQ服务中,可以看到对应生成的
Federation交换机。
接下来就可以尝试在上游服务Worker2的fed_exchange中发送消息,消息会同步到Local本地的联邦交换机中,从而被对应的消费者消费到。
懒队列 Lazy Queue
懒队列会尽可能早的将消息内容保存到硬盘当中,并且只有在用户请求到时,才临时从硬盘加载到RAM内存当中。
懒队列的设计目标是为了支持非常长的队列(数百万级别)。队列可能会因为一些原因变得非常长-也就是数据堆积。
- 费者服务宕机了
- 有一个突然的消息高峰,生产者生产消息超过消费者
- 消费者消费太慢了
默认情况下,RabbitMQ接收到消息时,会保存到内存以便使用,同时把消息写到硬盘。但是,消息写入硬盘的过程中,是会阻塞队列的。RabbitMQ虽然针对写入硬盘速度做了很多算法优化,但是在长队列中,依然表现不是很理想,所以就有了懒队列的出现。
懒队列会尝试尽可能早的把消息写到硬盘中。这意味着在正常操作的大多数情况下,RAM中要保存的消息要少得多。当然,这是以增加磁盘IO为代价的。
声明懒队列有两种方式:
1、给队列指定参数
在代码中可以通过x-queue-mode参数指定
点击查看代码
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
2、设定一个策略,在策略中指定queue-mode 为 lazy。
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"default"}' -apply-to queues
要注意的是,当一个队列被声明为懒队列,那即使队列被设定为不持久化,消息依然会写入到硬盘中。并且,在镜像集群中,大量的消息也会被同步到当前节点的镜像节点当中,并写入硬盘。这会给集群资源造成很大的负担。
后一句话总结:懒队列适合消息量大且长期有堆积的队列,可以减少内存使用,加快消费速度。但是这是以大量消耗集群的网络及磁盘IO为代价的。
消息分片存储插件-Sharding Plugin
RabbitMQ提供的Sharding插件,他会将一个队列中的消息分散存储到不同的节点上,并提供多个节点的负载均衡策略实现对等的读与写功能。
安装Sharding插件
在当前RabbitMQ的运行版本中,已经包含了Sharding插件,需要使用插件时,只需要安装启用即可
rabbitmq-plugins enable rabbitmq_sharding
配置Sharding策略
启用完成后,需要配置Sharding的策略。
按照要求,就可以配置一个针对sharding_开头的交换机和队列的策略。
新增带Sharding的Exchange交换机
在创建队列时,可以看到,安装了Sharding插件后,多出了一种队列类型,xmodulus-hash
往分片交换机上发送消息
点击查看代码
public class ShardingProducer {
private static final String EXCHANGE_NAME = "sharding2_exchange";
public static void main(String[] args) throws Exception{
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
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。查看这个交换机的详情,可以看到他的分片情况:
并且,一万条消息被平均分配到了三个队列当中。
Sharding插件带来的x-modulus-hash类型Exchange,会忽略之前的routingkey配置,而将消息以轮询的方式平均分配到Exchange绑定的所有队列上。
消费分片交换机上的消息
现在sharding2_exchange交换机上的消息已经平均分配到了三个碎片队列上。这时如何去消费这些消息呢?你会发现这些碎片队列的名字并不是毫无规律的,他是有一个固定的格式的。都是固定的这种格式:sharding:{exchangename}-{node}-{shardingindex} 。你当然可以针对每个队列去单独声明消费者,这样当然是能够消费到消息的,但是这样,你消费到的消息就是一些零散的消息了,这不符合分片的业务场景要求。
数据分片后,还是希望能够像一个普通队列一样消费到完整的数据副本。这时,Sharding插件提供了一种伪队列的消费方式。你可以声明一个名字为exchangename(就是代码示例中的sharding2_exchange)的伪队列,然后像消费一个普通队列一样去消费这一系列的碎片队列。
为什么说是伪队列?因为名为exchangename的队列实际是不存在的。
点击查看代码
public class ShardingConsumer {
public static final String QUEUENAME="sharding2_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
factory.setPort(5672);
factory.setUsername("admin");
factory.setPassword("admin");
factory.setVirtualHost("/mirror");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUENAME,false,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);
}
};
channel.basicConsume(QUEUENAME, true, myconsumer);
}
}
注意事项
Sharding插件将消息分散存储时,是尽量按照轮询的方式进行。
首先,这些消息在分片的过程中,是没有考虑消息顺序的,这会让RabbitMQ中原本就不是很严谨的消息顺序变得更加雪上加霜。所以,Sharding插件适合于那些对于消息延迟要求不严格,以及对消费顺序没有任何要求的的场景。
然后,Sharding插件消费伪队列的消息时,会从消费者最少的碎片中选择队列。这时,如果你的这些碎片队列中已经有了很多其他的消息,那么再去消费伪队列消息时,就会受到这些不均匀数据的影响。所以,如果使用Sharding插件,这些碎片队列就尽量不要单独使用了(就是不要单独向队列中推消息,尽量用shardingExchange自动去给队列分消息,不要单独操作shardingExchange下的某个队列)。
RabbitMQ如何保证消息不丢失?
哪些环节会有丢消息的可能?
我们考虑一个通用的MQ场景:
其中,1,2,4三个场景都是跨网络的,而跨网络就肯定会有丢消息的可能。
然后关于3这个环节,通常MQ存盘时都会先写入操作系统的缓存page cache 中,然后再由操作系统异步的将消息写入硬盘。这个中间有个时间差,就可能会造成消息丢失。如果服务挂了,缓存中还没有来得及写入硬盘的消息就会丢失。这也是任何用户态的应用程序无法避免的。
对于任何MQ产品,都应该从这四个方面来考虑数据的安全性。那我们看看用RabbitMQ时要如何解决这个问题。
RabbitMQ消息零丢失方案:
生产者保证消息正确发送到RibbitMQ
对于单个数据,可以使用生产者确认机制。通过多次确认的方式,保证生产者的消息能够正确的发送到RabbitMQ中。
RabbitMQ的生产者确认机制分为同步确认和异步确认。同步确认主要是通过在生产者端使用Channel.waitForConfirmsOrDie()指定一个等待确认的完成时间。异步确认机制则是通过channel.addConfirmListener(ConfirmCallback var1, ConfirmCallback var2)在生产者端注入两个回调确认函数。第一个函数是在生产者发送消息时调用,第二个函数则是生产者收到Broker的消息确认请求时调用。两个函数需要通过sequenceNumber自行完成消息的前后对应。sequenceNumber的生成方式需要通过channel的序列获取。int sequenceNumber = channel.getNextPublishSeqNo();
在RabbitMQ中,另外还有一种手动事务的方式,可以保证消息正确发送。手动事务机制主要有几个关键的方法: channel.txSelect() 开启事务;channel.txCommit() 提交事务;channel.txRollback() 回滚事务;用这几个方法来进行事务管理。但是这种方式需要手动控制事务逻辑,并且手动事务会对channel 产生阻塞,造成吞吐量下降
RabbitMQ消息存盘不丢消息
这个在RabbitMQ中比较好处理,对于Classic经典队列,直接将队列声明成为持久化队列即可。而新增的Quorum队列和Stream队列,都是明显的持久化队列,能更好的保证服务端消息不会丢失。
RabbitMQ 主从消息同步时不丢消息
这涉及到RabbitMQ的集群架构。首先他的普通集群模式,消息是分散存储的,不会主动进行消息同步了,是有可能丢失消息的。而镜像模式集群,数据会主动在集群各个节点当中同步,这时丢失消息的概率不会太高。
另外,启用Federation联邦机制,给包含重要消息的队列建立一个远端备份,也是一个不错的选择。
RabbitMQ消费者不丢失消息
RabbitMQ在消费消息时可以指定是自动应答,还是手动应答。如果是自动应答模式,消费者会在完成业务处理后自动进行应答,而如果消费者的业务逻辑抛出异常,RabbitMQ会将消息进行重试,这样是不会丢失消息的,但是有可能会造成消息一直重复消费。
将RabbitMQ的应答模式设定为手动应答可以提高消息消费的可靠性。
点击查看代码
channel.basicConsume(queueName, false, new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
long deliveryTag = envelope.getDeliveryTag();
channel.basicAck(deliveryTag, false);
}
});
channel.basicConsume(queueName, true, myconsumer);
另外这个应答模式在SpringBoot集成案例中,也可以在配置文件中通过属性spring.rabbitmq.listener.simple.acknowledge-mode 进行指定。可以设定为AUTO 自动应答; MANUAL 手动应答;NONE 不应答; 其中这个NONE不应答,就是不启动应答机制,RabbitMQ只管往消费者推送消息后,就不再重复推送消息了,这样效率更高,但是显然会有丢消息的可能。
最后,任何用户态的应用程序都无法保证绝对的数据安全,所以,备份与恢复的方案也需要考虑到。
如何保证消息幂等?
幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
RabbitMQ的自动重试功能:
当消费者消费消息处理业务逻辑时,如果抛出异常,或者不向RabbitMQ返回响应,默认情况下,RabbitMQ会无限次数的重复进行消息消费。
处理幂等问题,首先要设定RabbitMQ的重试次数。在SpringBoot集成RabbitMQ时,可以在配置文件中指定spring.rabbitmq.listener.simple.retry开头的一系列属性,来制定重试策略。
然后,需要在业务上处理幂等问题。
处理幂等问题的关键是要给每个消息一个唯一的标识。
在SpringBoot框架集成RabbitMQ后,可以给每个消息指定一个全局唯一的MessageID,在消费者端针对MessageID做幂等性判断。关键代码:
点击查看代码
//发送者指定ID字段
Message message2 = MessageBuilder.withBody(message.getBytes()).setMessageId(UUID.randomUUID(). toString()).build();
rabbitTemplate.send(message2);
//消费者获取MessageID,自己做幂等性判断
@RabbitListener(queues = "fanout_email_queue")
public void process(Message message) throws Exception {
// 获取消息Id
String messageId = message.getMessageProperties().getMessageId();
}
要注意下这里用的message要是 org.springframework.amqp.core.Message
在原生API当中,也是支持MessageId的。当然,在实际工作中,最好还是能够添加一个具有业务意义的数据作为唯一键会更好,这样能更好的防止重复消费问题对业务的影响。比如,针对订单消息,那就用订单ID来做唯一键。在RabbitMQ 中,消息的头部就是一个很好的携带数据的地方。
点击查看代码
// ==== 发送消息时,携带sequenceNumber和orderNo
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.deliveryMode(MessageProperties.PERSISTENT_TEXT_PLAIN.getDeliveryMod e());
builder.priority(MessageProperties.PERSISTENT_TEXT_PLAIN.getPriority());
//携带消息ID
builder.messageId(""+channel.getNextPublishSeqNo());
Map<String, Object> headers = new HashMap<>();
//携带订单号
headers.put("order", "123");
builder.headers(headers);
channel.basicPublish("", QUEUE_NAME, builder.build(), message.getBytes("UTF-8"));
// ==== 接收消息时,拿到sequenceNumber
Consumer myconsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties, byte[] body)
throws IOException {
//获取消息ID
System.out.println("messageId:"+properties.getMessageId());
//获取订单ID
properties.getHeaders().forEach((key,value)-> System.out.println("key: "+key +"; value: "+value));
// (process the message components here ...)
//消息处理完后,进行答复。答复过的消息,服务器就不会再次转发。
//没有答复过的消息,服务器会一直不停转发。
channel.basicAck(deliveryTag, false);
}
};
channel.basicConsume(QUEUE_NAME, false, myconsumer);
如何保证消息的顺序?
某些场景下,需要保证消息的消费顺序,例如一个下单过程,需要先完成扣款,然后扣减库存,然后通知快递发货,这个顺序不能乱。如果每个步骤都通过消息进行异步通知的话,这一组消息就必须保证他们的消费顺序是一致的。
在RabbitMQ当中,针对消息顺序的设计其实是比较弱的。唯一比较好的策略就是 单队列+单消息推送。即一组有序消息,只发到一个队列中,利用队列的FIFO特性保证消息在队列内顺序不会乱。但是,显然,这是以极度消耗性能作为代价的,在实际适应过程中,应该尽量避免这种场景。
然后在消费者进行消费时,保证只有一个消费者,同时指定prefetch属性为1,即每次RabbitMQ都只往客户端推送一个消息。像这样:
spring.rabbitmq.listener.simple.prefetch=1
而在多队列情况下,如何保证消息的顺序性,目前使用RabbitMQ的话,还没有比较好的解决方案。在使用时,应该尽量避免这种情况。
关于RabbitMQ的数据堆积问题
RabbitMQ一直以来都有一个缺点,就是对于消息堆积问题的处理不好。当RabbitMQ中有大量消息堆积时,整体性能会严重下降。而目前新推出的Quorum 队列以及Stream队列,目的就在于解决这个核心问题。但是这两种队列的稳定性和周边生态都还不够完善,因此,在使用RabbitMQ时,还是要非常注意消息堆积的问题。尽量让消息的消费速度和生产速度保持一致。
而如果确实出现了消息堆积比较严重的场景,就需要从数据流转的各个环节综合考虑,设计适合的解决方案。
首先在消息生产者端:
对于生产者端,最明显的方式自然是降低消息生产的速度。但是,生产者端产生消息的速度通常是跟业务息息相关的,一般情况下不太好直接优化。但是可以选择尽量多采用批量消息的方式,降低IO频率。
然后在RabbitMQ服务端:
从前面的分享中也能看出,RabbitMQ本身其实也在着力于提高服务端的消息堆积能力。对于消息堆积严重的队列,可以预先添加懒加载机制,或者创建Sharding 分片队列,这些措施都有助于优化服务端的消息堆积能力。另外,尝试使用Stream 队列,也能很好的提高服务端的消息堆积能力。
接下来在消息消费者端:
要提升消费速度最直接的方式,就是增加消费者数量了。尤其当消费端的服务出现问题,已经有大量消息堆积时。这时,可以尽量多的申请机器,部署消费端应用,争取在最短的时间内消费掉积压的消息。但是这种方式需要注意对其他组件的性能压力。
对于单个消费者端,可以通过配置提升消费者端的吞吐量。例如
点击查看代码
# 单次推送消息数量
spring.rabbitmq.listener.simple.prefetch=1
# 消费者的消费线程数量
spring.rabbitmq.listener.simple.concurrency=5
灵活配置这几个参数,能够在一定程度上调整每个消费者实例的吞吐量,减少消息堆积数量。
当确实遇到紧急状况,来不及调整消费者端时,可以紧急上线一个消费者组,专门用来将消息快速转录。保存到数据库或者Redis,然后再慢慢进行处理。
RabbitMQ的备份与恢复
RabbitMQ有一个data目录会保存分配到该节点上的所有消息。我们的实验环境中,默认是在/var/lib/rabbitmq/mnesia目录下 这个目录里面的备份分为两个部分,一个是元数据(定义结构的数据),一个是消息存储目录。
对于元数据,可以在Web管理页面通过json文件直接导出或导入。
而对于消息,可以手动进行备份恢复
其实对于消息,由于MQ的特性,是不建议进行备份恢复的。而RabbitMQ如果要进行数据备份恢复,也非常简单。
首先,要保证要恢复的RabbitMQ中已经有了全部的元数据,这个可以通过上一步的json文件来恢复。
然后,备份过程必须要先停止应用。如果是针对镜像集群,还需要把整个集群全部停止。
最后,在RabbitMQ的数据目录中,有按virtual hosts组织的文件夹。你只需要按照虚拟主机,将整个文件夹复制到新的服务中即可。持久化消息和非持久化消息都会一起备份。 我们实验环境的默认目录是/var/lib/rabbitmq/mnesia/rabbit@worker2/msg_stores/vhosts
RabbitMQ的性能监控
关于RabbitMQ的性能监控,在管理控制台中提供了非常丰富的展示。例如在下面这个简单的集群节点图中,就监控了非常多系统的关键资源。
还包括消息的生产消费频率、关键组件使用情况等等非常多的信息,都可以从这个管理控制台上展现出来。但是,对于构建一个自动化的性能监控系统来说,这个管理页面就不太够用了。为此,RabbitMQ也提供了一系列的HTTP接口,通过这些接口可以非常全面的使用并管理RabbitMQ的各种功能。
这些HTTP的接口不需要专门去查手册,在部署的管理控制台页面下方已经集成了详细的文档,我们只需要打开HTTP API的页面就能看到。
比如最常用的 http://[server:port]/api/overview 接口,会列出非常多的信息,包含系统的资源使用情况。通过这个接口,就可以很好的对接Promethus、Grafana等工具,构建更灵活的监控告警体系。
可以看到,这里面的接口相当丰富,不光可以通过GET请求获取各种消息,还可以通过其他类型的HTTP请求来管理RabbitMQ中的各种资源,因此在实际使用时,还需要考虑这些接口的安全性。
搭建HAProxy,实现高可用集群
我们之前搭建的镜像集群,已经具备了集群的功能,请求发送到任何一个节点上,数据都是在集群内共享的。但是,在企业使用时,通常还会选择在集群基础上增加负载均衡的能力。即希望将客户端的请求能够尽量均匀的分配到集群中各个节点上,这样可以让集群的压力得到平衡。 实现负载均衡的方式有很多,HAProxy就是其中一种可选方案。HAProxy是一个免费、快速并且可靠的解决方案,有很多大型互联网公司都在使用。通过HAProxy,应用可以直连一个单一的IP地址,然后HAProxy会将这个IP地址的TCP 请求进行转发,并在转发过程中实现负载均衡。
安装步骤如下:
安装HAProxy
点击查看代码
#安装
yum install haproxy
#检测安装是否成功
haproxy
#查找haproxy.cfg文件的位置
find / ‐name haproxy.cfg
#配置haproxy.cfg文件 后面会列出参考配置
vim /etc/haproxy/haproxy.cfg
#启动haproxy
haproxy ‐f /etc/haproxy/haproxy.cfg
#查看haproxy进程状态
systemctl status haproxy.service
#状态如下说明 已经启动成功 Active: active (running)
#访问如下地址对mq节点进行监控
http://47.114.175.29:1080/haproxy_stats
#代码中访问mq集群地址,则变为访问haproxy地址:5672
配置HAProxy
修改haproxy.cfg文件。下面是参考配置。注意将节点的IP地址和端口换成你自己的环境。
点击查看代码
#‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
# Example configuration for a possible web application. See the
# full configuration options online.
# # http://haproxy.1wt.eu/download/1.4/doc/configuration.txt
#
#‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
#‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
# Global settings
#‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
global
# to have these messages end up in /var/log/haproxy.log you will
# need to:
#16 # 1) configure syslog to accept network log events. This is done
# by adding the '‐r' option to the SYSLOGD_OPTIONS in
# /etc/sysconfig/syslog
#
# 2) configure local2 events to go to the /var/log/haproxy.log
# file. A line like the following can be added to
# /etc/sysconfig/syslog
# # local2.* /var/log/haproxy.log # log 127.0.0.1 local2
chroot /var/lib/haproxy
pidfile /var/run/haproxy.pid
maxconn 4000
user haproxy
group haproxy
daemon
# turn on stats unix socket stats socket /var/lib/haproxy/stats
#‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
# common defaults that all the 'listen' and 'backend' sections will
# use if not designated in their block
#‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐
defaults
mode http
log global
option httplog
option dontlognull
option http‐server‐close
option forwardfor except 127.0.0.0/8
option redispatch
retries 3
timeout http‐request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http‐keep‐alive 10s
timeout check 10s
maxconn 300059
#对MQ集群进行监听
listen rabbitmq_cluster
bind 0.0.0.0:5672
option tcplog
mode tcp
option clitcpka
timeout connect 1s
timeout client 10s
timeout server 10s
balance roundrobin
server node1 worker1:5672 check inter 5s rise 2 fall 3
server node2 worker2:5672 check inter 5s rise 2 fall 3
server node3 worker3:5672 check inter 5s rise 2 fall 3
#开启haproxy监控服务
listen http_front
bind 0.0.0.0:1080
stats refresh 30s
stats uri /haproxy_stats
stats auth admin:admin
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)