今日完成记录
| Time | Plan | 完成情况 |
|---|---|---|
| 7:00 - 7:40 | 爬坡 | √ |
| 8:30 - 11:30 | Rabbit MQ | √ |
| 17:30 - 18:30 | 羽毛球 | √ |
RabbitMQ
消费者端如何保证可靠性?
- 消息投递过程出现网络故障
- 消费者接收到消息但是突然宕机未消费消息
- 消费者接收到消息后处理不当抛出异常
- 。。。
消费者确认机制
Consumer Acknowledgement:消费者处理消息结束应该给MQ发送一个回执,告知自己的消息处理状态:ack【成功处理消息,MQ从队列中删除消息】nack【消息处理失败,MQ需要重新推送消息】reject【消息处理失败并拒绝该消息,MQ从队列删除消息】
springAMQP提供了三种ACK处理方式:
- none:不处理,消息投递给消费者后直接返回ack【不安全,不建议】
- manual:手动处理,自己在业务代码中调用api发送ack或者reject【存在业务入侵但是更灵活】
- auto:自动处理,利用aop自动对业务代码进行增强,正常执行则返回ack,出现异常则根据异常类型处理【业务异常返回nack, 消息处理或者校验异常返回reject】
返回reject常见异常:MessageConversionException、MethodArgumentNotValidException、MethodArgumentTypeMissmatchException、NoSuchMethodException、ClassCastException
基本上就是消息校验异常以及不匹配处理方法或者参数的异常
通过如下配置可以设置ack处理方法:
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none # 不做处理
1、测试none处理方式,修改消费者端代码,使其抛出触发reject的异常。在抛出异常前打断点,并观察发现rabbitmq的客户端发现消息在发送到消费者则触发了自动ack并且删除了消息 ,触发异常后客户端并没有做任何处理。

2、修改acknowledge-mode为auto,再观察发现阻塞异常触发前消息处于uack状态,但同时观察到收到了一个manual ack。

(1)当代码继续执行,抛出MessageConversionException,会向MQ发送reject,删除消息。
这里发生了一个有意思的现象,因为我消息阻塞了太久触发了MQ消息重新投递,因此又出现了一个manual ack以及交替出现的ready和unack。
(2)当抛出异常是RuntimeException,可以观察到unack一直是1,且一直尝试重新投递。(重新投递没有触发那个自动的manual ack)

这里留两个小问题:为什么会自动发送了一个manual ack?这个重传是否是超时重传还是什么其他机制?
3、设置acknowledge-mode为manual,修改消费者端代码手动调用api返回消息回执
@RabbitListener(bindings = @QueueBinding(
key="*.top",
value = @Queue(value="df.topic.queue1"),
exchange = @Exchange(value = "df.topic1", type = ExchangeTypes.TOPIC)
))
public void listenDirectQueue1(Object msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
System.out.println("这是第" + (cnt++) + "条消息");
channel.basicAck(deliveryTag, false);
}
(1)测试ack
首先是接着2的调试代码继续调试【也就是此时消息队列中有一个消息没有被接收】,所以启动测试代码后这个消息会被重新投递,消息被消费者接收后手动回复确定,整个过程如下图

接下来重新投递一条消息观察正常的手动ack全过程,图中上面的图蓝色线(unacked)被红色线遮挡,它们其实是同样的走势。也就是当消息成功投递到消费者,会触发一次自动的ack(Deliver manual ack),但是消息处于uack,等到业务代码完成手动进行ack后该消息被ack并且删除。

(2)测试nack
@RabbitListener(bindings = @QueueBinding(
key="*.top",
value = @Queue(value="df.topic.queue1"),
exchange = @Exchange(value = "df.topic1", type = ExchangeTypes.TOPIC)
))
public void listenDirectQueue1(Object msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
try {
System.out.println("这是第" + (cnt++) + "条消息");
throw new BusinessException();
// channel.basicAck(deliveryTag, false);
}catch (BusinessException e){
// nack且重新入队 重新推送
channel.basicNack(deliveryTag, false, true);
}catch (MessageConversionException e){
// reject 并且不重新入队
channel.basicReject(deliveryTag, false);
}
}
上面的代码抛出了自定义的业务异常,这个异常会被捕获并且返回nack,然后重新推送,如下图

(3)测试reject
@RabbitListener(bindings = @QueueBinding(
key="*.top",
value = @Queue(value="df.topic.queue1"),
exchange = @Exchange(value = "df.topic1", type = ExchangeTypes.TOPIC)
))
public void listenDirectQueue1(Object msg, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag) throws IOException {
try {
System.out.println("这是第" + (cnt++) + "条消息");
throw new MessageConversionException("just a test for msg reject");
// channel.basicAck(deliveryTag, false);
}catch (MessageConversionException e){
// reject 并且不重新入队
channel.basicReject(deliveryTag, false);
}
}
这里抛出MessageConversionException,捕获后手动返回拒绝并且不重新投递,过程如下

