一.初识MQ
1.消息的发送流程
RabbitMQ包括生产者,交换机,队列,消费者。
队列与交换机相绑定,消费者订阅队列。
生产者通过交换机名称发送消息到指定交换机。
然后由交换机将消息路由到指定队列,最后由消费者消费队列中的信息。
实现了消息的异步接收。
2.虚拟主机
通过虚拟主机将不同应用,不同功能之间的队列与交换机隔离起来。
能有效避免以下问题:消息误发,命名重复。也可以实现权限控制。
3.WorkQueue任务模型
WorkQueue任务模型支持多个消费者绑定同一个队列,大大提高了消息的处理速度
4.交换机类型
Fanout:广播,将消息发送到与交换机绑定的队列
Direct:订阅,基于路由key发送消息给订阅了该交换机的队列
Topic:通配符订阅,和订阅类似,支持路由key使用通配符
在通配符订阅中,*代表任意个词,.代表一个词
5.消息转换器
RabbitMQ默认的消息转换器由Java编写。
因为RabbitMQ传输的是二进制数据,所以需要将消息对象序列化为二进制数据。
然后由消费者将二进制数据反序列化为消息对象。
这样导致默认的消息转换器存在以下缺点。
跨语言性极差,消息庞大且难懂。
所以我们需要自定义消息转换器
二.MQ进阶
1.消息丢失的几种情况
<1>发送消息时丢失
生产者没有连接到MQ
生产者找不到交换机
交换机没有路由消息到相关队列
消息到达MQ时,网络异常
<2>MQ接收到消息后丢失
消息到达队列中,尚未消费,MQ便宕机
<3>消费者处理消息时丢失
消费者接收到消息,未来得及处理,服务宕机
消费者处理消息时抛异常
2.发送者的可靠性
<1>生产者重试机制
当生产者连接MQ超时时,进行重新连接。主要针对网络问题
这种连接是阻塞式的,在高性能业务中往往不会使用。
即便使用这种机制,也要合理设置重试时间以及尝试次数,或者采用异步线程。
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
<2>生产者确认机制
主要针对消息发送到mq后消息丢失的问题。
包括Publisher Confim机制和Publisher return 机制
当消息到达MQ,但是交换机路由消息失败时,会返回异常信息和ack
当临时消息到达交换机并路由到队列时,会返回ack
当持久消息到达交换机并路由到队列,并完成持久化时,会返回ack
其他情况均返回nack
<<1开启生产者确认机制
spring:
rabbitmq:
publisher-confirm-type: correlated # 开启publisher confirm机制,并设置confirm类型
publisher-returns: true # 开启publisher return机制
none:关闭生产者确认机制
simple:同步阻塞等待MQ回执
correlated:异步回调等待MQ回执
异步回调等待MQ回执:
生产者发送消息到交换机后,直接执行之后的操作,等到mq确认完返回结果给生产者然后回调执行下面的内容。
<<2定义ReturnCallback
ReturnCallback是失败时返回回调,每个RabbitTemplate只能配置一个ReturnCallback,因此我们可以在配置类中统一设置。
@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error("触发return callback,");
log.debug("exchange: {}", returned.getExchange());
log.debug("routingKey: {}", returned.getRoutingKey());
log.debug("message: {}", returned.getMessage());
log.debug("replyCode: {}", returned.getReplyCode());
log.debug("replyText: {}", returned.getReplyText());
}
});
}
}
<<3定义ConfirmCallback
确认回调,因为每个消息确认之后回调的逻辑都不相同,所以每个消息都应该由一个专门的ConfirmCallback,我们通过讲ConfirmCallback当作方法参数来实现这种逻辑。
@Test
void testPublisherConfirm() {
// 1.创建CorrelationData
CorrelationData cd = new CorrelationData();
// 2.给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm>() {
@Override
public void onFailure(Throwable ex) {
// 2.1.Future发生异常时的处理逻辑,基本不会触发
log.error("send message fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
log.debug("发送消息成功,收到 ack!");
}else{ // result.getReason(),String类型,返回nack时的异常描述
log.error("发送消息失败,收到 nack, reason : {}", result.getReason());
}
}
});
// 3.发送消息
rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd);
}
<<4总结
为了解决生产者发送消息时丢失的问题,我们有两种机制,一中是生产者重试机制,一种是生产者确认机制。
通常采用第二种方法,因为第一种无法适应高性能业务。
生产者确认机制主要通过Pulisher Confirm和Publisher Return 。
我们采用异步回调等待MQ回执的方法实现生产者确认机制。
当消息发送失败时,会将消息返回给mq,并返回异常信息,我们通过重写ReturnCallback返回回调实现。
当消息发送到MQ时,会根据结果返回ack和nack然后进行ComfirmCallback回调。
我们将ComfirmCallback存到方法参数中,因为每个消息的回调的具体逻辑并不一致
3.MQ的可靠性
<1>数据持久化
交换机持久化 队列持久化 消息持久化
<2>LazyQueue
Queue接收到消息之后直接将消息存到磁盘中,只有消费者需要消费消息时,才将消息加载到内存中(懒加载,例如微信聊天)
为什么要采用懒加载:
当MQ中接收消息的速度远远大于消费者处理消息的宿舍时,MQ很快就会达到内存上线,然后将内存存到磁盘中(PageOut),会消耗大量时间,而且在这个过程中不会再接收生产者发送的消息,就会出现消息丢失。
4.消费者的可靠性
<1>消费者确认机制
当mq发送到消息给消费者之后,并不能确保消费者一定正确处理了该消息,所以需要直到消费者的处理状态,决定是否重新发送消息给消费者。Spring AMQP提供了消费者确认机制。
ack 成功处理消息,mq删除消息
nack 消息处理失败,mq重新添加消息到队列中
reject 消息处理失败并拒绝该消息,mq删除消息
<2>失败重试机制
当消息一直处理失败,一直返回nack,那么消息就会一直去添加到队列中,形成恶性循环,降低性能。
所以在消费者出现异常时现在本地重试,设置最大重试次数,本地重试成功则返回ack,否则返回nack或者reject,默认返回reject
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000ms # 初识的失败等待时长为1秒
multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
<3>失败处理策略
这个策略是由MessageRecovery接口来定义的
,它有3个不同实现:
-
RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式 -
ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队 -
RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机
一般我们采用RepublishMessageRecover
失败后将消息投递到一个指定的,专门存放异常消息的队列,后续由人工集中处理。
@Configuration
@ConditionalOnProperty(name = "spring.rabbitmq.listener.simple.retry.enabled", havingValue = "true")
public class ErrorMessageConfig {
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
}
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
5.延迟消息
某些消息不能被正常消费,就会成为死信。
主要有以下几种情况:
消费者reject消息,消息无法正常回到队列。
消息TTL超时。
队列达到最大内存限制,消息无法被正常接收。
延迟消息的实现主要通过以下方式:
设置一个队列,该队列不存在消费者,当消息TTL超时时就会进入死信交换机,然后由死信交换机通过路由key发送消息到指定队列。
但是这种方式有以下缺点:
RabbitMQ采用队首检查的方式检查过期消息,如果过期消息不位于队首,就没有办法被立即检测到,导致延迟时间不一定准确。
所以我们通过DelayExchange插件实现延迟消息发送。
开启方式:
一.基于注解方式 dalayed = "true"
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "delay.queue", durable = "true"),
exchange = @Exchange(name = "delay.direct", delayed = "true"),
key = "delay"
))
public void listenDelayMessage(String msg){
log.info("接收到delay.queue的延迟消息:{}", msg);
}
二.基于Bean
@Bean
public DirectExchange delayExchange(){
return ExchangeBuilder
.directExchange("delay.direct")
.delayed()
.durable(true)
.build();
}