RabbitMQ: message loss | message duplication | cause of message backlog + solution + usage experience that cannot be learned online

foreword

First of all, the most commonly used in the enterprise is actually neither RocketMQ nor Kafka, but RabbitMQ.

RocketMQ is very powerful, but it is mainly a message queue open sourced by Alibaba to promote its own cloud products. In fact, there are not as many small and medium-sized enterprises using RocketMQ as expected.

The deep-seated reason is that Rabbit was popularized earlier in small and medium-sized enterprises, and it has been tested for a longer time. It is easy to generate "repeat customers". Most of the talents who grew up with RabbitMQ have now become the backbone of the enterprise. The probability of favoring RabbitMQ is even higher.

As for Kafka, it is mainly used in big data and log collection. Except for some companies that have specific needs, companies that have high requirements for message sending and receiving accuracy still use RabbitMQ as the first choice for enterprise-level message queues.

My own feeling after working for so many years is that RabbitMQ is enduring. Unless other message middleware has a different experience in the follow-up, RabbitMQ still has a higher share.

Therefore, for the small partners who are going to enter the software industry, I suggest that it is necessary to systematically learn RabbitMQ first, and then learn other message middleware to expand their horizons. Their principles are similar, and they can be used by analogy.


two concepts

The main method of RabbitMQ to avoid message loss is to use the message confirmation mechanism and the manual sign-in mechanism, so it is necessary to clarify these two concepts.

1. Message confirmation mechanism

Mainly the mechanism used by the producer to confirm whether the message was successfully consumed.

The configuration is as follows:

spring: 
    rabbitmq:
        address: 192.168.x.x:xxxx
        virtual-host: /
        username: guest
        password: guest
        connection-timeout: 5000
        publisher-confirms: true # 消息成功确认
        publisher-returns: true # 消息失败确认
        template: 
            mandatory: true # 手动签收机制

In this way, when you implement the methods of RabbitTemplate.ConfirmCallback and RabbitTemplate.ReturnCallback, you can record the message confirmation in a targeted manner, and then do further message sending compensation to achieve the goal of close to 100% delivery.

The pseudo code is as follows:

@Component
@Slf4j
public class RabbitMQSender implements RabbitTemplate.ConfirmCallback, 
RabbitTemplate.ReturnCallback {
    
    /**
     * 发送消息
     */
    public void sendOrder(Order order) {
        rabbitTemplate.setConfirmCallback(this);
        rabbitTemplate.setReturnCallback(this);
        
        // 发送消息
        rabbitTemplate.convertAndSend(xx, xx, order, xx);
    }
    
    
    /**
     * 成功接收后的回调
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {
    
        // 如果成功接收了,这里可以对日志表的消息收发状态做更新。
        // ....
        
    }
    
    
    /**
     * 失败后的回调
     */
    @Override
    public void returnedMessage(Message message, int i, String s, String s1, String s2) {
    
        // 如果失败了,这里可以对日志表的消息收发状态做更新,之后通过任务调度去补偿发送。
        // ....
        
    }
}

2. Message Signing Mechanism

The message of RabbitMQ is automatically signed, you can understand that the express has signed, then the status of the express will change from sent to signed. The only difference is that the express company will have a record of the logistics track, and after MQ signs, it will be removed from the queue. deleted.

In enterprise-level development, we basically turn on the manual sign-in method for RabbitMQ, which can effectively avoid the loss of messages.

前文中已经在生产者开启了手动签收机制,那么作为消费方,也要设置手动签收。

配置如下:

spring: 
    rabbitmq:
        address: 192.168.x.x:xxxx
        virtual-host: /
        username: guest
        password: guest
        connection-timeout: 5000
        listener: 
            simple: 
                concurrency: 5 # 并发数量
                max-concurrency: 10 # 最大并发数量
                acknowledge-mode: manual # 开启手动签收
                prefetch: 1 # 限制每次只消费一个(一个线程),上面配置5,也就是能一次接收5个

消费监听时,手动签收就一行代码,伪代码如下:

@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel, 
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
    
    // ....
    
    // 手动签收
    channel.basicAck(tag, false);
    
}

消息丢失

两个概念搞清楚后,就可以来学习消息丢失的问题和处理方案了。

1、出现原因

消息丢失的原因无非有三种:

1)、消息发出后,中途网络故障,服务器没收到;

2)、消息发出后,服务器收到了,还没持久化,服务器宕机;

3)、消息发出后,服务器收到了,消费方还未处理业务逻辑,服务却挂掉了,而消息也自动签收,等于啥也没干。

这三种情况,(1) 和 (2)是由于生产方未开启消息确认机制导致,(3)是由于消费方未开启手动签收机制导致。

2、解决方案

1)、生产方发送消息时,要try...catch,在catch中捕获异常,并将MQ发送的关键内容记录到日志表中,日志表中要有消息发送状态,若发送失败,由定时任务定期扫描重发并更新状态;

2)、生产方publisher必须要加入确认回调机制,确认成功发送并签收的消息,如果进入失败回调方法,就修改数据库消息的状态,等待定时任务重发;

3)、消费方要开启手动签收ACK机制,消费成功才将消息移除,失败或因异常情况而尚未处理,就重新入队。

其实这就是前面阐述两个概念时已经讲过的内容,也是接近100%消息投递的企业级方案之一,主要目的就是为了解决消息丢失的问题。


消息重复

1、出现原因

消息重复大体上有两种情况会出现:

1)、消息消费成功,事务已提交,签收时结果服务器宕机或网络原因导致签收失败,消息状态会由unack转变为ready,重新发送给其他消费方;

2)、消息消费失败,由于retry重试机制,重新入队又将消息发送出去。

2、解决方案

