RabbitMQ 结合业务实现消息确认

唠个嗑

网络上搜罗了多次想知道 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


    org.springframework.boot
    spring-boot-starter-parent
    2.2.5.RELEASE
     

    

    org.springframework.boot
    spring-boot-starter-amqp

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 结合业务实现消息确认)