今天绞尽脑汁地去防止RabbitMQ消息丢失

项目中有一个往ES里面写数据的逻辑,可是数据比较多往ES里面写得很慢,影响系统处理效率。最近在做的事就是将数据写进ES优化为异步动作,这样就不会影响项目处理接下来逻辑了。方案选型是用RabbitMQ消息队列做异步,生产环境中的这些数据是很重要的,是不允许丢失的,所以今天也是绞尽脑汁地去防止消息丢失,这篇博客总结一下。

要想解决消息丢失问题,首先得了解消息为什么会丢失。RabbitMQ是基于AMQP协议,高级消息协议嘛,设计上就和其他MQ产品不一样,关于AMQP协议的设计画了一个简单的图:

 AMQP协议高级就高级在生产者不是直接把消息放进队列让消费者消费的,中间还要经过一个消息交换机Exchange。

RabbitMQ是Erlang开发的,所以RabbitMQ依赖于Erlang毋庸置疑,其实RabbitMQ还依赖其他两个东西,截一张官网上的图:

logrotate是日志相关的我们不管,而这个socat是一个多功能的网络工具,解释起来就是两个独立数据通道之间的双向数据传输的继电器,一般linux的系统会自带socat的,所以我们安装RabbitMQ的时候一般不会去关注这个东西,只需要装Erlang就行了。这解释了消息在RabbitMQ中有一个被传输的过程,也就是上面那张图。消息传输的过程就是这样,有传输就有丢失,理论上每一步传输都是有可能消息丢失的。接下来就是针对每一步出现的消息丢失总结对应的解决方案。

1.生产者  ---  消息交换机

生产者把消息给出去了,交换机Exchange没收到,针对这个,RabbitMQ有一个ConfirmCallback机制,就是一个确认回调函数。这个回调函数可以自定义RabbitTemplate的时候去设置,如图:

意思就是生产者把消息发出去后,就会回调这个ConfirmCallback, 里面有三个参数correlationData、ack和cause,ack是一个布尔值,表示Exchange有没有成功收到消息,如果ack为false可以通过cause看失败原因,至于这个correlationData是可以和被发送消息绑定的,就理解为消息的关联数据,可以看一下这个CorrelationData类:

这个CorrelationData属性,既然是和被发送消息绑定的,那它的id也可以用来标识被发送消息。也就是说发送消息前可以定义CorrelationData的id,在ConfirmCallback回调函数中就可以取出来,用来标识哪条消息发送失败了哪条消息发送成功了,如下图:

这是发送的时候定义了CorrelationData的id,在上图中的ConfirmCallback回调函数中也写了:

这样,处理生产者到交换机Exchange的消息丢失的思路就很清晰了。

生产者既然要发送消息,可以先在数据库里存一份消息Msg,发送Msg的时候定义Msg的绑定数据CorrelationData,并将CorrelationData的id初始化为表中Msg的id。数据发出去后,通过ConfirmCallback回调函数的ack参数判断Exchange有没有成功接收到消息,还可以通过correlationData的id定位到具体是哪条消息,这样这条消息发送成功或者不成功就可以去执行相应的逻辑。比如如果发送成功了,就把数据库中这条消息的状态修改为已发送,如果发送失败了就重发,类似这样。

需要注意的是要想使用这个ConfirmCallback机制,需要在配置文件中配置publisher-confirm-type属性为correlated。如图:

2.消息交换机  ---  队列

生成者成功把消息给到Exchange了,但是Exchange把消息分发给消息队列Queue的时候失败了,比如路由键没有匹配到等原因,针对这个,RabbitMQ也有一个返回回调函数,叫ReturnCallback机制。和ConfirmCallback一样也是可以自定义RabbitTemplate的时候去设置,如图:

ReturnCallback和ConfirmCallback有一点区别,ConfirmCallback是生产者只要把消息给出去了不管成不成功都会回调,但是ReturnCallback是Exchange交换机把消息给队列Queue后失败才会回调,如果成功就不会回调了。

这个回调函数参数比较多,但是我们一般需要的只有message,通过这个message我们可以拿到失败的消息内容和消息id。

处理这种Exchange到Queue的消息丢失思路也很清晰,如果ReturnCallback函数被回调了,我们可以通过message拿到消息,说明这条消息发送失败了,那接下来就走重发逻辑就好了。

需要注意的是使用这个ReturnCallback需要配置mandator为true,意思就是Exchange没有找到Queue的时候不要丢弃消息,如果这个mandator为false,Exchange没有找到Queue就会丢弃消息,也就不会触发ReturnCallback回调函数了。还要设置publisher-returns为true,这是开启ReturnCallback机制,如下图:

 3.队列  ---  消费者

