RabbitMQ高级特性 - 消费者消息确认机制

文章目录

RabbitMQ 消息确认机制


背景

上图中可以看出,从生产者发送消息消费者接收到消息并正确处理,这些里路线都可能会出现问题,那么为了保证这些消息最后能被正确处理,RabbitMQ 就提供了消息确认机制.

消费者消息确认机制

概述

为了保证消息从 队列 到 消费者正确消费,那么就引入了消费者消息确认机制.

a)消费者在订阅队列时,可以指定 autoAck 参数,根据这个参数设置,消息确认机制分为以下两种(以下讲到的方法和参数来自于 RabbitMQ 原生的 SDK,非 Spring 提供).

  • 自动确认:当 autoAck = true 时,RabbitMQ 会自动把发送出去的消息置为确认,然后不管消费者是否真正的消费这些消息,都会从内存中删除.(适合对消息可靠性要求不高的场景).
  • 手动确认:当 autoAck = false 时,RabbitMQ 会等待消费者显示的调用 Basic.Ack 命令(波安排时间哦且确认消息),然后才会从 内存或磁盘 中删除消息.(适合对消息可靠性要求高的场景).

Ps:可靠性高了,性能也就下降了,所以请综合考虑.

b)对于 MQ队列 中的消息,在 MQ管理平台上可以看到以下两种类别:

Ready:队列已经准备好消息,随时准备发送给消费者 的消息数量(只要消费者来要,就立刻发送).

Unacked:消息已经发送给消费者,但是消费者没有返回消息确认 的消息数量(消息确认包括 ack肯定确认nack否定确认

手动确认(RabbitMQ 原生 SDK)

消费者在收到消息之后,可以选择确认,也可以选择拒绝或者跳过,RabbitMQ因此提供了不同的确认应答方式,消费者客户端可通过调用 channel 的相关方法实现.

a)肯定确认:消费者已经接收到消息,并且成功处理消息,可以将其丢弃了.

java 复制代码
Channel.basicAck(long deliveryTag, boolean multiple)
  • deliveryTag:消息的唯一标识(单调递增的 long),特点如下:
    • deliveryTag 是每个 Channel 通道独立维护,所以每个通道上的都是唯一的(生产者 和 Broker 建立一个 channel 会生成一个 deliverTag,消费者 和 Broker 建立一个 channel 会生成一个 deliverTag,这俩 deliverTag 是不同的).
    • 当消费者 ack确认 一条消息时,必须使用对应的通道上 deliveryTag 进行确认.
  • multiple:是否批量确认. 如果值为 true,那么就会一次性 ack确认 所有小于或等于指定的 deliveryTag 的消息,大大减少了网络开销
    • 假设 deliveryTag = 8,multiple = true:那么 deliveryTag <= 8 的消息都会被确认.
    • 假设 deliveryTag = 8,multiple = false:只确认 8.

Ps:deliveryTag 确保了消息传递的可靠性和顺序性.

b)否定确认(单个):用来拒绝这个消息. 被拒绝的消息如何处理,具体要看 requeue 参数.

java 复制代码
Channel.basicReject(long deliveryTag, boolean requeue)
  • requeue:标识拒绝后,这条消息如何处理.
    • requeue = true:消息会重新存入队列,将来会发送给下一个订阅的消费者.
    • requeue = false:消息会从队列中移除,因此不会发送给消费者.

c)否定确认(批量):Channel.basicReject 只能拒绝一条消息,如果要批量拒绝消息,就可以使用 Channel.basicNack.

java 复制代码
Channel.basicNack(long deliveryTag, boolean multiple, boolean requeue)

multiple:参数设置为 true 则表示拒绝 deliveryTag 编号之前所有未被当前消费者确认的消息.

d)MQ 的管理平台上也提供了几种确认方式.

手动确认(Spring-AMQP 封装 RabbitMQ SDK)

Spring-AMQP 对消息确认提供了三种策略:

kotlin 复制代码
public enum AcknowledgeMode {
    NONE,
    MANUAL,
    AUTO;
}

这里根 RabbitMQ 原生 SDK 是有些不同的.

AcknowledgeMode.NONE

不管消费者是否成功处理了消息,RabbitMQ 都会自动确认消息,然后从 队列 中移除消息.

a)配置手动确认

yml 复制代码
spring:
  application:
    name: rabbitmq
  rabbitmq:
    host: env-base
    port: 5672
    username: root
    password: 1111
    listener:
      simple:
        acknowledge-mode: none

