前言
在进入本节内容之前,我们先体验下一种情况,假设我手滑了,一不小心把RabbitMq服务器关了,这时候会出现什么样的情况呢,
我们先启动一个生产者并发送一条消息,然后到管理页面看看对应的队列消息的信息
发送消息:
队列消息信息:
我们是可以看到有一个队列以及一条待消费的消息,这时候重启RabiitMqsystemctl restart rabbitmq-server
,然后看看该队列消息情况:
注意:这时候重启之前,要把生产者关掉,否则重启后过一会还是会监听到生产者声明的队列
那这肯定是不对的,你看啊,我们的消息还没有被消费掉,然后他就把我的消息给丢掉了甚至存储消息的容器队列都被嘎了,你说可恨不可恨,因此啊,我们就希望你服务器重启了就重启吧,但是吧,你不能把我的东西给嘎了,所以喽持久化数据就显得非常重要了。
队列持久化
我们修改生产者的代码,很简单,在信道声明队列的时候,有一个durable参数就是用来控制队列是否持久化
public class Task_02 {
private static final String QUEUE_NAME = "durable_queue";
public static void main(String[] args) {
try(Channel channel= RabbitmqUtils.getChannel()) {
boolean durable = true;
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
String message = "队列持久化";
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息完成:"+message);
} catch (IOException | TimeoutException e) {
throw new RuntimeException(e);
}
}
}
注意:这时候,如果已经存在同名的队列并且持久标志被设置为false,你改成true之后重启动,就报鲜红的错误
这是因为RabbitMq中的队列持久属性是在队列声明时确定的,并且在创建后是不可更改的。这意味着一旦队列被声明,就不能直接修改它的属性。服务器中已存在同名队列,并且持久化属性被设置了false,现在你要想去修改这个队列的持久化属性就必须删除掉原有的队列
启动生产者并发送消息
可以看到我们的队列啊,他多了一个天蓝色的标志D,这个D就是代表了该队列已被设置为持久化,这样子的话,当服务器重启后,该队列则不会被嘎掉了
可是啊,我们仔细地看,队列是没被嘎掉,但是我的消息被嘎掉了啊!!!
消息持久化
通过以上操作,我们可以知道,队列持久化了,并不能代表消息就被持久化,还是会丢失
设置消息的持久化也是很简单的,通过消息属性就可以设置。
PERSISTENT_TEXT_PLAIN
是一种 MessageProperties
对象,用于在 RabbitMq 中表示持久化的文本消息。
在RabbitMq中,当需要发送一个持久化的文本消息时,可以使用 PERSISTENT_TEXT_PLAIN
对象作为消息的属性。这个对象通常用于设置 BasicProperties
,以便将消息标记为持久化的。
在上述队列持久化代码中,我们只需要修改一行即可:
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
我们看下管理页面中消息内容信息,其中delivery_mode
(整数):表示消息的投递模式,可选值为 1(非持久化)和 2(持久化)。
不公平分发
上面我们已经将队列以及消息进行了持久化,下面我们就看看RabbitMq是如何分发消息的。
刚开始的时候,我们是一个消息一个消息发送给消费者,有几个消费者就均匀的一个一个发送,知道消息分发完毕,这种分发方式为轮训分发。但是这种并不推荐,假设我们只有两个消费者,其中一个消费者A处理消息的能力很快,几乎一两秒一个,而另一个B处理的很慢十几秒一个,这样子的话,我有20条消息,向两个消费者轮询分发,你一个我一个…发完后,在不考虑其他情况下,每一个消费者都有10个消息处理,那么消费者A一秒一个,10秒后就结束了,而消费者B30秒一个,就需要5分钟才能结束,这样子看,消费者A咋那么清闲,B咋那么忙,这时候再来十几个任务,消费者A又很快处理完事,B有积累了很多事情没做,你要是老板,你会这么采用这种分配方式吗?但是我们RabbitMq它不知啊,它只知道我要雨露均沾,任何一个消费者都不能被失宠,嘿嘿,多公平
生产者代码
public class PollingDistribution {
private static final String QUEUE_NAME = "fair_dispatch_queue";
public static void main(String[] args) {
try(Channel channel= RabbitmqUtils.getChannel()) {
boolean durable = true;
channel.queueDeclare(QUEUE_NAME,durable,false,false,null);
for (int i = 0; i < 20; i++) {
String message = "轮询分发消息---" + (i + 1);
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());
}
System.out.println("20条消息轮询分发完成!!!");
} catch (IOException | TimeoutException e) {
throw new RuntimeException(e);
}
}
}
消费者代码,这里我们开两个消费者
public class Consumer_01 {
private static final String QUEUE_NAME = "fair_dispatch_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitmqUtils.getChannel();
// 声明 接受消息
DeliverCallback deliverCallback = (String var1, Delivery var2) -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.err.println("Consumer_01 接收到消息:" + new String(var2.getBody()));
//true 代表批量应答 channel 上未应答的消息 false 单条应答
boolean multiple = false;
channel.basicAck(var2.getEnvelope().getDeliveryTag(),multiple);
};
// 取消消息时的回调
CancelCallback cancelCallback = consumerTag -> {
System.err.println(consumerTag+"消费者取消消费接口回调逻辑");
};
//不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
System.out.println("Consumer_01 消费者启动等待消费.................. ");
channel.basicConsume(QUEUE_NAME,false,deliverCallback,cancelCallback);
}
}
public class Consumer_02 {
private static final String QUEUE_NAME = "fair_dispatch_queue";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitmqUtils.getChannel();
// 声明 接受消息
DeliverCallback deliverCallback = (String var1, Delivery var2) -> {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.err.println("Consumer_02 接收到消息:" + new String(var2.getBody()));
//true 代表批量应答 channel 上未应答的消息 false 单条应答
boolean multiple = false;
channel.basicAck(var2.getEnvelope().getDeliveryTag(),multiple);
};
// 取消消息时的回调
CancelCallback cancelCallback = consumerTag -> {
System.err.println(consumerTag+"消费者取消消费接口回调逻辑");
};
//不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
System.out.println("Consumer_02 消费者启动等待消费.................. ");
channel.basicConsume(QUEUE_NAME,false,deliverCallback,cancelCallback);
}
}
然后先启动消费者,在启动生产者,如果报队列不存在啥的,请先手动添加个队列,然后就会看到下图情况:消费者1已经处理完了,消费者2才执行一两个
那么在消费者2处理剩下的消息这段时间内,消费者1明显就处于空闲状态,秉着压榨劳动力原则,不能让这种情况出现。为了避免这种情况,消费者可以通过信道的basicQos方法,设置参数prefetchCount=1,告诉RabbitMQ一次不要给一个我发送多条消息。或者,换句话说,消费者在未处理完某一个消息或者并未确认之前,不要向消费者发送新消息。然后 rabbitmq 就会把消息发送给下一个还不忙的消费者。当然如果所有的消费者都还没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到撑满的情况,这个时候就只能添加 新的 worker 或者改变其他存储任务的策略。
修改消费者的代码,添加以下两行即可
//不公平分发
int prefetchCount = 1;
channel.basicQos(prefetchCount);
然后启动消费者2,启动生产者重新分发消息:
很明显,消费者1速度快处理了7条消息,消费者2速度慢只处理了3条
预取值
前面我们学习了消息的不公平分发,是要设置信道的basicQos中的参数prefetchCount=1,那么这个prefetchCount为什么设置为1就可以实现不公平分发,如果我要设置5/6/7/8等等的数字呢?又代表了什么意思?
我们先忘记掉不公平分发,假设我没有设置这个值,那么生产者发送多少消息,消费者就会接受多少消息,如果处理的快也就罢了,但是处理的慢,跟不上生产者发送的速度,这时候,队列中大量未处理的消息就会积压在该消费者的缓冲区中,不仅要花费大量的时间来处理消息,也会使得最新的消息无法及时的处理。同样的,其他处理快的消费者相对来说就显得很悠闲。
因此,我么就需要控制消费者从队列中接收消息的速率和数量,防止消费者被过量的消息淹没。
而这个控制的手段就是设置Qos。
Qos(Quality of Service,服务质量)概念
QoS(Quality of Service)是一种网络通信中用于描述服务质量的机制。它指定了在不同网络条件下,数据传输的可靠性、延迟、带宽、抖动等方面的保证程度。在RabbitMQ消息队列系统中,QoS 是一种用于控制消息传递和处理方式的机制,以确保系统的可靠性和性能。它主要用于消费者端,用于控制消费者从队列中接收消息的速率和数量,防止消费者被过量的消息淹没。
为什么要设置Qos
设置预取值(prefetch value)的目的是在消费者端控制消费消息的速率和数量,以优化系统的性能和资源利用率。
以下是设置预取值的主要原因:
-
避免消费者被淹没:当队列中有大量未处理的消息时,如果消费者不限制同时接收的消息数量,就可能会导致消费者被瞬间大量的消息所淹没。这可能会导致消费者处理过慢,对系统的性能产生负面影响。通过设置适当的预取值,可以确保消费者一次只接收到有限数量的消息,从而平衡处理能力。
-
均衡消息分发:在使用消息队列的情况下,通常会有多个消费者同时从同一个队列中接收消息。如果不限制预取数量,会导致某些消费者接收到更多的消息,而其他消费者相对较少。通过设置适当的预取值,可以更均衡地分发消息给每个消费者,提高整体系统的吞吐量。
-
提高资源利用率:在一些场景中,消费者的处理速度可能相对较慢,而发布者的速度可能相对较快。如果不限制预取值,消费者的缓冲区可能会塞满大量未处理的消息,从而占用大量的系统资源。通过设置合理的预取值,可以控制缓冲区的大小,避免不必要的资源浪费。
如何设置Qos
设置Qos有两种方式:
- Prefetch Count(预取计数):这个设置用于指定消费者在进行消息处理之前可以接收的最大未确认消息数量。通过设置适当的预取计数,可以控制同时处理的消息数量,防止消费者被过多的未确认消息淹没。
- Prefetch Size(预取大小):这个设置用于指定消费者在进行消息处理之前可以接收的最大未确认消息的大小。通常在限制网络带宽或者需要严格控制消息大小时使用。
代码实现,也就和不公平分发一样设置basicQos值,只不过这里的prefetchCount不设置为1,假设我们设置速度快的预取值为6,速度慢的预取值为3:
根据消费者设置的预取值,分发对应的消息数量,一旦满了,就会停止分发,转而想起他空闲的消费者发送
消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处理的消息的数量也会增加,从而增加了消费者的RAM 消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。
Qos的取值问题
在传输效率和消费者消费速度之间做一个平衡。这个值是需要不断尝试的,因为太低,信道传输消息效率太低,如果太高,消费者来不及确认消息导致消息积累问题,内存消耗不断增大