一、RabbitMQ高级
1. 过期时间TTL
过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。
目前有两种方法可以设置:
- 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
- 第二种方法是对消息进行单独设置,每条消息TTL可以不同。
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。
1.1. 设置队列TTL
在 spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--定义过期队列及其属性,不存在则自动创建-->
<rabbit:queue id="my_ttl_queue" name="my_ttl_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投递到该队列的消息如果没有消费都将在6秒之后被删除-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
</rabbit:queue-arguments>
</rabbit:queue>
</beans>
然后在测试类 spring-rabbitmq-producer\src\test\java\top\onefine\rabbitmq\ProducerTest.java
中编写如下方法发送消息到上述定义的队列:
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 过期队列消息
* 投递到该队列的消息如果没有消费都将在6秒之后被删除
*/
@Test
public void ttlQueueTest() {
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", "发送到过期队列my_ttl_queue,6秒内不消费则不能再被消费。");
}
/**
* 过期消息
* 该消息投递任何交换机或队列中的时候;如果到了过期时间则将从该队列中删除
*/
@Test
public void ttlMessageTest() {
MessageProperties messageProperties = new MessageProperties();
//设置消息的过期时间,5秒
messageProperties.setExpiration("5000");
Message message = new Message("测试过期消息,5秒钟过期".getBytes(), messageProperties);
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", message);
}
}
参数 x-message-ttl 的值 必须是非负 32 位整数 (0 <= n <= 2^32-1) ,以毫秒为单位表示 TTL 的值。这样,值 6000 表示存在于 队列 中的当前 消息 将最多只存活 6 秒钟。
如果不设置TTL,则表示此消息不会过期。如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃。
1.2. 设置消息TTL
消息的过期时间;只需要在发送消息(可以发送到任何队列,不管该队列是否属于某个交换机)的时候设置过期时间即可。在测试类中编写如下方法发送消息并设置过期时间到队列:
/**
* 过期消息
* 该消息投递任何交换机或队列中的时候;如果到了过期时间则将从该队列中删除
*/
@Test
public void ttlMessageTest(){
MessageProperties messageProperties = new MessageProperties();
//设置消息的过期时间,5秒
messageProperties.setExpiration("5000");
Message message = new Message("测试过期消息,5秒钟过期".getBytes(), messageProperties);
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", message);
}
expiration 字段以微秒为单位表示 TTL 值。且与 x-message-ttl 具有相同的约束条件。因为 expiration 字段必须为字符串类型,broker 将只会接受以字符串形式表达的数字。
当同时指定了 queue 和 message 的 TTL 值,则两者中较小的那个才会起作用。
注:项目中使用:@ImportResource("classpath:/spring/spring-rabbitmq.xml")
导入配置文件使rabbitmq配置生效
package top.onefine.rabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
/**
* 生产者的启动类
*/
@SpringBootApplication
@ImportResource("classpath:/spring/spring-rabbitmq.xml") // 导入配置文件使rabbitmq配置生效
public class ProducerApplication {
public static void main(String[] args) {
SpringApplication.run(ProducerApplication.class, args);
}
}
2. 死信队列
DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。
消息变成死信,可能是由于以下的原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange
指定交换机即可。
具体步骤如下面的章节。
2.1. 定义死信交换机
在 spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下内容:
<!--定义定向交换机中的持久化死信队列,不存在则自动创建-->
<rabbit:queue id="my_dlx_queue" name="my_dlx_queue" auto-declare="true"/>
<!--定义广播类型交换机;并绑定上述两个队列-->
<rabbit:direct-exchange id="my_dlx_exchange" name="my_dlx_exchange" auto-declare="true">
<rabbit:bindings>
<!--绑定路由键my_ttl_dlx、my_max_dlx,可以将过期的消息转移到my_dlx_queue队列-->
<rabbit:binding key="my_ttl_dlx" queue="my_dlx_queue"/>
<rabbit:binding key="my_max_dlx" queue="my_dlx_queue"/>
</rabbit:bindings>
</rabbit:direct-exchange>
2.2. 队列设置死信交换机
为了测试消息在过期、队列达到最大长度后都将被投递死信交换机上;所以添加配置如下:
在 spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下内容:
<!--定义过期队列及其属性,不存在则自动创建-->
<rabbit:queue id="my_ttl_dlx_queue" name="my_ttl_dlx_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投递到该队列的消息如果没有消费都将在6秒之后被投递到死信交换机-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
<!--设置当消息过期后投递到对应的死信交换机-->
<entry key="x-dead-letter-exchange" value="my_dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--定义限制长度的队列及其属性,不存在则自动创建-->
<rabbit:queue id="my_max_dlx_queue" name="my_max_dlx_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投递到该队列的消息最多2个消息,如果超过则最早的消息被删除投递到死信交换机-->
<entry key="x-max-length" value-type="long" value="2"/>
<!--设置当消息过期后投递到对应的死信交换机-->
<entry key="x-dead-letter-exchange" value="my_dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--定义定向交换机 根据不同的路由key投递消息-->
<rabbit:direct-exchange id="my_normal_exchange" name="my_normal_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding key="my_ttl_dlx" queue="my_ttl_dlx_queue"/>
<rabbit:binding key="my_max_dlx" queue="my_max_dlx_queue"/>
</rabbit:bindings>
</rabbit:direct-exchange>
2.3. 消息过期的死信队列测试
1)发送消息代码
添加 spring-rabbitmq-producer\src\test\java\top\onefine\rabbitmq\ProducerTest.java
方法
/**
* 过期消息投递到死信队列
* 投递到一个正常的队列,但是该队列有设置过期时间,到过期时间之后消息会被投递到死信交换机(队列)
*/
@Test
public void dlxTTLMessageTest(){
rabbitTemplate.convertAndSend("my_normal_exchange", "my_ttl_dlx", "测试过期消息;6秒过期后会被投递到死信交换机");
}
2)在rabbitMQ管理界面中结果
未过期:
略,自个写代码观察去。
过期后:
略,自个写代码观察去。
3)流程
具体因为队列消息过期而被投递到死信队列的流程:
2.4. 消息过长的死信队列测试
1)发送消息代码
添加 spring-rabbitmq-producer\src\test\java\top\onefine\rabbitmq\ProducerTest.java
方法
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 过期消息投递到死信队列
* 投递到一个正常的队列,但是该队列有设置过期时间,到过期时间之后消息会被投递到死信交换机(队列)
*/
@Test
public void dlxTTLMessageTest() {
rabbitTemplate.convertAndSend("my_normal_exchange", "my_ttl_dlx", "测试过期消息;6秒过期后会被投递到死信交换机");
}
/**
* 消息长度超过2,会投递到死信队列中
* 投递到一个正常的队列,但是该队列有设置最大消息数,到最大消息数之后队列中最早的消息会被投递到死信交换机(队列)
*/
@Test
public void dlxMaxMessageTest() {
for (int i = 1; i <= 3; i++) {
rabbitTemplate.convertAndSend("my_normal_exchange",
"my_max_dlx",
"发送消息" + i + ";消息长度超过2最早的消息会被投递到死信队列中");
}
}
}
2)在rabbitMQ管理界面中结果
上面发送的3条消息中的第1条消息会被投递到死信队列中(如果启动了消费者,那么队列消息很快会被取走消费掉);
3)消费者接收死信队列消息
与过期消息投递到死信队列的代码和配置是共用的,并不需要重新编写。
4)流程
消息超过队列最大消息长度而被投递到死信队列的流程在前面的图中已包含。
3. 延迟队列
延迟队列存储的对象是对应的延迟消息;所谓“延迟消息” 是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
在RabbitMQ中延迟队列可以通过 过期时间
+ 死信队列
来实现;具体如下流程图所示:
在上图中;分别设置了两个5秒、10秒的过期队列,然后等到时间到了则会自动将这些消息转移投递到对应的死信队列中,然后消费者再从这些死信队列接收消息就可以实现消息的延迟接收。
延迟队列的应用场景;如:
- 在电商项目中的支付场景;如果在用户下单之后的几十分钟内没有支付成功;那么这个支付的订单算是支付失败,要进行支付失败的异常处理(将库存加回去),这时候可以通过使用延迟队列来处理
- 在系统中如有需要在指定的某个时间之后执行的任务都可以通过延迟队列处理
4. 消息确认机制
确认并且保证消息被送达,提供了两种方式:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务。
4.1 发布确认
有两种方式:消息发送成功确认和消息发送失败回调。
- 消息发送成功确认
在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
connectionFactory 中启用消息确认:
<!-- publisher-confirms="true" 表示:启用了消息确认 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-confirms="true" />
配置消息确认回调方法如下:
<!-- 消息回调处理类 -->
<bean id="confirmCallback" class="top.onefine.rabbitmq.MsgSendConfirmCallBack"/>
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<!-- confirm-callback="confirmCallback" 表示:消息失败回调 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
confirm-callback="confirmCallback"/>
消息确认回调方法com.itheima.rabbitmq.MsgSendConfirmCallBack如下:
package top.onefine.rabbitmq;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* @author one fine<br/>
*
* 消息确认与回退
*/
public class MsgSendConfirmCallBack implements RabbitTemplate.ConfirmCallback {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息确认成功....");
} else {
//处理丢失的消息
System.out.println("消息确认失败," + cause);
}
}
}
功能测试如下:
发送消息
com.itheima.rabbitmq.ProducerTest#queueTest
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 消息确认
*/
@Test
public void queueTest(){
//路由键与队列同名
rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息。");
}
}
管理界面确认消息发送成功
消息确认回调
- 消息发送失败回调
在spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
connectionFactory 中启用回调:
<!-- publisher-returns="true" 表示:启用了失败回调 -->
<rabbit:connection-factory id="connectionFactory" host="${rabbitmq.host}"
port="${rabbitmq.port}"
username="${rabbitmq.username}"
password="${rabbitmq.password}"
virtual-host="${rabbitmq.virtual-host}"
publisher-returns="true" />
配置消息失败回调方法如下:
注意:同时需配置mandatory=“true”,否则消息则丢失
<!-- 消息失败回调类 -->
<bean id="sendReturnCallback" class="top.onefine.rabbitmq.MsgSendReturnCallback"/>
<!-- return-callback="sendReturnCallback" 表示:消息失败回调 ,同时需配置mandatory="true",否则消息则丢失-->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
confirm-callback="confirmCallback" return-callback="sendReturnCallback"
mandatory="true"/>
消息失败回调方法com.itheima.rabbitmq.MsgSendReturnCallback如下:
package top.onefine.rabbitmq;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
/**
* @author one fine<br/>
*/
public class MsgSendReturnCallback implements RabbitTemplate.ReturnCallback {
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
String msgJson = new String(message.getBody());
System.out.println("Returned Message:" + msgJson);
}
}
功能测试如下:
模拟消息发送失败
com.itheima.rabbitmq.ProducerTest#testFailQueueTest
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 由于queue错误,所以会确认失败
*/
@Test
public void testFailQueueTest() {
//exchange 正确,queue 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
rabbitTemplate.convertAndSend("test_fail_exchange", "", "测试消息发送失败进行确认应答。");
}
}
失败回调结果如下:
完整的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- publisher-confirms="true" 表示:启用了消息确认 -->
<rabbit:connection-factory id="connectionFactory" host="127.0.0.1"
port="5672"
username="guest"
password="123456"
virtual-host="/"
publisher-confirms="true"
publisher-returns="true"/>
<!-- 消息回调处理类 -->
<bean id="confirmCallback" class="top.onefine.rabbitmq.MsgSendConfirmCallBack"/>
<!-- 消息失败回调类 -->
<bean id="sendReturnCallback" class="top.onefine.rabbitmq.MsgSendReturnCallback"/>
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<!-- confirm-callback="confirmCallback" 表示:消息失败回调 -->
<!-- return-callback="sendReturnCallback" 表示:消息失败回调 ,同时需配置mandatory="true",否则消息则丢失-->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
confirm-callback="confirmCallback"
return-callback="sendReturnCallback"
mandatory="true"/>
</beans>
4.2 事务支持
场景:业务处理伴随消息的发送,业务处理失败(事务回滚)后要求消息不发送。rabbitmq 使用调用者的外部事务,通常是首选,因为它是非侵入性的(低耦合)。
外部事务的配置:spring-rabbitmq-producer\src\main\resources\spring\spring-rabbitmq.xml
<!-- channel-transacted="true" 表示:支持事务操作 -->
<rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"
confirm-callback="confirmCallback" return-callback="sendReturnCallback"
channel-transacted="true" />
<!--平台事务管理器-->
<bean id="transactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
- 模拟业务处理失败的场景:
测试类或者测试方法上加入@Transactional注解
@Transactional
public class ProducerTest
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
@Transactional // 开启事务
@SuppressWarnings("All")
// @Rollback(value = false) // spring中的测试方法执行后会自动回滚,所以在这里需要手动指定回滚策略:关闭回滚操作
public void queueTest2(){
//路由键与队列同名
rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--01。");
System.out.println("----------------dosoming:可以是数据库的操作,也可以是其他业务类型的操作---------------");
//模拟业务处理失败
System.out.println(1/0);
rabbitTemplate.convertAndSend("spring_queue", "只发队列spring_queue的消息--02。");
}
}
完整的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!-- publisher-confirms="true" 表示:启用了消息确认 -->
<rabbit:connection-factory id="connectionFactory" host="127.0.0.1"
port="5672"
username="guest"
password="123456"
virtual-host="/"/>
<!-- 事务和确认机制不能共存 -->
<!-- publisher-confirms="true"-->
<!-- publisher-returns="true"/>-->
<!--定义rabbitTemplate对象操作可以在代码中方便发送消息-->
<!-- channel-transacted="true" 表示:支持事务操作 -->
<rabbit:template id="rabbitTemplate"
connection-factory="connectionFactory"
mandatory="true"
channel-transacted="true"
/>
<!--平台事务管理器-->
<bean id="transactionManager" class="org.springframework.amqp.rabbit.transaction.RabbitTransactionManager">
<property name="connectionFactory" ref="connectionFactory"/>
</bean>
</beans>
5. 消息追踪
消息中心的消息追踪需要使用Trace实现,Trace是Rabbitmq用于记录每一次发送的消息,方便使用Rabbitmq的开发者调试、排错。可通过插件形式提供可视化界面。Trace启动后会自动创建系统Exchange:amq.rabbitmq.trace ,每个队列会自动绑定该Exchange,绑定后发送到队列的消息都会记录到Trace日志。
5.1 消息追踪启用与查看
以下是trace的相关命令和使用(要使用需要先rabbitmq启用插件,再打开开关才能使用):
命令集 | 描述 |
---|---|
rabbitmq-plugins list | 查看插件列表 |
rabbitmq-plugins enable rabbitmq_tracing | rabbitmq启用trace插件 |
rabbitmqctl trace_on | 打开trace的开关 |
rabbitmqctl trace_on -p one | 打开trace的开关(one为需要日志追踪的vhost) |
rabbitmqctl trace_off | 关闭trace的开关 |
rabbitmq-plugins disable rabbitmq_tracing | rabbitmq关闭Trace插件 |
rabbitmqctl set_user_tags onefine administrator | 只有administrator的角色才能查看日志界面 |
[root@onefine ~]# rabbitmq-plugins list
# ...
[root@onefine ~]# rabbitmq-plugins enable rabbitmq_tracing
# ...
[root@onefine ~]# rabbitmqctl trace_on
Starting tracing for vhost "/"
[root@onefine ~]# rabbitmqctl trace_on -p one
Starting tracing for vhost "one"
[root@onefine ~]#
安装插件并开启 trace_on 之后,会发现多个 exchange:amq.rabbitmq.trace ,类型为:topic。
5.2 日志追踪
第一步:发送消息
package top.onefine.rabbitmq;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author one fine<br/>
*/
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void queueTest2(){
//路由键与队列同名
rabbitTemplate.convertAndSend("spring_queue", "消息追踪...");
}
}
发送成功,web查看多了一条消息
第二步:查看trace
第三步:点击Tracing查看Trace log files
第四步:点击itcast-trace.log确认消息轨迹正确性
url:http://127.0.0.1:15672/api/trace-files/itcast-trace.log
浏览器截图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HbVKT8M-1592451812752)(assets/1572257077651.png)]
更新中…
二、RabbitMQ集群
RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。
在实际使用过程中多采取多机多实例部署方式,为了便于同学们练习搭建,有时候你不得不在一台机器上去搭建一个rabbitmq集群,本章主要针对单机多实例这种方式来进行开展。
主要参考官方文档:https://www.rabbitmq.com/clustering.html
1. 集群搭建
1.1. 准备工作
配置的前提是你的rabbitmq可以运行起来,比如"ps aux|grep rabbitmq"你能看到相关进程,又比如运行“rabbitmqctl status”你可以看到类似如下信息,而不报错:
执行"ps aux|grep rabbitmq"结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZLNrh5S2-1592451875269)(assets/1565664545643.png)]
执行“rabbitmqctl status”结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QwNnjwXQ-1592451875270)(assets/1565664577505.png)]
注意:确保RabbitMQ可以运行的,确保完成之后,把RabbitMQ停止,后台看不到RabbitMQ的进程
搭建之前一定要把后台的RabbitMQ的进程停止
1.2. 单机多实例搭建
目标:完成单机多实例的搭建
情景:假设有两个rabbitmq节点,分别为rabbit-1, rabbit-2,rabbit-1作为主节点,rabbit-2作为从节点。
启动命令:RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server -detached
结束命令:rabbitmqctl -n rabbit-1 stop
集群启动
第一步:启动第一个节点rabbit-1,命令如下:
sudo RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server start &
执行结果如下:0
itcast@Server-node:/$ sudo RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit-1 rabbitmq-server start
...............省略...................
########## Logs: /var/log/rabbitmq/rabbit-1.log
###### ## /var/log/rabbitmq/rabbit-1-sasl.log
##########
Starting broker...
completed with 7 plugins.
至此节点rabbit-1启动完成。
第二步:启动第二个节点rabbit-2,命令如下:
itcast@Server-node:/$ sudo RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]" RABBITMQ_NODENAME=rabbit-2 rabbitmq-server start &
注意:web管理插件端口占用,所以还要指定其web插件占用的端口号
RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]"
执行结果如下:
itcast@Server-node:/$ sudo RABBITMQ_NODE_PORT=5673 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,1567
3}]" RABBITMQ_NODENAME=rabbit-2 rabbitmq-server start
..............省略..................
########## Logs: /var/log/rabbitmq/rabbit-2.log
###### ## /var/log/rabbitmq/rabbit-2-sasl.log
##########
Starting broker...
completed with 7 plugins.
至此节点rabbit-2启动完成。
第三步:验证启动 “ps aux|grep rabbitmq”
rabbitmq 2022 2.7 0.4 5349380 77020 ? Sl 11:03 0:06 /usr/lib/erlang/erts-9.2/bin/beam.smp -W w -A 128 -P 1048576 -t 5000000 -stbt db -zdbbl 128000 -K true -B i -- -root /usr/lib/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/ebin -noshell -noinput -s rabbit boot -sname rabbit-1 -boot start_sasl -kernel inet_default_connect_options [{nodelay,true}] -rabbit tcp_listeners [{"auto",5672}] -sasl errlog_type error -sasl sasl_error_logger false -rabbit error_logger {file,"/var/log/rabbitmq/rabbit-1.log"} -rabbit sasl_error_logger {file,"/var/log/rabbitmq/rabbit-1-sasl.log"} -rabbit enabled_plugins_file "/etc/rabbitmq/enabled_plugins" -rabbit plugins_dir "/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/plugins" -rabbit plugins_expand_dir "/var/lib/rabbitmq/mnesia/rabbit-1-plugins-expand" -os_mon start_cpu_sup false -os_mon start_disksup false -os_mon start_memsup false -mnesia dir "/var/lib/rabbitmq/mnesia/rabbit-1" -kernel inet_dist_listen_min 25672 -kernel inet_dist_listen_max 25672 start
rabbitmq 2402 4.2 0.4 5352196 77196 ? Sl 11:05 0:05 /usr/lib/erlang/erts-9.2/bin/beam.smp -W w -A 128 -P 1048576 -t 5000000 -stbt db -zdbbl 128000 -K true -B i -- -root /usr/lib/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/ebin -noshell -noinput -s rabbit boot -sname rabbit-2 -boot start_sasl -kernel inet_default_connect_options [{nodelay,true}] -rabbit tcp_listeners [{"auto",5673}] -sasl errlog_type error -sasl sasl_error_logger false -rabbit error_logger {file,"/var/log/rabbitmq/rabbit-2.log"} -rabbit sasl_error_logger {file,"/var/log/rabbitmq/rabbit-2-sasl.log"} -rabbit enabled_plugins_file "/etc/rabbitmq/enabled_plugins" -rabbit plugins_dir "/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.6.15/plugins" -rabbit plugins_expand_dir "/var/lib/rabbitmq/mnesia/rabbit-2-plugins-expand" -os_mon start_cpu_sup false -os_mon start_disksup false -os_mon start_memsup false -mnesia dir "/var/lib/rabbitmq/mnesia/rabbit-2" -rabbitmq_management listener [{port,15673}] -kernel inet_dist_listen_min 25673 -kernel inet_dist_listen_max 25673 start
第四步:rabbit-1操作作为主节点,命令集如下:
//停止应用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-1 stop_app
Stopping rabbit application on node 'rabbit-1@Server-node'
//目的是清除节点上的历史数据(如果不清除,无法将节点加入到集群)
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-1 reset
Resetting node 'rabbit-1@Server-node'
//启动应用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-1 start_app
Starting node 'rabbit-1@Server-node'
第五步:rabbit2操作为从节点,命令集如下:
//停止应用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 stop_app
Stopping rabbit application on node 'rabbit-2@Server-node'
//目的是清除节点上的历史数据(如果不清除,无法将节点加入到集群)
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 reset
Resetting node 'rabbit-2@Server-node'
//将rabbit2节点加入到rabbit1(主节点)集群当中【Server-node服务器的主机名】
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 join_cluster rabbit-1@'Server-node'
Clustering node 'rabbit-2@Server-node' with 'rabbit-1@Server-node'
//启动应用
itcast@Server-node:/$ sudo rabbitmqctl -n rabbit-2 start_app
Starting node 'rabbit-2@Server-node'
第六步:验证集群状态
itcast@Server-node:/$ sudo rabbitmqctl cluster_status -n rabbit-1
Cluster status of node 'rabbit-1@Server-node'
//集群有两个节点:rabbit-1@Server-node、rabbit-2@Server-node
[{nodes,[{disc,['rabbit-1@Server-node','rabbit-2@Server-node']}]},
{running_nodes,['rabbit-2@Server-node','rabbit-1@Server-node']},
{cluster_name,<<"[email protected]">>},
{partitions,[]},
{alarms,[{'rabbit-2@Server-node',[]},{'rabbit-1@Server-node',[]}]}]
第七步:Web监控
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e3y5FmcD-1592451875273)(assets/1565666186483.png)]
总结:至此单机多实例集群搭建完成
Tips:
如果采用多机部署方式,需读取其中一个节点的cookie, 并复制到其他节点(节点之间通过cookie确定相互是否可通信)。cookie存放在/var/lib/rabbitmq/.erlang.cookie。
例如:主机名分别为rabbit-1、rabbit-2
1、逐个启动各节点
2、配置各节点的hosts文件( vim /etc/hosts)
ip1:rabbit-1
ip2:rabbit-2
其它步骤雷同单机部署方式
2. 集群监控
在广大的互联网行业中RabbitMQ几乎都会有集群,那么对于集群的监控就成了企业生态中必不可少的一环。接下来我们来将讲解主要的4种监控。
2.1. 管理界面监控
管理界面监控需要我们开启对应的插件(rabbitmq-plugins enable rabbitmq_management)
然后访问http://ip:15672
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6F24drDA-1592451875273)(assets/1572492635332.png)]
在管理控制台我们就可以直观的看到集群中的每一个节点是否正常,如果为红色则表示节点挂掉了,同时可以很方便的查看到各个节点的内存、磁盘等相关的信息,使用起来也是非常方便的。但是遗憾的该功能做的比较简陋,没有告警等一些列的个性化设置,同时如果想把他接入到公司其他的监控系统统一管理也是很难做到的,所以扩展性不强,一般在小型企业的小集群中使用。
2.2. tracing日志监控
对于企业级的应用开发来讲,我们通常都会比较关注我们的消息,甚至很多的场景把消息的可靠性放在第一位,但是我们的MQ集群难免会出现消息异常丢失或者客户端无法发送消息等异常情况,此时为了帮助开发人员快速的定位问题,我们就可以对消息的投递和消费过程进行监控,而tracing日志监控插件帮我们很好的实现了该功能,具体的实现参见7.5章节
2.3. 定制自己的监控系统
RabbitMQ提供了很丰富的restful风格的api接口,我们可以通过这些接口得到对应的集群数据,此时我们就可以定制我们的监控系统。
HTTP API URL | HTTP 请求类型 | 接口含义 |
---|---|---|
/api/connections | GET | 获取当前RabbitMQ集群下所有打开的连接 |
/api/nodes | GET | 获取当前RabbitMQ集群下所有节点实例的状态信息 |
/api/vhosts/{vhost}/connections | GET | 获取某一个虚拟机主机下的所有打开的connection连接 |
/api/connections/{name}/channels | GET | 获取某一个连接下所有的管道信息 |
/api/vhosts/{vhost}/channels | GET | 获取某一个虚拟机主机下的管道信息 |
/api/consumers/{vhost} | GET | 获取某一个虚拟机主机下的所有消费者信息 |
/api/exchanges/{vhost} | GET | 获取某一个虚拟机主机下面的所有交换器信息 |
/api/queues/{vhost} | GET | 获取某一个虚拟机主机下的所有队列信息 |
/api/users | GET | 获取集群中所有的用户信息 |
/api/users/{name} | GET/PUT/DELETE | 获取/更新/删除指定用户信息 |
/api/users/{user}/permissions | GET | 获取当前指定用户的所有权限信息 |
/api/permissions/{vhost}/{user} | GET/PUT/DELETE | 获取/更新/删除指定虚拟主机下特定用户的权限 |
/api/exchanges/{vhost}/{name}/publish | POST | 在指定的虚拟机主机和交换器上发布一个消息 |
/api/queues/{vhost}/{name}/get | POST | 在指定虚拟机主机和队列名中获取消息,同时该动作会修改队列状态 |
/api/healthchecks/node/{node} | GET | 获取指定节点的健康检查状态 |
更多API的相关信息和描述可以访问http://ip:15672/api/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-78WHw6EA-1592451875273)(assets/1572491850973.png)]
接下来我们使用RabbitMQ Http API接口来获取集群监控数据
- HttpClient以及Jackson的相关Jar
-
创建MonitorRabbitMQ类实现具体的代码
package com.itheima.rabbitmq;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.http.HttpEntity;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* RabbitMQ的监控
*/
public class MonitorRabbitMQ {
//RabbitMQ的HTTP API——获取集群各个实例的状态信息,ip替换为自己部署相应实例的
private static String RABBIT_NODES_STATUS_REST_URL = "http://192.168.13.111:15672/api/nodes";
//RabbitMQ的HTTP API——获取集群用户信息,ip替换为自己部署相应实例的
private static String RABBIT_USERS_REST_URL = "http://192.168.13.111:15672/api/users";
//rabbitmq的用户名
private static String RABBIT_USER_NAME = "guest";
//rabbitmq的密码
private static String RABBIT_USER_PWD = "guest";
public static void main(String[] args) {
try {
//step1.获取rabbitmq集群各个节点实例的状态信息
Map<String, ClusterStatus> clusterMap =
fetchRabbtMQClusterStatus(RABBIT_NODES_STATUS_REST_URL, RABBIT_USER_NAME, RABBIT_USER_PWD);
//step2.打印输出各个节点实例的状态信息
for (Map.Entry entry : clusterMap.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
//step3.获取rabbitmq集群用户信息
Map<String, User> userMap =
fetchRabbtMQUsers(RABBIT_USERS_REST_URL, RABBIT_USER_NAME, RABBIT_USER_PWD);
//step4.打印输出rabbitmq集群用户信息
for (Map.Entry entry : userMap.entrySet()) {
System.out.println(entry.getKey() + " : " + entry.getValue());
}
} catch (IOException e) {
e.printStackTrace();
-
启动测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AR6TJu9F-1592451875275)(assets/1572492408012.png)]
2.4. Zabbix 监控RabbitMQ
Zabbix是一个基于WEB界面提供分布式系统监视以及网络监视功能的企业级开源解决方案,他也可以帮助我们搭建一个MQ集群的监控系统,同时提供预警等功能,但是由于其搭建配置要求比较高一般都是由运维人员负责搭建,感兴趣的同学可以访问https://www.zabbix.com/ 官网进行了解学习。
三、RabbitMQ高可用集群
1. RabbitMQ集群架构模式
-
主备模式
用来实现RabbitMQ的高可用集群,一般是在并发和数据不是特别多的时候使用,当主节点挂掉以后会从备份节点中选择一个节点出来作为主节点对外提供服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFSLRaTX-1592451926039)(assets/1572494240934.png)]
-
远程模式
主要用来实现双活,简称为Shovel模式,所谓的Shovel模式就是让我们可以把消息复制到不同的数据中心,让两个跨地域的集群互联。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-adAuPA4A-1592451926041)(assets/1572494294216.png)]
-
镜像队列模式
镜像队列也被称为Mirror队列,主要是用来保证mq消息的可靠性的,他通过消息复制的方式能够保证我们的消息100%不丢失,同时该集群模式也是企业中使用最多的模式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QtT17fDn-1592451926043)(assets/1572494368606.png)]
-
多活模式
多活模式主要是用来实现异地数据复制,Shovel模式其实也可以实现,但是他的配置及其繁琐同时还要受到版本的限制,所以如果做异地多活我们更加推荐使用多活模式,使用多活模式我们需要借助federation插件来实现集群与集群之间或者节点与节点之前的消息复制,该模式被广泛应用于饿了么、美团、滴滴等企业。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zdj8fF9j-1592451926045)(assets/1572494419652.png)]
-
集群模式总结
主备模式下主节点提供读写,从节点不提供读写服务,只是负责提供备份服务,备份节点的主要功能是在主节点宕机时,完成自动切换 从–>主,同时因为主备模式下始终只有一个对外提供服务那么对于高并发的情况下该模式并不合适.
远程模式可以让我们实现异地多活的mq,但是现在已经有了更好的异地多活解决方案,所以在实际的项目中已经不推荐使用了
镜像队列模式可以让我们的消息100%不丢失,同时可以结合HAProxy来实现高并发的业务场景所以在项目中使用得最多
2. 镜像队列集群搭建
-
集群节点规划
ip地址 用途 主机名 192.168.13.101 mq 主节点 server1 192.168.13.102 mq 从节点 server2 192.168.13.103 mq 从节点 server3 192.168.13.104 HAProxy KeepAlive server4 192.168.13.105 HAProxy KeepAlive server5 特别注意: 每个同学的ip可能都是不一样的,各个学员根据自己的ip进行规划,同时特别注意主机名称,因为我们后面的命令中会使用到所以需要提前设置好。
需要配置主机名的映射:
vi /etc/hosts
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQgvfYSN-1592451926046)(assets/1572507661664.png)]
-
复制主节点的.erlang.cookie文件到其他所有的从节点
-
停掉所有的MQ节点然后使用集群的方式启动
主节点:
- 将从节点添加到主节点的集群中
- 查看集群的状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0vNHkrc-1592451926052)(assets/05查看集群的状态.png)]
-
访问集群中的任何一个节点的控制台查看集群情况
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DULyIKf2-1592451926055)(assets/06管理控制台查看集群信息.png)]
-
设置镜像队列策略
-
在管控台创建一个队列然后发送一条消息查看其他节点是否接收到
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J9Ev0Px4-1592451926057)(assets/07.png)]
-
修改集群信息
-
总结
到此为止我们的镜像队列的集群就搭建完成了,主要注意.erlang.cookie文件的同步和集群命令的书写
3. HAProxy 实现镜像队列的负载均衡
-
HAProxy 简介
HAProxy是一款提供高可用性、负载均衡以及基于TCP和HTTP应用的代理软件,HAProxy是完全免费的、借助HAProxy可以快速并且可靠的提供基于TCP和HTTP应用的代理解决方案。
HAProxy适用于那些负载较大的web站点,这些站点通常又需要会话保持或七层处理。
HAProxy可以支持数以万计的并发连接,并且HAProxy的运行模式使得它可以很简单安全的整合进架构中,同时可以保护web服务器不被暴露到网络上。
-
HAProxy 的规划(搭建2台是为后面做HAProxy高可用做准备)
ip 用途 主机名 192.168.13.104 HAProxy server4 192.168.13.105 HAProxy server5 -
HAProxy 的安装
-
HAProxy 的配置
104机器ha配置
105机器ha配置
- 启动HAproxy
-
访问HAproxy( 如果不能访问看下防火墙是否关闭)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vQkMySbO-1592451926059)(assets/1572509988524.png)]
关闭集群中的某一个节点:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8CdmcfvR-1592451926061)(assets/1572510114961.png)]
再次查看HAProxy的管理控制台:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eeiJKoM6-1592451926062)(assets/1572510210880.png)]
-
编写程序测试负载均衡
- 生产者一端申明交换机和队列,并完成绑定
- application.yml配置文件(重要)
- 在生产者一端编写测试方法发送消息
- 测试结果(连续发送了2条消息一条被server1接收一条被server2接收,)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ulnDBrxD-1592451926063)(assets/09.png)]
4. KeepAlived 搭建高可用的HAProxy集群
-
KeepAlived 简介
Keepalived,它是一个高性能的服务器高可用或热备解决方案,Keepalived主要来防止服务器单点故障的发生问题,可以通过其与Nginx、Haproxy等反向代理的负载均衡服务器配合实现web服务端的高可用。Keepalived以VRRP协议为实现基础,用VRRP协议来实现高可用性(HA).VRRP(Virtual Router Redundancy Protocol)协议是用于实现路由器冗余的协议,VRRP协议将两台或多台路由器设备虚拟成一个设备,对外提供虚拟路由器IP(一个或多个)。
-
KeepAlived 的安装
- KeepAlived 的配置
- 编写执行脚本(一定要赋权否则不能执行)
- 执行脚本赋权
- 启动keepalived
-
高可用测试
-
查看keepalived启动状态
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GuPUvEMs-1592451926065)(assets/12.png)]
-
停止server4节点的keepalived查看我们的虚拟ip是否漂移到了server5
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HIzx3AWZ-1592451926066)(assets/13.png)]
-
编写程序连接虚拟ip查看我们的mq是否成功
- 修改application.yml文件
-
2. 启动测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-firu3oY2-1592451926069)(assets/14.png)]
四、RabbitMQ应用
1. 消息堆积
当消息生产的速度长时间,远远大于消费的速度时。就会造成消息堆积。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w160K4ZG-1592451967287)(assets/消息堆积02.png)]
- 消息堆积的影响
- 可能导致新消息无法进入队列
- 可能导致旧消息无法丢失
- 消息等待消费的时间过长,超出了业务容忍范围。
- 产生堆积的情况
- 生产者突然大量发布消息
- 消费者消费失败
- 消费者出现性能瓶颈。
- 消费者挂掉
- 解决办法
- 排查消费者的消费性能瓶颈
- 增加消费者的多线程处理
- 部署增加多个消费者
- 场景介绍
在用户登录成功之后,会向rabbitmq发送一个登录成功的消息。这个消息可以被多类业务订阅。
登录成功,记录登录日志;登录成功,根据规则送积分。其中登录送积分可以模拟成较为耗时的处理
场景重现:让消息产生堆积
-
生产者大量发送消息:使用Jmeter开启多线程,循环发送消息大量进入队列。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pBJyJ8Z9-1592451967289)(assets/消息堆积03.png)]
模拟堆积10万条数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NxLyNmGF-1592451967290)(assets/消息堆积04.png)]
-
消费者消费失败:随机抛出异常,模拟消费者消费失败,没有ack(手动ack的时候)。
-
设置消费者的性能瓶颈:在消费方法中设置休眠时间,模拟性能瓶颈
-
关闭消费者:停掉消费者,模拟消费者挂掉
-
消费者端示例核心代码:
public class LoginIntegralComsumer implements MessageListener {
public void onMessage(Message message) {
String jsonString = null;
try {
jsonString = new String(message.getBody(),"UTF8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
if(new Random().nextInt(5)==2){
//模拟发生异常
throw new RuntimeException("模拟处理异常");
}
try {
//模拟耗时的处理过程
TimeUnit.MILLISECONDS.sleep(1000);
System.out.println(Thread.currentThread().getName()+"处理消息:"+jsonString);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 如果每1秒钟处理一条消息
1小时处理 60*60=3600条
处理完10万条数据 100000/3600=27.7小时
问题解决:消息已经堆积如何解决
消息队列堆积,想办法把消息转移到一个新的队列,增加服务器慢慢来消费这个消息可以
生产环境的队列可用状态
1、解决消费者消费异常问题
2、解决消费者的性能瓶颈:改短休眠时间
5.4小时。
3、增加消费线程,增加多台服务器部署消费者。快速消费。
增加10个线程
concurrency="10" prefetch="10"
1小时
增加一台服务器
0.5小时
2. 消息丢失
在实际的生产环境中有可能出现一条消息因为一些原因丢失,导致消息没有消费成功,从而造成数据不一致等问题,造成严重的影响,比如:在一个商城的下单业务中,需要生成订单信息和扣减库存两个动作,如果使用RabbitMQ来实现该业务,那么在订单服务下单成功后需要发送一条消息到库存服务进行扣减库存,如果在此过程中,一条消息因为某些原因丢失,那么就会出现下单成功但是库存没有扣减,从而导致超卖的情况,也就是库存已经没有了,但是用户还能下单,这个问题对于商城系统来说是致命的。
消息丢失的场景主要分为:消息在生产者丢失,消息在RabbitMQ丢失,消息在消费者丢失。
2.1. 消息在生产者丢失
场景介绍
消息生产者发送消息成功,但是MQ没有收到该消息,消息在从生产者传输到MQ的过程中丢失,一般是由于网络不稳定的原因。
解决方案
采用RabbitMQ 发送方消息确认机制,当消息成功被MQ接收到时,会给生产者发送一个确认消息,表示接收成功。RabbitMQ 发送方消息确认模式有以下三种:普通确认模式,批量确认模式,异步监听确认模式。spring整合RabbitMQ后只使用了异步监听确认模式。
*说明*
异步监听模式,可以实现边发送消息边进行确认,不影响主线程任务执行。
*步骤*
-
生产者发送3000条消息
-
在发送消息前开启开启发送方确认模式
- 在发送消息前添加异步确认监听器
2.2. 消息在RabbitMQ丢失
场景介绍
消息成功发送到MQ,消息还没被消费却在MQ中丢失,比如MQ服务器宕机或者重启会出现这种情况
解决方案
持久化交换机,队列,消息,确保MQ服务器重启时依然能从磁盘恢复对应的交换机,队列和消息。
spring整合后默认开启了交换机,队列,消息的持久化,所以不修改任何设置就可以保证消息不在RabbitMQ丢失。但是为了以防万一,还是可以申明下。
2.3. 消息在消费者丢失
场景介绍
消息费者消费消息时,如果设置为自动回复MQ,消息者端收到消息后会自动回复MQ服务器,MQ则会删除该条消息,如果消息已经在MQ被删除但是消费者的业务处理出现异常或者消费者服务宕机,那么就会导致该消息没有处理成功从而导致该条消息丢失。
解决方案
设置为手动回复MQ服务器,当消费者出现异常或者服务宕机时,MQ服务器不会删除该消息,而是会把消息重发给绑定该队列的消费者,如果该队列只绑定了一个消费者,那么该消息会一直保存在MQ服务器,直到消息者能正常消费为止。本解决方案以一个队列绑定多个消费者为例来说明,一般在生产环境上也会让一个队列绑定多个消费者也就是工作队列模式来减轻压力,提高消息处理效率
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2.消费者未响应ACK,消费者服务挂掉
3. 有序消费消息
3.1. 场景介绍
场景1
当RabbitMQ采用work Queue模式,此时只会有一个Queue但是会有多个Consumer,同时多个Consumer直接是竞争关系,此时就会出现MQ消息乱序的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j7FlaQsD-1592451967293)(assets/MQ乱序1.png)]
场景2
当RabbitMQ采用简单队列模式的时候,如果消费者采用多线程的方式来加速消息的处理,此时也会出现消息乱序的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WkhpmR4q-1592451967295)(assets/MQ乱序2.png)]
3.2. 场景1解决
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpmneC9M-1592451967295)(assets/场景1解决方案.png)]
3.3. 场景2解决
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y28fRVLu-1592451967296)(assets/场景2解决方案.png)]
4. 重复消费
4.1. 场景介绍
为了防止消息在消费者端丢失,会采用手动回复MQ的方式来解决,同时也引出了一个问题,消费者处理消息成功,手动回复MQ时由于网络不稳定,连接断开,导致MQ没有收到消费者回复的消息,那么该条消息还会保存在MQ的消息队列,由于MQ的消息重发机制,会重新把该条消息发给和该队列绑定的消息者处理,这样就会导致消息重复消费。而有些操作是不允许重复消费的,比如下单,减库存,扣款等操作。
MQ重发消息场景:
1.消费者未响应ACK,主动关闭频道或者连接
2.消费者未响应ACK,消费者服务挂掉
4.2. 解决方案
如果消费消息的业务是幂等性操作(同一个操作执行多次,结果不变)就算重复消费也没问题,可以不做处理,如果不支持幂等性操作,如:下单,减库存,扣款等,那么可以在消费者端每次消费成功后将该条消息id保存到数据库,每次消费前查询该消息id,如果该条消息id已经存在那么表示已经消费过就不再消费否则就消费。本方案采用redis存储消息id,因为redis是单线程的,并且性能也非常好,提供了很多原子性的命令,本方案使用setnx命令存储消息id。
setnx(key,value):如果key不存在则插入成功且返回1,如果key存在,则不进行任何操作,返回0