b)生产者接口

kotlin 复制代码
@RestController
@RequestMapping("/mq")
class MQApi(
   val rabbitTemplate: RabbitTemplate
) {

    @RequestMapping("/ack")
    fun ack(): String {
        rabbitTemplate.convertAndSend(MQConst.ACK_EXCHANGE, MQConst.ACK_BINDING, "ack msg 1")
        return "ok"
    }

}

c)消费者

kotlin 复制代码
import com.cyk.rabbitmq.constants.MQConst
import com.rabbitmq.client.Channel //注意这里的依赖
import org.springframework.amqp.core.Message //注意这里的依赖
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
import java.nio.charset.Charset

@Component
class AckListener {

    @RabbitListener(queues = [MQConst.ACK_QUEUE])
    fun handMessage(
        message: Message,
        channel: Channel,
    ) {
        println("接收到消息: ${String(message.body, Charset.forName("UTF-8"))}, ${message.messageProperties.deliveryTag}")
        //业务处理...
        println("业务逻辑处理完成")
    }

}

d)效果演示

触发接口之后,回到 MQ 管理平台,可以看到队列中消息已经被删除.

AcknowledgeMode.AUTO(默认)

分为以下情况:

  • 消费者处理消息过程中没有抛出异常,则自动确认消息,然后从 队列 中移除消息.
  • 消费者处理消息过程中抛出异常,则不会确认消息,消息会重返队列,并且不断重试(MQ 管理平台中 Unacked +1).

a)配置文件

kotlin 复制代码
spring:
  application:
    name: rabbitmq
  rabbitmq:
    host: env-base
    port: 5672
    username: root
    password: 1111
    listener:
      simple:
        acknowledge-mode: auto

b)生产者接口

kotlin 复制代码
@RestController
@RequestMapping("/mq")
class MQApi(
   val rabbitTemplate: RabbitTemplate
) {

    @RequestMapping("/ack")
    fun ack(): String {
        rabbitTemplate.convertAndSend(MQConst.ACK_EXCHANGE, MQConst.ACK_BINDING, "ack msg 1")
        return "ok"
    }

}

c)消费者(正常处理消息)

kotlin 复制代码
import com.cyk.rabbitmq.constants.MQConst
import com.rabbitmq.client.Channel //注意这里的依赖
import org.springframework.amqp.core.Message //注意这里的依赖
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
import java.nio.charset.Charset

@Component
class AckListener {

    @RabbitListener(queues = [MQConst.ACK_QUEUE])
    fun handMessage(
        message: Message,
        channel: Channel,
    ) {
        println("接收到消息: ${String(message.body, Charset.forName("UTF-8"))}, ${message.messageProperties.deliveryTag}")
        //业务处理...
        println("业务逻辑处理完成")
    }

}

效果如下:

d)消费者(异常处理消息)

kotlin 复制代码
import com.cyk.rabbitmq.constants.MQConst
import com.rabbitmq.client.Channel //注意这里的依赖
import org.springframework.amqp.core.Message //注意这里的依赖
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
import java.nio.charset.Charset

@Component
class AckListener {

    @RabbitListener(queues = [MQConst.ACK_QUEUE])
    fun handMessage(
        message: Message,
        channel: Channel,
    ) {
        println("接收到消息: ${String(message.body, Charset.forName("UTF-8"))}, ${message.messageProperties.deliveryTag}")
        //业务处理...
        val a = 1 / 0
        println("业务逻辑处理完成")
    }

}

效果如下:

消息未被确认,会不断重返队列,进行重试,因此 IDEA 中会循环报错输出.

AcknowledgeMode.MANUAL

分为以下情况:

  • 消费者在处理完消息后显示调用 basicAck 方法 来确认消息,然后从 队列 中移除消息.
  • 消费者在处理完消息后显示调用 basicNack 方法 来否定确认消息,是否从队列中移除消息需要看 requeue 参数的值
    • requeue = true:重返队列,不断重试.
    • requeue = false:丢弃消息.
  • 消费者在处理完消息后什么都不做,则不会确认消息,消息会重返队列,并且不断重试(MQ 管理平台中 Unacked +1).

a)配置文件

kotlin 复制代码
spring:
  application:
    name: rabbitmq
  rabbitmq:
    host: env-base
    port: 5672
    username: root
    password: 1111
    listener:
      simple:
        acknowledge-mode: manual

b)生产者接口

