1. 如何保证消息顺序消费
在RabbitMQ中,消息最终会保存在队列中,在同一个队列中,消息是顺序的,保持先进先出的原则,这个由Rabbitmq保证。而不同队列中的消息,RabbitMQ 是无法保证其顺序性。顺序消费主要是指消费者按照队列中消息出队的顺序消费,出现顺序错乱的场景主要有两种:
- 一个queue存在多个consumer去消费,这样就可能会造成顺序的错乱。虽然consumer从MQ里面读取消息是顺序的,但是每个consumer的执行时间是不固定的,无法保证先读到消息的consumer一定先完成操作,如果先消费的处理时间很长,后面消费的处理时间很快,这样就会出现先出队的消息要晚于后出队的消息执行,从而造成消息顺序错乱。
- 一个quque上只有一个consumer去消费,但是这个consumer是多线程异步处理,因此也不能保证这个consumer按顺序消费;
解决方案:
- 单一队列顺序消费:将所有需要按顺序处理的消息发送到同一个队列中,然后只使用一个消费者单线程去处理消费队列中的消息。这样可以确保消息按照发送的顺序被消费,只是会牺牲消息消费的吞吐量。
- 设置消息序号标识:根据队列中消息的顺序设置一个递增的序号,当消费者消费处理时判断当前消息的序号的前一位是否已存在消息的处理记录表中,不存在说明前面一个消息还未处理完,然后不断轮询记录表中的前一个序号的记录,直到查询到前一个序号的记录之后再执行当前消息的处理逻辑,执行完成后再将当前消息的序号信息存储到记录表中。
2. 如何实现消息延时消费
有些场景需要保证队列中的消息不能立即被消费,需要延长一段时间后再进行消费。正常来说,消息到达队列后并且队列没有消息堆积的情况,只要有消费者监听了这个队列并且消费者是正常运行的,那么消息就会立刻被消费掉。实现这种延时消费的方案有两种:
- 延时队列:RabbitMQ 本身不支持延时队列,但是我们可以通过 RabbitMQ 的插件 rabbitmq-delayed-message-exchange来发生延迟消息。
- 死信队列 + 消息过期:创建一个队列并设置好对应的死信队列,该队列不设置任何消费者。先将消息放到这个队列中,并设置消息过期时间,当消息过期后,就会转存到死信队列中,然后消费者只要监听死信队列就好。
3. 如何解决消息堆积问题
消息的堆积是指在消息队列中,当生产者以较快的速度发送消息,而消费者处理消息的速度较慢或者消费者挂了的时候,导致消息在队列中积累并达到队列的存储上限。解决方案有两种:
- 增加更多消费者,提高消费速度:通过水平扩展消费者的数量,提高消息的处理速度,从而减少消息在队列中的滞留时间。
- 扩大队列容积,提高堆积上限:如果要提升队列容积,只把消息保存在内存中显然是不行的。可以使用惰性队列,惰性队列接收到消息后直接存入磁盘而非内存,消费者要消费消息时才会从磁盘中读取并加载到内存。
4. 如何保证消息的可靠性
4.1 生产者丢失消息:生产者发送消息由于网络等原因并没有发送到RabbitMq,或者消息发送到RabbitMq,但是没有找到指定的交换机或者没有匹配到对应的消息队列时,这时就会造成消息的丢失。
解决方案:
- 消息确认机制:在生产者创建的channel上开启确认模式,并在channel上添加监听,通过回调channel.addConfirmListener()函数来创建一个ConfirmListener。如果Broker收到消息,则会给生产者返回一个应答结果,ConfirmListener监听到broker的应答结果,根据具体的应答结果对消息进行重新发送,或记录日志等后续处理。
- 消息返回机制:对于一些不可达的消息,broker会返回一个信号通知生产端,如果消息可达,则不会返回任何信号。通过在channel上添加channel.addReturnListener()函数来创建一个ReturnListener,用于监听不可达的消息,然后进行后续的处理。
4.2 broker消息中间件自身丢失消息:RabbitMq收到生产者的消息后还没有来得及持久化到磁盘,又或者创建队列没有持久化以及消息并没有设置为持久化,在Mq故障宕机后都会有消息丢失的情况。
解决方案:
- 做持久化设置:在创建队列以及发送消息的时候同时设置队列持久化和消息持久化。
4.3 consumer消费者丢失消息:消费者自动ack配置情况下,业务代码异常或者其他故障消息并没有处理完成也会自动ack,从而导致消息丢失。
解决方案:
- 设置手动确认:消费者在声明队列时,设置autoAck=false,RabbitMQ就会等待消费者显式返回ack信号后才从队列中移去消息。消费者成功处理消息后,手动调用void basicAck(long deliveryTag, boolean multiple)方法通知broker该消息已经消费成功。消息处理失败后,通过回调void basicNack(long deliveryTag, boolean multiple, boolean requeue)方法或者void basicReject(long deliveryTag, boolean requeue)方法通知broker该消息消费失败。
4.4 消费者手动确认过程中异常:消费端在消费消息过程中出现异常,还没能调用basicAck应答,此时消息将变成unacked状态,并且一直处于队列中。
解决方案:
- 进行异常捕获:对消费者业务处理逻辑进行异常捕获,然后在catch里面调用basicNack方法返回消费失败的ack信息给broker。再将消费失败的消息记录到日志或者数据库中,后续人工进行处理。
4.5 无效消息循环重入队列:如果消费端在消费消息过程中捕获异常,并进行basicNack应答,将消息重新放入队列中,可能会出现无效的消息循环入队列的问题。假设消息或者代码本身有bug,每次处理这个消息都会报异常,那消息将一直处于消费------>报异常------>重入队列------>继续消费------>报异常。。。的死循环过程。
解决方案:
- 禁止消息重入队:当捕获到异常的时候,调用basicNack方法时,通过设置requeue参数为false,消息不会重入队消费。再将消费失败的消息记录到日志或者数据库中,后续人工进行处理。
4.6 broker未接收到ack信息:如果消费端消费完后,调用basicAck应答过程中由于网络中断等原因,未能将ack信息发送到broker时,会导致队列中的消息一直处于unacked状态。
解决方案:
- 在声明队列的时候添加死信队列,当broker一直未收到消费者返回的ack确认信息的消息,那么RabbitMQ消息队列就不清楚这条消息到底有没有被消费成功,就会将这条消息存放到死信队列中。后续再由人工去进行处理。
5. 如何防止消息重复消费
如果消息已经被消费者消费了,只是在消费者返回ack确认信息的时候出现异常,导致ack信息未能发送到broker,从而导致RabbitMQ不清楚这条消息到底有没有被消费成功,消息队列可能会在消费者未确认消息时进行重试,导致消息被重复消费。
解决方案:
- 消费端在消费逻辑处理中做好幂等性,保证同一个消息不被消费者重复消费两次。实现消息幂等性的方案:生产者每次发送消息的时候会生成一个全局唯一的id放到信息中,每次消费消息之前根据这个全局id去查询db或者redis中是否存在该id的消息信息,如果有,则说明该消息已经消费过,直接返回不再做后续处理;如果没有,则说明该消息未被消费过,继续进行后续业务处理,处理成功之后再将该全局id插入到bd或者redis中。