RabbitMQ - 稳定性探讨 - SpringBoot集成
概念:
Broker:接收和分发消息的应用,RabbitMQ Server就是Message Broker
Connection: publisher / consumer和 broker之间的TCP连接
Channel:如果每一次访问RabbitMQ都建立一个Connection,在消息量大的时候建立TCP
Connection的开销将是巨大的,效率也较低。Channel是在connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个thread创建单独的channel进行通讯,AMQP method包含了channel id 帮助客户端和message broker识别 channel,所以channel 之间是完全隔离的。Channel作为轻量级的Connection极大减少了操作系统建TCP connection的开销
Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有: direct (point-to-point), topic(publish-subscribe) and fanout
(multicast)
Routing Key:生产者将消息发送到交换机时会携带一个key,来指定路由规则
binding Key:在绑定Exchange和Queue时,会指定一个BindingKey,生产者发送消息携带的RoutingKey会和bindingKey对比,若一致就将消息分发至这个队列
vHost 虚拟主机:每一个RabbitMQ服务器可以开设多个虚拟主机每一个vhost本质上是一个mini版的RabbitMQ服务器,拥有自己的 "交换机exchange、绑定Binding、队列Queue",更重要的是每一个vhost拥有独立的权限机制,这样就能安全地使用一个RabbitMQ服务器来服务多个应用程序,其中每个vhost服务一个应用程序。
工作模式
五种常用模式
1.simple (简单模式)
一个消费者消费一个生产者生产的信息
2.Work queues(工作模式)
一个生产者生产信息,多个消费者进行消费,但是一条消息只能消费一次
3.Publish/Subscribe(发布订阅模式)
生产者首先投递消息到交换机,订阅了这个交换机的队列就会收到生产者投递的消息
4.Routing(路由模式)
生产者生产消息投递到direct交换机中,扇出交换机会根据消息携带的routing Key匹配相应的队列
5.Topics(主题模式)
生产者生产消息投递到topic交换机中,上面是完全匹配路由键,而主题模式是模糊匹配,只要有合适规则的路由就会投递给消费者
保证消息的稳定性
消息持久化
RabbitMQ的消息默认存在内存中的,一旦服务器意外挂掉,消息就会丢失
消息持久化需做到三点
Exchange设置持久化
Queue设置持久化
Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
ACK确认机制
多个消费者同时收取消息,收取消息到一半,突然某个消费者挂掉,要保证此条消息不丢失,就需要acknowledgement机制,就是消费者消费完要通知服务端,服务端才将数据删除
这样就解决了,及时一个消费者出了问题,没有同步消息给服务端,还有其他的消费端去消费,保证了消息不丢的case。
设置集群镜像模式
我们先来介绍下RabbitMQ三种部署模式:
1)单节点模式:最简单的情况,非集群模式,节点挂了,消息就不能用了。业务可能瘫痪,只能等待。
2)普通模式:默认的集群模式,某个节点挂了,该节点上的消息不能用,有影响的业务瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)。
3)镜像模式:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案
为什么设置镜像模式集群,因为队列的内容仅仅存在某一个节点上面,不会存在所有节点上面,所有节点仅仅存放消息结构和元数据。
消息发送失败补偿方案
消息发送失败处理方案
场景一:消息找不到队列导致消息发送失败。
设置mandatory=true
当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列的话,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。
这时候可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。
channel.basicPublish("EXCHANGE_NAME", "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "text".getBytes());
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
String message = new String(body);
System.out.println("返回结果是:" + message);
}
});
场景二:生产者客户端发送出去之后可以发生网络丢包、网络故障等造成消息丢失
- 方案一:开启MQ的事务机制
在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。
缺点:只有消息成功被RabbitMQ接收,事务才能提交成功,否则我们便可在捕获异常之后进行事务回滚,与此同时可以进行消息重发。但是使用事务机制的话会“吸干”RabbitMQ的性能。
- 方案二:生产者将信道设置成confirm(确认)模式
一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。RabbitMQ回传给生产者的确认消息中的deliveryTag包含了确认消息的序号,此外RabbitMQ也可以设置channel.basicAck方法中的multiple参数,表示到这个序号之前的所有消息都已经得到了处理。
消息发送失败补偿方案
当消息发送失败后,结合MQ配置,对消息进行重试并记录error日志,达到重试次数后,将处理结果通过回调接口的方式告诉生产者,生产者去进行额外的补偿机制。
confirm方案对比
客户端实现生产者confirm有三种方式:
普通confirm模式:每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。实际上是一种串行confirm了。
批量confirm模式:每发送一批消息后,调用waitForConfirms()方法,等待服务器端confirm。
异步confirm模式:提供一个回调方法,服务端confirm了一条或者多条消息后Client端会回调这个方
注意:
批量模式极大提升confirm效率,但是问题在于一旦出现confirm返回false或者超时的情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且,当消息经常丢失时,批量confirm性能应该是不升反降的。
消息消费失败处理方案
设置死信队列
当消息发送失败后,设置requeue=false消息进入死信队列,并获取死信队列的长度,设置重新发送到正常队列的重试时间和重试间隔,重新发送到正常队列。
监控死信队列长度,日志记录及时预警。
实现延迟队列
RabbitMQ本身没有延迟队列,需要靠TTL和DLX模拟出延迟的效果
TTL来设置一个消息的的过期时间,DLX设置一个死信队列,将过期的消息推送到死信队列中,消费端监听死信队列来消费数据,从而达到消息延迟的效果。
死信队列补偿机制
当消息消费失败后,进入死信队列,框架层实现逻辑,获取对应死信队列的消息长度,当大于0时并判断是否超过重试次数并达到重试间隔。当没有超过重试次数时,自动将消息从死信队列迁移到正常队列。
消息防堆积方案
- 加强对不合理使用MQ的审批。
- 监控消费能力(耗时<300ms),及时预警。
- 框架层实现发送方限流。(默认值:100条/s)
- 设置消息TTL。
- 使用惰性队列。
关键节点日志记录
MQ成功接受消息时。(info)
生产者消息发送失败时。(error)
生产者confrim确认失败时。(error)
生产消息量过大,限流时。(error)
生产者连接MQ超时时。 (error)
消息大小大于10KB时。(error)
消费者成功消费消息是。(info)
消费者连接MQ超时时。(error)
消费者消费失败时。(error)
消费者进入死信队列时。(error)
消费耗时低于300ms时。(error)
项目集成
maven引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
yam配置
spring:
#配置rabbitMq 服务器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#虚拟host 可以不设置,使用server默认host
virtual-host: itclub
# 开启publisher-confirm,
# 这里支持两种类型:simple:同步等待confirm结果,直到超时;# correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
publisher-confirm-type: correlated
# publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
publisher-returns: true
# 定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
template:
mandatory: true
经典案例代码实现
1. 延迟队列实现
/**
* 延迟队列
* @Author : onePiece
*/
@Configuration
public class TtlQueueConfig {
/**
* 普通交换机名称
*/
public static final String X_EXCHANGE = "X";
/**
* 死信交换机名称
*/
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
/**
* 普通队列名称
*/
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
/**
* 死信队列名称
*/
public static final String DEAD_LETTER_QUEUE = "QD";
public static final String DEAD_LETTER_QUEUE_KEY = "YD";
/**
* 声明 XExchange
*/
@Bean
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
/**
* 声明 yExchange
*/
@Bean
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
/**
* 声明队列QA
*/
@Bean
public Queue queueA(){
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
arguments.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUE_KEY);
// 设置过期时间
arguments.put("x-message-ttl", 2000);
return new Queue(QUEUE_A, true, true, false, arguments);
}
/**
* 声明队列QB
*/
@Bean
public Queue queueB(){
Map<String, Object> arguments = new HashMap<>(3);
// 设置死信交换机
arguments.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
// 设置死信路由键
arguments.put("x-dead-letter-routing-key", DEAD_LETTER_QUEUE_KEY);
// 设置过期时间
arguments.put("x-message-ttl", 4000);
return new Queue(QUEUE_B, true, true, false, arguments);
}
/**
* 死信队列QD
*/
@Bean
public Queue queueD(){
return QueueBuilder.durable(DEAD_LETTER_QUEUE).build();
}
/**
* 绑定交换机
*/
@Bean
public Binding queueABindingX(){
return BindingBuilder.bind(queueA()).to(xExchange()).with("XA");
}
@Bean
public Binding queueBBindingX(){
return BindingBuilder.bind(queueB()).to(xExchange()).with("XB");
}
@Bean
public Binding queueDBindingY(){
return BindingBuilder.bind(queueD()).to(yExchange()).with(DEAD_LETTER_QUEUE_KEY);
}
}
生产端
/**
* 延迟队列-生产端
*
* @author: onePiece
* @date: 2023-02-21
*/
@RestController
@RequestMapping("demo")
public class TtlProvider {
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("ttl")
public AjaxResult ttl(){
for (int i = 0; i < 5; i++) {
String messageId = Convert.toStr(UUID.randomUUID());
String messageData = "ttl hello rabbit";
String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
Map<String, Object> map = new HashMap<>(16);
map.put("messageId", messageId);
map.put("messageData", messageData);
map.put("createTime", createTime);
if (i % 2 == 0) {
rabbitTemplate.convertAndSend("X", "XA", map);
}else {
rabbitTemplate.convertAndSend("X", "XB", map);
}
}
return AjaxResult.success();
}
}
消费端
/**
* 延迟消费
*
* @author: onePiece
* @date: 2023-02-21
*/
@Component
public class TtlReceiver {
// @RabbitListener(queues = "QA")
// public void xa(Message message) throws Exception {
// String nowDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
// byte[] body = message.getBody();
// ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body));
// System.out.println("QA队列消费: 当前时间" + nowDate + ois.readObject().toString());
// }
@RabbitListener(queues = "QD")
public void xb(Message message) throws Exception {
String nowDate = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
byte[] body = message.getBody();
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(body));
System.out.println("死信队列消费: 当前时间" + nowDate + ois.readObject().toString());
}
}
2. 惰性队列实现
/**
* 惰性队列
* 防止消息堆积
* @author: onePiece
*/
@SuppressWarnings("all")
@Configuration
public class LazyQueueConfig {
public static final String LAZY_TOPIC_EXCHANGE = "lazy.topic.exchange";
public static final String LAZY_TOPIC_QUEUE = "lazy_topic_queue";
public static final String LAZY_TOPIC_ROUTING_KEY = "*.topic.*";
/**
* 声明队列
*/
@Bean
public Queue topicLazyQueue(){
Map<String, Object> args = new HashMap<>(2);
args.put("x-queue-mode", "lazy");
/**
* 设置持久化队列
*/
return QueueBuilder.durable(LAZY_TOPIC_QUEUE).withArguments(args).build();
}
/**
* 声明Topic类型交换器
*/
@Bean
public TopicExchange topicLazyExchange(){
TopicExchange exchange = new TopicExchange(LAZY_TOPIC_EXCHANGE);
return exchange;
}
/**
* Topic交换器和队列通过bindingKey绑定
* @return
*/
@Bean
public Binding bindingTopicLazyExchangeQueue(){
return BindingBuilder.bind(topicLazyQueue()).to(topicLazyExchange()).with(LAZY_TOPIC_ROUTING_KEY);
}
}
消费端
/**
* 惰性队列-生产端
*
* @author: onePiece
* @date: 2023-02-21
*/
@SuppressWarnings("all")
@RestController("demo")
public class LazyQueueProvider {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 创建一个消息是否投递成功的回调方法
*/
private final RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
/**
*
* @param correlationData 消息的附加信息
* @param ack true for ack, false for nack
* @param cause 是一个可选的原因,对于nack,如果可用,否则为空。
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(!ack){
//可以进行日志记录、异常处理、补偿处理等
System.err.println("异常ack-"+ack+",id-"+correlationData.getId()+",cause:"+cause);
}else {
//更新数据库,可靠性投递机制
System.out.println("正常ack-"+ack+",id-"+correlationData.getId());
try{
System.out.println(new String(correlationData.getReturnedMessage().getBody()));
} catch (Exception e){
}
}
}
};
/**
* 创建一个消息是否被队列接收的监听对象,如果没有队列接收发送出的消息,则调用此方法进行后续处理
*/
private final RabbitTemplate.ReturnCallback returnCallback = new RabbitTemplate.ReturnCallback() {
/**
*
* @param message 被退回的消息
* @param replyCode 错误编码
* @param replyText 错误描述
* @param exchange 交换器
* @param routingKey 路由
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.err.println("spring_returned_message_correlation:"+message.getMessageProperties().getHeaders().get(PublisherCallbackChannel.RETURNED_MESSAGE_CORRELATION_KEY)
+"return exchange: " + exchange
+ ", routingKey: "+ routingKey
+ ", replyCode: " + replyCode
+ ", replyText: " + replyText
+ ",message:" + message);
try {
System.out.println(new String(message.getBody()));
} catch (Exception e){
}
}
};
/**
* 扩展点,在消息转换完成之后,发送之前调用;可以修改消息属性、消息头信息
*/
private final MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties properties = message.getMessageProperties();
/**
* 设置消息发送到队列之后多久被丢弃,单位:毫秒
* 此种方案需要每条消息都设置此属性,比较灵活;
* 还有一种方案是在声明队列的时候指定发送到队列中的过期时间;
* * Queue queue = new Queue("test_queue2");
* * queue.getArguments().put("x-message-ttl", 10000);
* 这两种方案可以同时存在,以值小的为准
*/
//properties.setExpiration("10000");
/**
* 设置消息的优先级
*/
properties.setPriority(9);
/**
* 设置消息发送到队列中的模式,持久化|非持久化(只存在于内存中)
*/
properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
return message;
}
};
/**
* 发送消息
* @param exchange 交换器
* @param route 路由键
* @param message 消息
* @param properties
*/
@GetMapping("sendlazyQueue")
public void sendMsg(String exchange, String routingKey, String message, MessageProperties properties){
/**
* 设置生产者消息publish-confirm回调函数
*/
this.rabbitTemplate.setConfirmCallback(confirmCallback);
/**
* 设置消息退回回调函数
*/
this.rabbitTemplate.setReturnCallback(returnCallback);
/**
* 新增消息转换完成后、发送之前的扩展点
*/
this.rabbitTemplate.setBeforePublishPostProcessors(messagePostProcessor);
try {
if(null == properties){
properties = new MessageProperties();
}
/**
* 设置消息唯一标识
*/
properties.setMessageId(UUID.randomUUID().toString());
/**
* 创建消息包装对象
*/
Message msg = MessageBuilder.withBody(message.getBytes()).andProperties(properties).build();
/**
* 将消息主题和属性封装在Message类中
*/
Message returnedMessage = MessageBuilder.withBody(message.getBytes()).build();
/**
* 相关数据
*/
CorrelationData correlationData = new CorrelationData();
/**
* 消息ID,全局唯一
*/
correlationData.setId(msg.getMessageProperties().getMessageId());
/**
* 设置此相关数据的返回消息
*/
correlationData.setReturnedMessage(returnedMessage);
/**
* 如果msg是org.springframework.amqp.core.Message对象的实例,则直接返回,否则转化为Message对象
*/
this.rabbitTemplate.convertAndSend(exchange, routingKey, msg, correlationData);
} catch (Exception e){
e.printStackTrace();
}
}
}
消费端
/**
* 惰性队列-消费端
*
* @author: onePiece
* @date: 2023-02-21
*/
@SuppressWarnings("all")
@Component
public class LazyQueueReceiver {
/**
*
* @param channel 信道
* @param message 消息
* @throws Exception
*/
@RabbitListener(queues = LazyQueueConfig.LAZY_TOPIC_QUEUE)
public void onMessage(Channel channel, Message message) throws Exception {
System.out.println("--------------------------------------");
System.out.println("消费端Payload: " + message.getPayload()+"-ID:"+message.getHeaders().getId()+"-messageId:"+message.getHeaders());
Long deliveryTag = (Long)message.getHeaders().get(AmqpHeaders.DELIVERY_TAG);
//手工ACK,获取deliveryTag
channel.basicAck(deliveryTag, false);
}
}