RabbitMQ:消息發(fā)送確認(rèn) 與 消息接收確認(rèn)(ACK)

默認(rèn)情況下如果一個(gè) Message 被消費(fèi)者所正確接收則會(huì)被從 Queue 中移除

如果一個(gè) Queue 沒被任何消費(fèi)者訂閱,那么這個(gè) Queue 中的消息會(huì)被 Cache(緩存),當(dāng)有消費(fèi)者訂閱時(shí)則會(huì)立即發(fā)送,當(dāng) Message 被消費(fèi)者正確接收時(shí),就會(huì)被從 Queue 中移除

消息發(fā)送確認(rèn)

發(fā)送的消息怎么樣才算失敗或成功?如何確認(rèn)?

  • 當(dāng)消息無(wú)法路由到隊(duì)列時(shí),確認(rèn)消息路由失敗。消息成功路由時(shí),當(dāng)需要發(fā)送的隊(duì)列都發(fā)送成功后,進(jìn)行確認(rèn)消息,對(duì)于持久化隊(duì)列意味著寫入磁盤,對(duì)于鏡像隊(duì)列意味著所有鏡像接收成功

ConfirmCallback

  • 通過(guò)實(shí)現(xiàn) ConfirmCallback 接口,消息發(fā)送到 Broker 后觸發(fā)回調(diào),確認(rèn)消息是否到達(dá) Broker 服務(wù)器,也就是只確認(rèn)是否正確到達(dá) Exchange 中
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setConfirmCallback(this);            //指定 ConfirmCallback
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("消息唯一標(biāo)識(shí):"+correlationData);
        System.out.println("確認(rèn)結(jié)果:"+ack);
        System.out.println("失敗原因:"+cause);
    }
  • 還需要在配置文件添加配置
spring:
  rabbitmq:
    publisher-confirms: true 

ReturnCallback

  • 通過(guò)實(shí)現(xiàn) ReturnCallback 接口,啟動(dòng)消息失敗返回,比如路由不到隊(duì)列時(shí)觸發(fā)回調(diào)
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback{

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void init(){
        rabbitTemplate.setReturnCallback(this);             //指定 ReturnCallback
    }

    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("消息主體 message : "+message);
        System.out.println("消息主體 message : "+replyCode);
        System.out.println("描述:"+replyText);
        System.out.println("消息使用的交換器 exchange : "+exchange);
        System.out.println("消息使用的路由鍵 routing : "+routingKey);
    }
}
  • 還需要在配置文件添加配置
spring:
  rabbitmq:
    publisher-returns: true 

消息接收確認(rèn)

消息消費(fèi)者如何通知 Rabbit 消息消費(fèi)成功?

  • 消息通過(guò) ACK 確認(rèn)是否被正確接收,每個(gè) Message 都要被確認(rèn)(acknowledged),可以手動(dòng)去 ACK 或自動(dòng) ACK
  • 自動(dòng)確認(rèn)會(huì)在消息發(fā)送給消費(fèi)者后立即確認(rèn),但存在丟失消息的可能,如果消費(fèi)端消費(fèi)邏輯拋出異常,也就是消費(fèi)端沒有處理成功這條消息,那么就相當(dāng)于丟失了消息
  • 如果消息已經(jīng)被處理,但后續(xù)代碼拋出異常,使用 Spring 進(jìn)行管理的話消費(fèi)端業(yè)務(wù)邏輯會(huì)進(jìn)行回滾,這也同樣造成了實(shí)際意義的消息丟失
  • 如果手動(dòng)確認(rèn)則當(dāng)消費(fèi)者調(diào)用 ack、nack、reject 幾種方法進(jìn)行確認(rèn),手動(dòng)確認(rèn)可以在業(yè)務(wù)失敗后進(jìn)行一些操作,如果消息未被 ACK 則會(huì)發(fā)送到下一個(gè)消費(fèi)者
  • 如果某個(gè)服務(wù)忘記 ACK 了,則 RabbitMQ 不會(huì)再發(fā)送數(shù)據(jù)給它,因?yàn)?RabbitMQ 認(rèn)為該服務(wù)的處理能力有限
  • ACK 機(jī)制還可以起到限流作用,比如在接收到某條消息時(shí)休眠幾秒鐘
  • 消息確認(rèn)模式有:
    • AcknowledgeMode.NONE:自動(dòng)確認(rèn)
    • AcknowledgeMode.AUTO:根據(jù)情況確認(rèn)
    • AcknowledgeMode.MANUAL:手動(dòng)確認(rèn)

確認(rèn)消息(局部方法處理消息)

  • 默認(rèn)情況下消息消費(fèi)者是自動(dòng) ack (確認(rèn))消息的,如果要手動(dòng) ack(確認(rèn))則需要修改確認(rèn)模式為 manual
spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual
  • 或在 RabbitListenerContainerFactory 中進(jìn)行開啟手動(dòng) ack
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
    SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
    factory.setConnectionFactory(connectionFactory);
    factory.setMessageConverter(new Jackson2JsonMessageConverter());
    factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);             //開啟手動(dòng) ack
    return factory;
}
  • 確認(rèn)消息