kotlin 复制代码
@RestController
@RequestMapping("/mq")
class MQApi(
   val rabbitTemplate: RabbitTemplate
) {

    @RequestMapping("/ack")
    fun ack(): String {
        rabbitTemplate.convertAndSend(MQConst.ACK_EXCHANGE, MQConst.ACK_BINDING, "ack msg 1")
        return "ok"
    }

}

c)消费者(异常处理消息,requeue = true)

kotlin 复制代码
import com.cyk.rabbitmq.constants.MQConst
import com.rabbitmq.client.Channel //注意这里的依赖
import org.springframework.amqp.core.Message //注意这里的依赖
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
import java.nio.charset.Charset

@Component
class AckListener {

    @RabbitListener(queues = [MQConst.ACK_QUEUE])
    fun handMessage(
        message: Message,
        channel: Channel,
    ) {
        val deliveryTag = message.messageProperties.deliveryTag
        try {
            println("接收到消息: ${String(message.body, Charset.forName("UTF-8"))}, $deliveryTag")
            //业务处理...
            val a = 1 / 0
            println("业务逻辑处理完成")
            channel.basicAck(deliveryTag, false)
        } catch (e: Exception) {
            channel.basicNack(deliveryTag, false, true) //requeue: true
        }
    }

}

由于消息处理异常,发送 nack,并且 requeue = true,因此消息会重返队列,不断重试.

d)消费者(异常处理消息,requeue = false)

kotlin 复制代码
import com.cyk.rabbitmq.constants.MQConst
import com.rabbitmq.client.Channel //注意这里的依赖
import org.springframework.amqp.core.Message //注意这里的依赖
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
import java.nio.charset.Charset

@Component
class AckListener {

    @RabbitListener(queues = [MQConst.ACK_QUEUE])
    fun handMessage(
        message: Message,
        channel: Channel,
    ) {
        val deliveryTag = message.messageProperties.deliveryTag
        try {
            println("接收到消息: ${String(message.body, Charset.forName("UTF-8"))}, $deliveryTag")
            //业务处理...
            val a = 1 / 0
            println("业务逻辑处理完成")
            channel.basicAck(deliveryTag, false)
        } catch (e: Exception) {
            channel.basicNack(deliveryTag, false, false) //requeue: false
        }
    }

}

由于消息处理异常,发送 nack,并且 requeue = false,因此消息不会重返队列,消息被丢弃.

d)消费者(正常处理消息)

kotlin 复制代码
import com.cyk.rabbitmq.constants.MQConst
import com.rabbitmq.client.Channel //注意这里的依赖
import org.springframework.amqp.core.Message //注意这里的依赖
import org.springframework.amqp.rabbit.annotation.RabbitListener
import org.springframework.stereotype.Component
import java.nio.charset.Charset

@Component
class AckListener {

    @RabbitListener(queues = [MQConst.ACK_QUEUE])
    fun handMessage(
        message: Message,
        channel: Channel,
    ) {
        val deliveryTag = message.messageProperties.deliveryTag
        try {
            println("接收到消息: ${String(message.body, Charset.forName("UTF-8"))}, $deliveryTag")
            //业务处理...
            println("业务逻辑处理完成")
            channel.basicAck(deliveryTag, false)
        } catch (e: Exception) {
            channel.basicNack(deliveryTag, false, false) //requeue: false
        }
    }

}

消息被正常处理,返回 ack.

MANUAL 可能会引发的问题

如果这里捕获的不是 Exception 异常,那么消费者处理消息的时候,可能会引发一些不会被捕获的异常,就会导致没有返回 nack.

也就意味着,没有进行确认应答,那么 mq管理平台 上就会显示 Unacked 数值 +1.

Ps:具体还是需要根据业务场景而定

相关推荐
P.H. Infinity4 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
WX187021128736 小时前
在分布式光伏电站如何进行电能质量的治理?
分布式
不能再留遗憾了9 小时前
RabbitMQ 高级特性——消息分发
分布式·rabbitmq·ruby
茶馆大橘9 小时前
微服务系列六:分布式事务与seata
分布式·docker·微服务·nacos·seata·springcloud
材料苦逼不会梦到计算机白富美12 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang
想进大厂的小王12 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情12 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
许苑向上13 小时前
【零基础小白】 window环境下安装RabbitMQ
rabbitmq
ZHOU西口14 小时前
微服务实战系列之玩转Docker(十八)
分布式·docker·云原生·架构·数据安全·etcd·rbac
zmd-zk14 小时前
kafka+zookeeper的搭建
大数据·分布式·zookeeper·中间件·kafka