1.MQ的一些常见问题
消息可靠性问题:如何确保发送的消息至少被消费一次
延迟消息问题:如何实现消息的延迟投递
高可用问题:如何解决数百万消息堆积,无法及时消费的问题
消息堆积问题:如何避免单点的MQ故障而导致的不可用问题
2.消息可靠性
1.消息丢失场景
1.发送时丢失
1.生产者发送的消息未送达exchange
2.消息到达exchange后未到达queue
2.MQ宕机,queue将消息丢失
3.consumer接收到消息后未消费就宕机
2.生产者消息确认
RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。结果有两种:
1.publisher-confirm:发送者确认
消息成功投递到交换机,返回ack
消息未投递到交换机,返回nack
2.publisher-return:发送者回执
消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因。
确认机制发送消息时,需要给每个消息设置一个全局唯一id,以区分不同消息,避免ack冲突
3.SpringAMQP实现生产者确认
1.在生产者服务yml文件中添加配置
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher-confirm
publisher-returns: true # 开启publisher-return功能,基于callback机制
template:
mandatory: true # 定义消息路由失败时的策略。 true,则调用ReturnCallback,false,则直接丢弃消息
配置说明:
publish-confirm-type:开启publisher-confirm,支持两种类型
simple:同步等待confirm结果,直到超时
correlated:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
publish-returns:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallback
template.mandatroy:定义消息路由失败时的策略。true,则调用ReturnCallback,false,则直接丢弃消息
2.每个RabbitTemplate只能配置一个ReturnCallback,因此需要在项目启动过程中配置。
@Slf4j
@Configuration
public class CommonConfig implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate对象
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 配置ReturnCallback
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
// 记录日志
log.error("消息发送到队列失败,响应码:{},失败原因:{},交换机:{},路由key:{},消息:{}",
replyCode, replyText, exchange, routingKey, message.toString());
// 如果有需要的话,重发消息
});
}
}
3.发送消息,指定消息ID,消息ConfirmCallback
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue() {
String routingKey = "simple";
// 1.准备消息
String message = "hello, spring amqp!";
// 2.准备CorrelationData
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
// 3.准备ConfirmCallback
correlationData.getFuture().addCallback(result -> {
// 判断结果
if (result.isAck()) {
// ACK
log.debug("消息成功投递大宋交换机!消息ID:{}", correlationData.getId());
} else {
// NACK
log.error("消息投递到交换机失败!消息ID:{}", correlationData.getId());
// 重发
}
}, throwable -> {
log.error("消息发送失败!", throwable);
// 重发
});
rabbitTemplate.convertAndSend("camq.topic", routingKey, message, correlationData);
}
4.消息持久化
MQ默认是内存存储消息,开启持久化功能可用确保缓存在MQ中的消息不丢失
1.交换机持久化
@Bean
public DirectExchange simpleDirect() {
// 交换机名称 是否持久化 当没有queue与其绑定时是否自动删除
return new DirectExchange("simple.direct", true, false);
}
2.队列持久化
@Bean
public Queue simpleQueue() {
// 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();
}
3.消息持久化
SpringAMQP中的消息默认是持久的,可以通过MessageProperties中的DeliveryMode来指定
@Test
public void testDurableMessage() {
// 准备消息
Message message = MessageBuilder.withBody("hello, spring".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
// 发送消息
rabbitTemplate.convertAndSend("simple.queue", message);
}
5.消费者消息确认
RabbitMQ支持消费者确认机制。消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息。SpirngAMQP支持三种确认模式:
1.manual:手动ack,需要在业务代码结束后,调用api发送ack
2.auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack,抛出异常则返回nack
3.none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除
配置方式,消费者服务yam文件:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none
6.失败重试机制
当消费者出现异常后,消息会不断requeue(重新入队)到队列,再重新发送给消费者,然后再次异常,再次requeue,无限循环,导致mq的信息处理飙升,带来不必要的压力
解决:利用spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 重试的失败等待时长为1秒
multiplier: 1 # 下次失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态,false有状态,如果业务中包含事务,这里改为false
7.消费者失败消息处理策略
在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecover接口来处理,它包含三种不同的实现
1.RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认。
2.ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队。
3.RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。(优秀方案)
@Bean
public DirectExchange errorMessageExchange() {
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue() {
return new Queue("error.queue");
}
@Bean
public Binding errorMessageBinding() {
return BindingBuilder.bind(errorQueue()).to(errorMessageExchange()).with("error");
}
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) {
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
8.如何确保RabbitMQ消息的可靠性?
1.开启生产者确认机制,确保生产者的消息能到达队列
2.开启持久化功能,确保消息未消费前在队列中不会丢失
3.开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
4.开启消费者失败重试机制,并设置MessageRecoverer,多次重试失败后将消息投递到异常交换机,交由人工处理。
3.死信交换机
当一个队列中的消息满足以下情况之一时,成为死信(dead letter)
1.消费者使用basic.reject或basic.nack声明消费失败,并且消息的requeue重试设置为false
2.消息是一个过期消息,超时无人消费
3.要投递的队列消息堆积满了,最早的消息可能成为死信
如果该队列配置了dead-letter-exchange属性,指定了一个死信交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange, DLX)
1.TTL(Time-To-Live)
如果一个队列中的消息TTL结束仍未消费,则会变为死信,分两种情况:
1.消息所在的队列设置了存活时间
2.消息本身设置了存活时间
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.queue", durable = "true"),
exchange = @Exchange(name = "dl.direct"),
key = "dl"
))
public void listenDLQueue(String msg) {
log.info("消费者接收到了dl.queue的延迟消息");
}
@Configuration
public class TTLMessageConfig {
@Bean
public DirectExchange ttlDirectExchange() {
return new DirectExchange("ttl.direct");
}
@Bean
public Queue ttlQueue() {
return QueueBuilder
.durable("ttl.queue")
.ttl(10000)
.deadLetterExchange("dl.direct")
.deadLetterRoutingKey("dl")
.build();
}
@Bean
public Binding ttlBinding() {
return BindingBuilder.bind(ttlQueue()).to(ttlDirectExchange()).with("ttl");
}
}
@Test
public void testTTLMessage() {
// 准备消息
Message message = MessageBuilder.withBody("hello, ttl".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.setExpiration("5000") // 消息设置超时时间
.build();
// 发送消息
rabbitTemplate.convertAndSend("ttl.direct", "ttl", message);
}
2.延迟队列
利用TTL结合死信交换机,实现了消息发出后,消费者延迟收到消息的效果。这种模式称为延迟队列(Delay Queue)模式
使用场景:1.延迟发送短信 2.用户下单,15分钟内未支付,则自动取消 3.预约工作会议,20分钟后自动通知所有参会人员
延迟队列插件安装
见附件
SpringAMQP使用延迟队列插件
声明交换机时设定delayed属性为true即可
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.queue", durable = "true"),
exchange = @Exchange(name = "dl.direct", delayed = "true"),
key = "dl"
))
public void listenDLQueue(String msg) {
log.info("消费者接收到了dl.queue的延迟消息");
}
3.惰性队列
当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。最早接收到的消息,可能就会变成死信,会被丢弃,这就是消息堆积。
解决思路:
1.增加更多的消费者,提高消费速度
2.在消费者内开启线程池加快消息处理速度
3.扩大队列容积,提高堆积上限
从RabbitMQ的3.6.0版本,增加了惰性队列(Lazy Queues)的概念,特征如下:
1.接收到消息后直接存入磁盘而非内存
2.消费者要消费消息时才会从磁盘中读取并加载到内存
3.支持数百万条的消息存储
优点:
1.基于磁盘存储,消息上限高
2.没有间歇性的page-out,性能比较稳定
缺点:
1.基于磁盘存储,消息时效性会降低
2.性能受限于磁盘的IO
设置一个队列为惰性队列,只需要在声明队列时,指定x-queue-mode属性为lazy即可。
命令行方式
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' -- apply-to queues
@Bean方式
@Configuration
public class LazyConfig {
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable("lazy.queue")
.lazy()
.build();
}
@Bean
public Queue normalQueue() {
return QueueBuilder.durable("normal.queue").build();
}
}
注解方式
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "dl.queue", durable = "true", arguments = @Argument(name = "x-queue-mode", value = "lazy")),
exchange = @Exchange(name = "dl.direct"),
key = "dl"
))
public void listenDLQueue(String msg) {
log.info("消费者接收到了dl.queue的延迟消息");
}
4.MQ集群
1.集群分类
1.普通集群:是一种分布式集群,将队列分散到集群的各个节点,从而提高整个集群的并发能力。
2.镜像集群:是一种主从集群,普通集群的基础上,添加了主从备份功能,提高集群的数据可用性。
3.仲裁队列:镜像集群虽然支持主从,但主从同步并不是强一种的,某些情况下可能有数据丢失的风险。因此推出了仲裁队列代替镜像集群,底层采用Raft协议确保主从的数据一致性。
2.普通集群(标准集群,classic cluster)
特征:
1.会在集群的各个节点间共享部分数据,包括:交换机,队列元信息。不包含队列中的消息。
2.当访问集群节点是,如果队列不在该节点,会从数据所在节点传递到当前节点并返回。
3.队列所在节点宕机,队列中的消息就会丢失。
3.镜像集群
本质是主从模式,特征:
1.交换机,队列,队列中的消息会在各个mq的镜像节点之间同步备份。
2.创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
3.一个队列的主节点可能是另一个队列的镜像节点。
4.使用操作都是主节点完成,然后同步给镜像节点。
5.主节点宕机后,镜像节点会替代成新的主节点。
4.仲裁队列
特征:
1.与镜像队列一样,都是主从模式,支持主从数据同步
2.使用非常简单,没有复杂的配置
3.主从同步基于Raft协议,强一致
SpringAMQP连接集群配置
spring:
rabbitmq:
username: hzj
password: 123
virtual-host: /
addresses: ip1:port1, ip2:port2
SpringAMQP创建仲裁队列
@Configuration
public class QuorumConfig {
@Bean
public Queue quorumQueue() {
return QueueBuilder.durable("quorum.queue")
.quorum()
.build();
}
}