@RabbitHandler
public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
    System.out.println(message);
    try {
        channel.basicAck(tag,false);            // 確認(rèn)消息
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 需要注意的 basicAck 方法需要傳遞兩個(gè)參數(shù)
    • deliveryTag(唯一標(biāo)識(shí) ID):當(dāng)一個(gè)消費(fèi)者向 RabbitMQ 注冊(cè)后,會(huì)建立起一個(gè) Channel ,RabbitMQ 會(huì)用 basic.deliver 方法向消費(fèi)者推送消息,這個(gè)方法攜帶了一個(gè) delivery tag, 它代表了 RabbitMQ 向該 Channel 投遞的這條消息的唯一標(biāo)識(shí) ID,是一個(gè)單調(diào)遞增的正整數(shù),delivery tag 的范圍僅限于 Channel
    • multiple:為了減少網(wǎng)絡(luò)流量,手動(dòng)確認(rèn)可以被批處理,當(dāng)該參數(shù)為 true 時(shí),則可以一次性確認(rèn) delivery_tag 小于等于傳入值的所有消息

手動(dòng)否認(rèn)、拒絕消息

  • 發(fā)送一個(gè) header 中包含 error 屬性的消息
hducA.png
  • 消費(fèi)者獲取消息時(shí)檢查到頭部包含 error 則 nack 消息
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
    System.out.println(message);
    if (map.get("error")!= null){
        System.out.println("錯(cuò)誤的消息");
        try {
            channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true);      //否認(rèn)消息
            return;
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    try {
        channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);            //確認(rèn)消息
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 此時(shí)控制臺(tái)重復(fù)打印,說(shuō)明該消息被 nack 后一直重新入隊(duì)列然后一直重新消費(fèi)
hello
錯(cuò)誤的消息
hello
錯(cuò)誤的消息
hello
錯(cuò)誤的消息
hello
錯(cuò)誤的消息
  • 也可以拒絕該消息,消息會(huì)被丟棄,不會(huì)重回隊(duì)列
channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false);        //拒絕消息

確認(rèn)消息(全局處理消息)

  • 自動(dòng)確認(rèn)涉及到一個(gè)問(wèn)題就是如果在處理消息的時(shí)候拋出異常,消息處理失敗,但是因?yàn)樽詣?dòng)確認(rèn)而導(dǎo)致 Rabbit 將該消息刪除了,造成消息丟失
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");                 // 監(jiān)聽的隊(duì)列
    container.setAcknowledgeMode(AcknowledgeMode.NONE);     // NONE 代表自動(dòng)確認(rèn)
    container.setMessageListener((MessageListener) message -> {         //消息監(jiān)聽處理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //相當(dāng)于自己的一些消費(fèi)邏輯拋錯(cuò)誤
        throw new NullPointerException("consumer fail");
    });
    return container;
}
  • 手動(dòng)確認(rèn)消息
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 監(jiān)聽的隊(duì)列
    container.setAcknowledgeMode(AcknowledgeMode.MANUAL);        // 手動(dòng)確認(rèn)
    container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> {      //消息處理
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        if(message.getMessageProperties().getHeaders().get("error") == null){
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息已經(jīng)確認(rèn)");
        }else {
            //channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
            System.out.println("消息拒絕");
        }

    });
    return container;
}
  • AcknowledgeMode 除了 NONE 和 MANUAL 之外還有 AUTO ,它會(huì)根據(jù)方法的執(zhí)行情況來(lái)決定是否確認(rèn)還是拒絕(是否重新入queue)
    • 如果消息成功被消費(fèi)(成功的意思是在消費(fèi)的過(guò)程中沒有拋出異常),則自動(dòng)確認(rèn)

    • 當(dāng)拋出 AmqpRejectAndDontRequeueException 異常的時(shí)候,則消息會(huì)被拒絕,且 requeue = false(不重新入隊(duì)列)

    • 當(dāng)拋出 ImmediateAcknowledgeAmqpException 異常,則消費(fèi)者會(huì)被確認(rèn)

    • 其他的異常,則消息會(huì)被拒絕,且 requeue = true(如果此時(shí)只有一個(gè)消費(fèi)者監(jiān)聽該隊(duì)列,則有發(fā)生死循環(huán)的風(fēng)險(xiǎn),多消費(fèi)端也會(huì)造成資源的極大浪費(fèi),這個(gè)在開發(fā)過(guò)程中一定要避免的)。可以通過(guò) setDefaultRequeueRejected(默認(rèn)是true)去設(shè)置

@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
    SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.setQueueNames("consumer_queue");              // 監(jiān)聽的隊(duì)列
    container.setAcknowledgeMode(AcknowledgeMode.AUTO);     // 根據(jù)情況確認(rèn)消息
    container.setMessageListener((MessageListener) (message) -> {
        System.out.println("====接收到消息=====");
        System.out.println(new String(message.getBody()));
        //拋出NullPointerException異常則重新入隊(duì)列
        //throw new NullPointerException("消息消費(fèi)失敗");
        //當(dāng)拋出的異常是AmqpRejectAndDontRequeueException異常的時(shí)候,則消息會(huì)被拒絕,且requeue=false
        //throw new AmqpRejectAndDontRequeueException("消息消費(fèi)失敗");
        //當(dāng)拋出ImmediateAcknowledgeAmqpException異常,則消費(fèi)者會(huì)被確認(rèn)
        throw new ImmediateAcknowledgeAmqpException("消息消費(fèi)失敗");
    });
    return container;
}

消息可靠總結(jié)

  • 持久化
    • exchange要持久化
    • queue要持久化
    • message要持久化
  • 消息確認(rèn)
    • 啟動(dòng)消費(fèi)返回(@ReturnList注解,生產(chǎn)者就可以知道哪些消息沒有發(fā)出去)
    • 生產(chǎn)者和Server(broker)之間的消息確認(rèn)
    • 消費(fèi)者和Server(broker)之間的消息確認(rèn)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容