网上大体上能搜罗到的方法有三种:

1)、消费方业务接口做好幂等;

2)、消息日志表保存MQ发送时的唯一消息ID,消费方可以根据这个唯一ID进行判断避免消息重复;

3)、消费方的Message对象有个getRedelivered()方法返回Boolean,为TRUE就表示重复发送过来的。

我这里只推荐第一种,业务方法幂等这是最直接有效的方式,(2)还要和数据库产生交互,(3)有可能导致第一次消费失败但第二次消费成功的情况被砍掉。


消息积压

1、出现原因

消息积压出现的场景一般有两种:

1)、消费方的服务挂掉,导致一直无法消费消息;

2)、消费方的服务节点太少,导致消费能力不足,从而出现积压,这种情况极可能就是生产方的流量过大导致。

2、解决方案

1)、既然消费能力不足,那就扩展更多消费节点,提升消费能力;

2)、建立专门的队列消费服务,将消息批量取出并持久化,之后再慢慢消费。

(1)就是最直接的方式,也是消息积压最常用的解决方案,但有些企业考虑到服务器成本压力,会选择第(2)种方案进行迂回,先通过一个独立服务把要消费的消息存起来,比如存到数据库,之后再慢慢处理这些消息即可。


使用心得

这里单独讲一下本人在工作中使用RabbitMQ的一些心得,希望能有所帮助。

1)、消息丢失、消息重复、消息积压三个问题中,实际上主要解决的还是消息丢失,因为大部分公司遇不到消息积压的场景,而稍微有水准的公司核心业务都会解决幂等问题,所以几乎不存在消息重复的可能;

2)、消息丢失的最常见企业级方案之一就是定时任务补偿,因为不论是SOA还是微服务的架构,必然会有分布式任务调度的存在,自然也就成为MQ最直接的补偿方式,如果MQ一定要实现100%投递,这种是最普遍的方案。但我实际上不推荐中小企业使用该方案,因为凭空增加维护成本,而且没有一定规模的项目完全没必要,大家都小看了RabbitMQ本身的性能,比如我们公司,支撑一个三甲医院,也就是三台8核16G服务器的集群,上线至今3年毫无压力;

3)、不要迷信网上和培训机构讲解的生产者消息确认机制,也就是前面两个概念中讲到的ConfirmCallback和ReturnCallback,这种机制十分降低MQ性能,我们团队曾遇到过一次流量高峰期带来的MQ传输及消费性能大幅降低的情况,后来发现是消息确认机制导致,关闭后立马恢复正常,从此以后都不再使用这种机制,MQ运行十分顺畅。同时我们会建立后台管理实现人工补偿,通过识别业务状态判断消费方是否处理了业务逻辑,毕竟这种情况都是少数,性能和运维成本,在这一块我们选择了性能;

4)、我工作这些年使用RabbitMQ没见过自动签收方式,一定是开启手动签收;

5)、手动签收方式你在网上看到的教程几乎都是处理完业务逻辑之后再手动签收,但实际上这种用法是不科学的,在分布式的架构中,MQ用来解耦和转发是非常常见的,如果是支付业务,往往在回调通知中通过MQ转发到其他服务,其他服务如果业务处理不成功,那么手动签收也不执行,这个消息又会入队发给其他消费者,这样就可能在流量洪峰阶段因为偶然的业务处理失败造成堵塞,甚至标题所讲的三种问题同时出现,这样就会得不偿失。

不科学的用法:在处理完业务逻辑后再手动签收,否则不签收,就好比客人进店了你得买东西,否则不让走。

@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel, 
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
    
    // 处理业务
    doBusiness(order);
    
    // 手动签收
    channel.basicAck(tag, false);
    
}

科学的用法:不论业务逻辑是否处理成功,最终都要将消息手动签收,MQ的使命不是保证客人进店了必须消费,不消费就不让走,而是客人能进来就行,哪怕是随便看看也算任务完成。

@RabbitListener(xxx)
public void onOrderMessage(@Payload Order order, Channel channel, 
@Header(AmqpHeaders.DELIVERY_TAG) long tag) throws Exception {
    
    try {
        // 处理业务
        doBusiness(order);
    } catch(Exception ex) {
        // 记录日志,通过后台管理或其他方式人工处理失败的业务。
    } finally {
        // 手动签收
        channel.basicAck(tag, false);
    }
    
}

可能有人会问你这样不是和自动签收没区别吗,NO,你要知道如果自动签收,出现消息丢失你连记录日志的可能都没有。

另外,为什么一定要这么做,因为MQ是中间件,本身就是辅助工具,就是一个滴滴司机,保证给你送到顺便说个再见就行,没必要还下车给你搬东西。

如果强加给MQ过多压力,只会造成本身业务的畸形。我们使用MQ的目的就是解耦和转发,不再做多余的事情,保证MQ本身是流畅的、职责单一的即可。


总结

本篇主要讲了RabbitMQ的三种常见问题及解决方案,同时分享了一些作者本人工作中使用的心得,我想网上是很难找到的,如果哪一天用到了,不妨再打开看看,也许能避免一些生产环境可能出现的问题。

我总结下来就是三点:

1)、消息100%投递会增加运维成本,中小企业视情况使用,非必要不使用;

2)、消息确认机制影响性能,非必要不使用;

3)、消费者先保证消息能签收,业务处理失败可以人工补偿。

工作中怕的永远不是一个技术不会使用,而是遇到问题不知道有什么解决思路。



原创文章纯手打,觉得有一滴滴帮助就请举手之劳点个收藏吧~

持续分享工作中的真实经验和心得体会,喜欢的话就点个关注吧~

Guess you like

Origin juejin.im/post/7117842051286171655