**总结:**实际上SpringAMQP只是提供了三个接口basicAck、basicNack、basicReject,这三个接口何时触发,基于何种规则触发都是可以自定义的,上面的三个实现是基本与acknowledge-mode: auto一样的逻辑:业务异常nack且重新投递、消息异常reject且不重新投递、正常接收和消费则ack
消息失败重试机制
当消费者出现异常后,消息会不段重新入队再重新发送给消费者进行消费,一直反复直到消息处理成功。极端情况下,一直不成功就会导致消息无限循环,MQ的消息处理飙升,带来不必要的压力。
为了应对这种方法,SpringAMQP提供了一个本地失败重试机制,修改application.yaml设置相关配置
spring:
rabbitmq:
listener:
simple:
retry:
enable: true
initial-interval: 1000ms
multiplier: 1
max-attempts: 3
stateless: true # true 无状态、 false 有状态 如果业务中包含了事务,则应该是有状态的
这些设置很多和template的设置是一样的,设置完毕后可以设置实验代码,用生产者生产一条消息,然后消费者消费的时候抛出异常,观察是否进行了三次重试,并且观察最后的消息是什么状态
@RabbitListener(bindings = @QueueBinding(
key="*.top",
value = @Queue(value="df.topic.queue1"),
exchange = @Exchange(value = "df.topic1", type = ExchangeTypes.TOPIC)
))
public void listenDirectQueue1(Object msg) throws IOException {
System.out.println("这是第" + (cnt++) + "条消息");
throw new RuntimeException();
}
实验结果:控制台打印:
这是第0条消息
这是第1条消息
这是第2条消息
06-09 11:16:13:158 WARN 4700 --- [ntContainer#1-1] o.s.a.r.r.RejectAndDontRequeueRecoverer : Retries exhausted for message (Body:'"this is a msg to test confirm"' MessageProperties [headers={spring_listener_return_correlation=68642566-8a32-4ca2-9f9a-1e147383c99a, spring_returned_message_correlation=4b630e87-7fd4-494b-8f72-004dfad5a156, __TypeId__=java.lang.String}, contentType=application/json, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=df.topic1, receivedRoutingKey=a.top, deliveryTag=1, consumerTag=amq.ctag-fOEiXuBR7rn3NriHiSMZ9w, consumerQueue=df.topic.queue1])
org.springframework.amqp.rabbit.support.ListenerExecutionFailedException: Listener method 'public void com.itheima.consumer.listeners.MyMQListener.listenDirectQueue1(java.lang.Object) throws java.io.IOException' threw exception
at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:272) ~[spring-rabbit-2.4.12.jar:2.4.12]
at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.invokeHandlerAndProcessResult(MessagingMessageListenerAdapter.java:209) ~[spring-rabbit-2.4.12.jar:2.4.12]
at org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:148) ~[spring-rabbit-2.4.12.jar:2.4.12]
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:1674) ~[spring-rabbit-2.4.12.jar:2.4.12]
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.actualInvokeListener(AbstractMessageListenerContainer.java:1593) ~[
可见一共尝试了三次,前两次在捕获异常直接就本地重试了,而最后一次触发异常直接抛出了,观察MQ页面发现消息首先触发了一个deliver ack,然后三次后消息被丢弃了(ready、unacked都是0)。

失败处理措施
上面设置中失败达到最大次数消息就会被丢弃,这对于可靠性要求高的业务场景不合适,因此Spring允许自定义重试次数耗尽后的处理策略,通过MessageRecoverer接口定义,有三个实现:
RejectAndDontRequeueRecoverer:直接reject并且丢弃【默认】ImmediateRequeueMessageRecoverer:返回nack并且重新入队RepublishMessageRecoverer:将失败的消息投递到指定交换机【推荐】
建议用第三个实现,可以使用一个专门的队列存放异常消息 后续人工处理
上面提到的两个小问题
为什么会触发一次自动的Deliver(Manual ACK)
原来这个Deliver(Manual ACK)是一个投递事件ACK,当消息进入消息队列未被消费,其状态为ready,当其被投递到消费者,状态会更新为unacked,如果被成功消费并且确认,则会被删除。
当首次投递,则会触发一个投递事件(ready变为unacked)
当消息被重新投递,不会再触发投递事件
这是因为:
- 性能优化:重复发送投递事件会导致网络带宽浪费、Broker的CPU浪费、监控系统负载
- 语义精确性:RabbitMQ的事件新系统旨在“报告状态变化的边界,而非状态本身”,重复投递的状态变化是首次投递的重复,因此没有必要重复报告
- 避免误导性监控:重复报告投递事件会导致消息计数错误,无法区分实际新消息以及重新投递消息
实验中消费者端长时间没有ack消息,MQ端的自动重传是怎么回事?
Rabbit MQ的消息重发机制:
触发条件:
- 消息超时未ack:如果消息在消费者端处理时间超过了预设的TTL(time-to-live),或者未配置TTL但是长时间阻塞,Broker会认为消息处理失败并重新入队
- 消费者连接断开:若消费者因为阻塞崩溃或者连接断开,所有未ACK的消息会重新投递给其他消费者
重试策略:可以通过配置服务端的x-message-ttl【队列的属性,在队列中存放的时间不能超过】设置消息过期时间,超时后转入死信队列(DLQ,Dead Letter Queue),或者通过指数退避策略控制重试间隔
什么是死信队列 DLQ Dead Letter Queue
死信队列是用于存储“无法被正常消费”的信息的特殊队列。当消息满足某些条件的时候(被拒绝、超时未处理、队列达到最大长度等),RabbitMQ会自动将其重新路由到指定的死信队列,而不是直接丢弃,可以实现消息的异常处理、重试机制
消息进入死信队列的情况:
- 消息被消费者拒绝 nack 、 reject
- 消息过期TTL超时【消息设置了TTL 未消费完成前过期了;队列设置了x-message-ttl,消息在队列中停留超时了】
- 队列达到了最大长度【队列设置了x-max-length或者x-max-length-bytes】超出限制的消息会被送到死信队列
队列被删除或者无法路由

854

被折叠的 条评论
为什么被折叠?