队列到消费者的消息丢失相对来说比较复杂,因为队列是MQ的,消费者可能是另外一个服务,两个进程之间数据传输,消息丢失的情况比较多,可以分为三种:

一是队列自己把数据搞丢了,比如RabbitMQ挂了重启这种。

二是数据传输过程中消息搞丢了,比如没有找到消费者。

三是消息成功的给消费者了,消费者业务逻辑处理消息的时候自己服务来个异常,把数据弄丢了。这里不要钻牛角尖说消费者可以先存一份数据啊,不可能的,存消息也算消费者的逻辑,那如果存数据的时候异常了呢?

针对第一种情况,RabbitMQ是默认把队列初始化为持久化队列,可以看一下怎么初始化队列的:

这是我们初始化一个队列的简单写法,这里只给了队列名字,看一下 Queue怎么初始化的:

这里可以看到durable是默认为true的,即默认为持久化队列。我们也可以用QueueBuilder去初始化持久化队列与非持久化队列,即QueueBuilder的durable()nonDurable()方法,和如图:

队列被持久化后,在Rabbitmq崩溃的情况下,就队列本身保存下来,消息会被持久化到磁盘,重启后队列还在。接下来我们要将消息也保存下来,即消息的持久化。

针对第二三种情况,RabbitMQ也有一个ACK机制,这个ACK机制和刚刚说的Confirmcallback里面的ack是两码事。这个ACK机制是RabbitMQ把消息发给消费者后,如果消费者接收到了就回一个ACK,如果没有接收到或者接收有异常就回一个Nack,当RabbitMQ收到ACK后才会从队列中把这条消息删除,收到Nack就会去试着重发。

这个听上去不错,但可惜的是RabbitMQ的ACK机制默认是自动确认,就是说MQ只要能找到消费者,消费者只要收到消息了就立马回一个ACK,MQ不会去管消费者业务处理的怎么样,消息给你你收到了就不管你死活了,自动确认的ACK机制只能解决上面说的第二种情况。

那第三种情况怎么办呢?这就需要把这个ACK机制改为手动确认。配置如下:

意思就是说由消费者来手动确认消息有没有收到,这个就很nice了啊,消费者收到消息后先不忙着确认ACK,先拿消息处理自己的业务,自己业务逻辑处理完了,这条消息对自己没用了,再不慌不忙地回MQ一个ACK。如果自己在处理业务逻辑的过程中出现了异常,那就立马回MQ一个Nack,MQ就会重发这条消息。如图:

task()方法为直接的业务逻辑,处理成功了就回ACK,处理失败了就回Nack。

上面那张可以看到,回ACK或者Nack的时候,第二个参数表示是否批量确认,其实第一个参数tag可以理解为这条消息在队列中的index,批量确认消息就是确认队列中index小于当前消息index的所有消息,意思就是把旧消息一起确认掉,这里一般是false的,干嘛要去确认旧消息呢,万一人家还没有被消费呢咋办。

图里还可以看到回Nack的时候还有第三个参数,这个参数是true就是让MQ重发这条消息,false就是告诉MQ也别重发了,直接扔进死信队列吧,这就涉及到了避免消息丢失的另外一个点,即死信队列Dead letter Queue。

有三种消息会被放进死信队列:

(1).消息被消费者拒绝且告知MQ将该消息放入死信队列,这就是刚说的,消费者回了一个Nack,而且第三个参数为false。

(2).消息在队列中的存活时间过期,即消息可以设置TTL,TTL超时就会被放进死信队列。消息没人来消费也被老占着队列里的位置嘛。

(3).队列满了,新进来的消息会被直接放进死信队列。

有死信队列还得有一个专门的死信交换机,其实死信队列和死信交换机也是需要我们去初始化的,人家也是普普通通的Queue和Exchange,如下:

只不过有了死信队列和死信交换机后,初始化其他队列的时候就可以设置死信队列和死信交换机这两个属性,如图:

也就是说死信队列和死信交换机作为属性值被绑定到了普通工作队列上。

这个时候重启服务可能会报下面这个错:

这是因为服务器上同名Queue已经存在,启动的时候视图去设置x-dead-letter-exchange这个参数,而RabbitMQ默认是不支持修改队列属性的,要设置的参数值和服务器上现有的不一样就报这样的错。要么去控制台把这个队列删了再重启服务,要么直接改个名字,就好了。

有了死信队列,我们可以专门去监听这个死信队列,使得进入死信队列的消息也可以被处理,这也算是一种消息补偿机制。

总结一下,通过Confirmcallback、Returncallback、ACK/Nack机制以及死信队列这四个点,可以很大程度上保证消息不丢失,只能说很大程度,要求再高一点就要弄集群镜像了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值