美文网首页
RabbitMQ 结合业务实现消息确认

RabbitMQ 结合业务实现消息确认

作者: charmsongo | 来源:发表于2020-11-29 19:36 被阅读0次

    唠个嗑

    网络上搜罗了多次想知道 RabbitMQ 现实业务种怎么实现消息的可靠性的,但是大多都不太理想,站在各位大佬巨人的肩膀上研究了一段时间,我也整理了一套简单可行性的方案,包括消息异常处理。这篇文章想主要讲一些业务处理方案,项目中加入 RabbitMQ 中间件很简单,但是根据具体业务实现消息的可靠性,这个需要多加考虑。当然下面也会通过测试代码来分析,文末也会附上源码地址。

    1、准备

    1.1、环境准备

    之前博客上写过一篇编译安装的方法 地址,大家可以参考,因为 RabbitMQ 底层语言的原因可能稍微麻烦点,那就没有办法了吗?如果你是先搞测试,再在项目中使用的话,那可以使用 docker 安装,2 行代码,如下

    docker pull rabbitmq
    
    docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management
    

    1.2、理论准备

    RabbitMQ 的类型包括:direct、topic、fanout、headers、system(翻源码看到的)

    这里主要通过 topic 来分析,bindingkey 可以通过通配符 # 和 * 来匹配多个 路由键 (routingKey),
    bindingkey 是绑定交换机(exchange)和队列(queue)的, 生产者(publisher)发消息的时候会携带 routingKey、exchange 和 消息发送给 RabbitMQ,
    连接成功后实际是组件 exchange 接收了生产者的消息,然后通过 bindingkey 匹配 routingKey,决定送给哪个 queue,每个消费者都会有 queue,所以 queue接收到消息 后就可以确保消费者接可以收到消息了,最后消费者再消费。

    再详细的内容可以查看大佬 erlie 的总结 地址

    2、消息确认

    RabbitMQ 基础配置

    pom

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
        
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    

    application.yml

    spring:
      rabbitmq:
        host: 192.168.1.105
        port: 5672
        username: guest
        password: guest
        virtual-host: /
    
    # 交换机、队列和绑定键声明
    test:
      exchange: test.exchange
      one:
        queue: one.test.queue
        binding-key: one.test.key
    

    consumer

    @RabbitListener(bindings=@QueueBinding(
                    //配置交换器
                    exchange=@Exchange(value="${test.exchange}",type= ExchangeTypes.TOPIC),
                    //配置路由键
                    key="${test.one.binding-key}",
                    //配置队列名称
                    value=@Queue(value="${test.one.queue}",autoDelete="true")
    ))
    public void test(String msg) {
        log.info("test 收到的消息为:[{}]", msg);
        //业务代码...
    }
    

    publisher

    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Value("${test.exchange}")
    private String exchange;
    @Value("${test.one.binding-key}")
    private String routingKey;
    
    public void test() {
        rabbitTemplate.convertAndSend(exchange, routingKey, "test msg");
    }
    

    通过上面默认的配置基本上就可以使用 RabbitMQ 了,但是这不是本篇的重点.我们要知道消息发送和到消费的过程中出现问题怎么办?这就需要我们分段确认消息是否接收成功,如果失败了该如何处理,先想想可以怎么做。
    我们先捋一下思路,消息发送给 RabbitMQ ,如果连接 RabbitMQ 失败,则记录该消息,如果连接成功但是 exchange 接收失败则记录下该消息,如果 exchange 接收成功但是 queue 接收 exchange 的消息失败则记录下该消息,消息从生产者到 queue 有 3 个位置可能因网络抖动或其他原因出现问题,那我们在这三个位置记录下问题后,统一通过计划定时获取记录的消息并且重新发送,如果重发三次还没有成功则标记该记录为异常消息。

    2.1、生产者消息确认和回调

    这里分两步:

    1. 消息确认指的是 RabbitMQ(exchange) 确认接收到了消费者发送的消息
    2. 消息回调指的是 queue 接收 exchange 的消息失败,则回调告诉 RabbitMQ 失败的消息

    2.1.1、消息确认

    开启配置

    spring:
      rabbitmq:
        #publisher-confirms: true #已过时
        publisher-confirm-type: correlated #开启生产者消息确认
    

    还有另外 2 种模式:

    1. none 值是禁用发布确认模式,是默认值;
    2. simple 值经测试有两种效果,其一效果和 correlated 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms 或 waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是 waitForConfirmsOrDie 方法如果返回false则会关闭 channel,则接下来无法发送消息到 broker。

    当然还需新建一个类实现 RabbitTemplate.ConfirmCallback,重写方法 confirm,该方法有三个参数 correlationData、ack、cause,主要说下 ack,值为 true 表示 exchange 成功接收到消息,false 表示 exchange 接收消息失败,这里 2 种结果可以分别处理,比如 false 可以把接收失败的消息入库,然后通过定时器来处理,比较懂的同学现在可能就有疑问了,correlationData 只能得到 msgId,根本没有具体的消息,这里可以发挥你出色的想象力,可以通过对象封装得到,也可以通过存内存或者磁盘存储得到,方法总比困难多。

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
       String msgId = correlationData.getId();
       if (ack) {
           log.info("成功发送给 mq, msgId:[{}]", msgId);
       } else {
           log.error("发送给 mq 失败, msgId:[{}], 失败理由cause:[{}]", msgId, cause);
           //消息从生产者没有到 exchange,那存库
           saveToDB(msgId,...);
       }
    }
    

    这里还差一步,就是原生的 rabbitTemplate 怎么知道消息确认时使用刚建的类呢,注入即可
    rabbitTemplate.setConfirmCallback(刚建的类);如有疑问可看文末源码。

    2.1.2、消息回调

    spring:
      rabbitmq:
        publisher-returns: true #开启生产者消息回调
    

    同上,需要新建类并实现 RabbitTemplate.ReturnCallback,并且重写方法 returnedMessage, 最后需要注入如下内容

    rabbitTemplate.setReturnCallback(刚建的类);
    // 要想使 returnCallback 生效,必须设置为true
    rabbitTemplate.setMandatory(true);
    
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        String msgId = (String) message.getMessageProperties().getHeaders().get("spring_returned_message_correlation");
        String msg = new String(message.getBody());
        log.error("消息回调 msgId:[{}], msg:[{}] 不能被正确路由,replyCode:[{}], replyText:[{}], exchange:[{}], routingKey:[{}]", msgId, msg, replyCode, replyText, exchange, routingKey);
        //消息从 exchange 没有到 queue, 那存库
        saveToDB(msgId, exchange, routingKey, msg);
    }
    

    如果消息发送时走到了回调方法 returnedMessage 中,说明目前的消息有问题是需要处理的,同上,入库。定时器来处理。

    当然消息的发送方法 rabbitTemplate.convertAndSend() 会多一个参数 correlationData

    具体处理方法可以参考源码,这里只提供思路。

    2.2、消费者消息确认

    消费者消息确认是确认消费了队列中的消息,如果出现问题 RabbitMQ 会有重试机制,长时间失败则需要人工干预,这个和生产者的确认是先后关系,实际是没有关联关系的,说到这,说下我之前转的牛角尖,一直想寻找 exchange 如何确认消费者成功消费消息,但是无果,后来细想,RabbitMQ 应该设计的就是消费者和 queue 交互,没必要和 exchange 交互。如有大佬知道 exchange 如何 ack 消费者消费消息可以告诉小弟,不胜感激。
    圆规正转,上消费者代码

    spring:
      rabbitmq:
        listener:
          simple:
            acknowledge-mode: manual #开启消费者消息确认; none:自动确认、auto:根据情况确认
    
    @RabbitListener(bindings=@QueueBinding(
                        //配置交换器
                        exchange=@Exchange(value="${test.exchange}",type= ExchangeTypes.TOPIC),
                        //配置路由键
                        key="${test.one.binding-key}",
                        //配置队列名称
                        value=@Queue(value="${test.one.queue}",autoDelete="true")
    ))
        public void test(Message message, Channel channel) {
            String msg = new String(message.getBody());
            log.info("test 收到的消息为:[{}], msgId:[{}]", msg, message.getMessageProperties().getHeaders().get("spring_returned_message_correlation"));
            try {
                //业务处理
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } catch (Exception e) {
                e.printStackTrace();
                try {
                    channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            }
        }
    

    主要说 3 个方法:

    1、basicAck 是确认消息,需要传递两个参数

    1. deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限于 Channel
    2. multiple:为了减少网络流量,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息,false则只确认传入值等于 delivery_tag 的消息

    2、basicNack 是拒绝消息,可以拒绝多条,比 basicAck 多一个布尔值的参数,如果为 true,被 nack 后重新入队列然后重新消费消费;如果为 false 被 nack 就丢了。

    3、basicReject 只能拒绝一条消息,reject 后消息直接丢了。

    总结

    这里简单实现 RabbitMQ 消息可靠的方式是通过把消息发送时出现问题后直接入库,然后通过计划定时查询再重新发送给 RabbitMQ,如果 exchange 成功 ack 后则标记为重发成功,如果重发 3 次还是失败则标记异常,需要人工处理。

    讨论

    这种处理其实不算是最优的方案,技术上还可以有如下方案

    1. 生产者发消息时记录该条消息,并设该记录 1 分钟后生效,留 1 分钟给exchange 确认并直接标记该消息记录为成功,然后计划任务定时扫有效且未确认的消息,再发送给 RabbitMQ ,如果确认后则标记为成功,否则 3 次后标记为失败。
    2. 还和小编写的方案类似,记录失败的消息,但是定时任务获取到失败的消息后,直接调用消费者的服务处理,不通过 RabbitMQ, 但是这就需要维护消息和消费者服务的关系了,稍微复杂些。

    最后希望可以帮到看官,如果记录的不对烦请评论指出,一起讨论

    https://github.com/charmsongo/springboot-samples/tree/master/springboot-rabbitmq-songo

    相关文章

      网友评论

          本文标题:RabbitMQ 结合业务实现消息确认

          本文链接:https://www.haomeiwen.com/subject/oxgkwktx.html