RabbitMq 消息发送确认(可靠生产和推送确认)

此文档只是本人在项目中碰到的一些问题而产生的个人相关总结,实际上的消息确认机制可以做得更多(比如分布式事务等,但此处不做阐述)。

一.消息发送确认是什么:

是RabbitMq确认消息是否成功投递到交换机或者队列中的一种机制。

有两种确认方式:
1.确认消息是否到达交换机中。(Producer ----->  Exchange) 		
		   实现RabbitTemplate类的一个内部接口ConfirmCallback,
		   这个接口的作用是生产者把消息发送到交换机的结果回调。
/**
    * A callback for publisher confirmations.
    *
    */
   @FunctionalInterface
   public interface ConfirmCallback {

   	/**
   	 * Confirmation callback.
   	 * @param correlationData correlation data for the callback.
   	 * @param ack true for ack, false for nack
   	 * @param cause An optional cause, for nack, when available, otherwise null.
   	 */
   参数说明:
   CorrelationData correlationData:可以封装业务ID信息,需要在发送消息时传入此参数,
   									这里才能接收到,否则是null

  boolean ack:消息发送的结果状态,发送成功是true,失败是false

  String cause:发送失败的描述信息,如果发送成功是null。
  
   	void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause);

   }
2.确认消息是否从交换机转发到了具体队列中。(Exchange----->  Queue)
   实现RabbitTemplate类的内部接口ReturnCallback
   这个接口的作用是消息从交换机到队列的结果回调。
/**
    * A callback for returned messages.
    *
    */
   @FunctionalInterface
   public interface ReturnCallback {

   	/**
   	 * Returned message callback.
   	 * @param message the returned message.
   	 * @param replyCode the reply code.
   	 * @param replyText the reply text.
   	 * @param exchange the exchange.
   	 * @param routingKey the routing key.
   	 */
   	参数说明:
   	Message message:发送的消息内容 + 发送消息的配置信息

  int replyCode:状态码,发送成功是200

  String replyText:发送消息失败的描述信息

  String exchange:消息使用的交换机

  String routingKey:消息使用的路由键
   	void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey);

   }

二.为什么使用消息确认:

对于使用RabbitMq推送数据,很多时候我们的关注点在于消息是否发送。而发送过程是否出现异常,发送是否真的成功则不太关心。 但是在处理项目的业务核心数据时,一条数据发送失败往往会导致后续业务也受到影响,且对于这种是否发送的数据的筛选也是非常的麻烦,需要来回校验数据,还有是否需要重发、到底是消息生产端出现了问题、还是消费端出现了问题等等…对于上述问题,都可使用RabbitMq的消息确认机制来进行处理。

三.消息确认能解决上述问题中的哪些问题

1.问题的定位范围缩小
能够确保生产端生产数据正常,消息未被正常处理时,只需要关注后续的消费即可。
2.生产失败的消息容易筛选
消息生产失败时,可直接在确认的机制中进行其他兜底处理:入库或者推入死信等,
		然后根据业务场景进行数据的重新生产推送。不再需要重新筛选数据
3.可及时发现问题,减少业务异常
每天只需查看是否有生产失败的数据,然后针对这些数据进行优化即可。
4.缺点:
队列性能会受到一定程度的影响,且业务复杂度也上升了。
		因此在使用的时候切忌大面积使用,只需针对非常核心的业务进行处理即可。

四.使用步骤

1.yml配置

# 本服务端口
server:
  port: xxxxx

# 本服务应用名称
spring:
  application:
    name: xxxx-xxxx
# Nacos配置地址
  cloud:
    nacos:
      discovery:
        server-addr: xxxxx

#RabbitMq配置
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: 此处写你的RabbitMq服务地址
    port: 此处写你的RabbitMq端口
    publisher-confirm-type: correlated  #交换机是否收到消息确认,该配置有三个选项,具体看下面描述
    publisher-returns: true     # 队列是否收到消息确认,默认false

    #publisher-confirm-type的三个属性
    #NONE值是禁用发布确认模式,是默认值
    #CORRELATED值是发布消息成功到交换器后会触发回调方法,如1示例
    #SIMPLE值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,
    #其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,
    #根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;

2.消息生产

package com.rabbitmq.nacos.producer.confirm;


import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.UUID;


/**
 * RabbitMq消息确认
 */

@Slf4j
@Component
public class RabbitMqPushMessageConfirm {

    @Resource
    private RabbitTemplate rabbitTemplate;

    @PostConstruct
    public void messageConfirm() {
        //1.确认消息是否到达交换机中。(Producer ----->  Exchange) 
        //通过实现 ConfirmCallback 接口,确认消息是否正确到达Exchange中
        //实现RabbitTemplate类的一个内部接口ConfirmCallback,
        //注意:所有推送到该交换机的消息都会进入该方法中,推送ack参数来判断该交换机是否确切收到消息
        rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
            //如果ack为true则代表Exchange已经收到消息
            if (!ack) {
                log.warn("消息发送到exchange失败,correlationData={}, cause={}", correlationData, cause);
                //解析ID存储数据库兜底
                String cid = correlationData.getId();

                //模拟消息确认失败的数据入库场景
//              failedMsgMapper.insertSelective(mqFailed);
            }

            log.info("消息{},交换机已收到", correlationData);
        });

		//2.确认消息是否从交换机转发到了具体队列中。(Exchange----->  Queue)
        //实现RabbitTemplate类的一个内部接口ReturnCallback,启动消息失败返回,
        //只要消息没有从交换机转发到对应的队列中去就会进入该方法
        // 消息从交换到队列失败,失败原因可能是路由键不存在,通道未绑定等等,一般都跟配置有关系。
        rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {

            //模拟消息确认失败的数据入库场景
//              failedMsgMapper.insertSelective(message);

            log.info("未找到路由为{}的队列", routingKey);
        });
    }

    /**
     * 说明:消息发送,使用该方法发送消息才可实现消息确认的效果。
     *
     * @param exchange   交换机
     * @param routingKey 路由Key
     */
    public void pushMessageByConfirm(String exchange, String routingKey) {
        // 模拟发送邮件
        String emailmsg = UUID.randomUUID().toString();

        log.info("消息发送确认开始:给交换机:" + exchange + ",路由键为:" + routingKey + ",发送消息:" + emailmsg);

        //CorrelationData可以携带一个字符串信息,协同需要生产的消息一起推送给交换机
        //在消息确认中,可以针对CorrelationData对象携带字符串进行相关的业务操作
        //比如携带字符串为:order=123456,则在消息确认时就能知道是order队列发送的消息。
        //如果ack为false确认失败,也可以将其存起来,用以和上游业务比对数据
        rabbitTemplate.convertAndSend(exchange, routingKey, emailmsg, new CorrelationData(emailmsg));
    }
}