RabbitMQ应用问题 - 消息顺序性保证、消息积压问题

MQ 消息顺序性保证


概述

a)消息顺序性:消费者消费的消息的顺序 和 生产者发送消息的顺序是一致的.

例如 生产者 发送消息顺序是 msg1、msg2、msg3,那么消费者也需要按照 msg1、msg2、msg3 的顺序进行消费.

b)顺序不一致可能会导致哪些问题?

例如用户系统中,用户需要对昵称进行了两次修改,此时生产者发送两条消息:

  1. 消息1:修改 用户318 的昵称为 “白天”.
  2. 消息2:修改 用户318 的昵称为 “黑夜”.
    那么,按正常的逻辑来讲,用户318 的名称最后因该为 “黑夜”,但如果 消息1 是最后一个被消费者消费的消息,那么 用户318 的名称就变成了 “白天”.

原因分析

Note:以下场景成立的前提是,只能有一个生产者!因为生产者发送消息给 mq,中间都需要经过网络传输,而网络的不确定性是非常大的,因此无法保证多个生产者的消息谁先到 mq.

a)多个消费者:
多个消息会被不同的消费者并行处理,也就意味着有的消费者消费的快,有的消费者消费的慢,从而导致消息处理的顺序性无法保证.

b)网络波动:
网络波动可能会导致消费者消费完消息返回的 ack 丢失,从而使得 mq 以为消息发给消费者的中途丢失了,进而使得消息重新入队,这就意味着 如果队列中此时还有其他消息,那么这个重新入队的消息就会排在队列尾部,而头部的消息会被优先消费,导致顺序性问题.

实际上,也就意味着,只要触发了消息重新入队的操作,就会导致顺序性问题.

c)消息路由问题:
在复杂的路由场景中(例如大量应用 Topic 交换机),消息可能会根据 routingKey 被分发到不同的队列,使得无法保证全局的顺序性.

d)死信队列:
消息因为一些原因(例如被消费者返回 nack + requeue=false),然后放入死信队列,那么死信队列无论是网络传输,还是处理死信队列的消费者和普通队列的消费者并行处理,都会导致顺序不一致的情况.

解决方案

顺序性的保证分为 局部顺序性保证全局顺序性保证.
例如如下,假设消息入队的顺序为 msg1、msg2、msg3、msg4、msg5…
在这里插入图片描述
在这里插入图片描述
消息顺序性保证的常见策略:

Note:以下顺序性保证策略往往不是单独使用进行保证的,而是多种组合使用.

a)单队列,单消费者(全局顺序性)
最简单的方式就是使用单个队列,并由单个消费者进行消息. 对于消息在队列先进先出,这是 RabbitMQ 给我们保证的.

b)业务逻辑控制(全局顺序性)
例如给每个消息引入一个序号(类似 TCP 确认应答),序号 3 消费之前,要保证序号 2 被成功消费…

c)手动消息确认机制(局部顺序性)
消费者在处理完消息后,显式的发送确认,这样 RabbitMQ 才会移除并继续处理下一个消息.

Ps:在 RabbitMQ 中,当消费者接收到一条消息时,这条消息并不会立即从队列中删除。相反,消息会保持在队列中,直到 RabbitMQ 收到消费者发回的确认.

d)分区消费(局部顺序性)
单个消费者的吞吐量太低了,当需要多个消费者来提高处理速度时,可以使用分区消费. 也就是把一个队列分割成多个分区(例如根据订单系统,将 订单id 进行 hash 或者其他算法 -> 保证同样的订单 id,经过这个算法后,得到的队列名称是一致的(如果 同样的订单 id 一会跑到队列1,一会跑到队列2,就会导致多个消费并行消费,最终消费顺序不一致)),最后每个分区由一个消费者处理,保证每个分区内消息的顺序性.

Ps:RabbitMQ 本身没有实现分区消费

基于 spring-cloud-stream 实现分区消费

Note: https://docs.spring.io/spring-cloud-stream/reference/rabbit/rabbit_partitions.html

RabbitMQ 并没有实现分区消费,因此这里可以引入一些其他的机制来实现.

a)引入依赖

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2023.0.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-stream-binder-rabbit</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
    </dependencies>

b)配置文件如下:

spring:
  rabbitmq:
    host: env-base
    port: 5672
    username: root
    password: 1111
  cloud:
    stream:
      bindings: # bindings 表示消息通道绑定配置
        generate-out-0: # generate-out-0 是一个输出通信的名称,表示这是生成消息的第一个通道(还可能由类似 generate-out-1 的其他通道)
          destination: partitioned.destination # 消息发送的名称为 "partitioned.destination" 的目的地(目的地在这里就是 mq 消息队列).
          producer: # 生产者配置
            # partitioned: true
            partition-key-expression: headers['partitionKey'] # 表示消息应该发送到哪个分区(这个跟代码里配置的 header 有关)
            partition-count: 2 # 表示有两个分区(两个队列). 生产者会根据 "partition-key-expression" 计算的结果,将消息分配到这两个分区之一
            required-groups: # 配置消费组
              - myGroup

c)代码如下:

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

import java.util.Random;
import java.util.function.Supplier;

@SpringBootApplication
public class SpringCloudStreamMqApplication {

    private static final Random RANDOM = new Random(System.currentTimeMillis());

    private static final String[] data = new String[] {
            "abc1", "abc2", "abc3",
            "abc4",
    };

    public static void main(String[] args) {
        new SpringApplicationBuilder(SpringCloudStreamMqApplication.class)
                .web(WebApplicationType.NONE) //不运行其他 web 组件
                .run(args);
    }

    /**
     * 分区消息:
     * 方法返回一个函数,这个函数每次调用都会从 data 中随机选择一个字符串,
     * 生成一个带有分区键(partitionKey)的消息,并将这个消息返回.
     */
    @Bean
    public Supplier<Message<?>> generate() {
        return () -> {
            String value = data[RANDOM.nextInt(data.length)];
            System.out.println("Sending: " + value);
            return MessageBuilder.withPayload(value)
                    .setHeader("partitionKey", value)
                    .build();
        };
    }

}

d)效果演示:
在 mq 管理平台可以看到多出来了一个交换机 和 两个队列(分区)
在这里插入图片描述
在这里插入图片描述
在 partitioned.destination.myGroup-0 中获取消息,可以看到都是 “abc2” 和 “abc4”
在这里插入图片描述
在 partitioned.destination.myGroup-1 中获取消息,可以看到都是 “abc1” 和 “abc3”
在这里插入图片描述

消息挤压问题


概述

消息挤压:在消息队列中,待处理的消息数量超过了消费者的处理能力,导致消息在队列中不断堆积的现象.

原因分析

a)消息生产过快
在流量较大的情况下,生产者发送消息速率大于消费者消费消息速率.

b)消费者处理能力不足

  • 消费端业务复杂,耗时长.
  • 系统资源限制,例如 CPU、内存、磁盘I/O 限制消费者处理速度.
  • 消费者在处理消息时出现异常,导致消息无法被正确处理和确认.
  • 服务器端配置过低

c)网络问题
由于网络抖动,消费者没有及时反馈 ack/nack,导致消息不断重发.

d)消费者代码逻辑异常,引发重试
消费者配置了手动 ack + requeue= true,导致一旦由于消费者代码逻辑引发异常,就会造成消息不断重新入队,不断重试,进而导致消息积压.

解决方案

Note:实际工作中,更多的是处理消费者的效率

a)提高消费者效率:

  • 提高消费者的数量,比如新增机器.
  • 如果消费端业务分散耗时,可以考虑使用 CompletableFuture 实现多线程异步编排.
  • 设置 prefetchCount,当一个消费者阻塞时,消息转发到其他没有阻塞的消费者.
  • 消息引发异常时,考虑配置重试机制,或者转入死信队列.

b)限制生产者速率:

  • 使用限流工具,限制消息发送速率的上限.
  • 设置消息过期时间. 如果消息过期没有消费,可以配置死信队列,不仅避免消息丢失,还减少了主队列的压力.

在这里插入图片描述

<think>嗯,用户想解决RabbitMQ消息消费顺序问题。我需要先回忆一下RabbitMQ消息顺序处理相关的知识。根据之前看过的引用资料,消息顺序问题涉及发送、队列和消费三个环节。用户的问题主要集中在消费顺序上,所以需要重点看消费端的解决方案。 首先,引用[3]提到,如果consumer内部使用多线程消费,会导致顺序错误。这说明即使队列本身是顺序的,消费者处理的方式也会影响最终结果。所以解决方案可能需要限制消费者内部的处理方式,比如单线程处理或者内部排序。 然后引用[4]提供了几个方法:比如单队列单消费者,使用多个队列和分区键,或者消费者内部排序。比如,生产者根据订单ID将相同订单的消息发到同一个队列,这样每个队列只有一个消费者处理,就能保证顺序。另外,分区键的使用也是关键,比如根据消息的某个属路由到特定队列,确保相同属消息在同一个队列中被顺序处理。 还需要考虑消息系统的设计,比如RabbitMQ本身不保证全局顺序,但可以通过业务端的设计来实现。引用[2]提到,消息重复的问题最好由业务端处理,这可能也适用于顺序问题。比如,业务端可以在消费者内部维护缓冲区,按消息的序列号排序后再处理。 然后要组织这些思路,给出具体的解决方案。可能需要分点说明,比如单队列单消费者、分区键路由、内部排序等。同时,要结合代码示例或者配置建议,让用户更容易理解如何实现这些方案。 还要注意用户可能的疑问,比如问题,多个队列是否会影响吞吐量,或者内部排序带来的延迟。需要指出优缺点,比如单消费者可能成为瓶颈,但分区键可以分散负载。此外,确保每个方案的适用场景,比如订单系统适合用订单ID作为分区键。 最后,生成相关问题时要围绕消息顺序保证方法、能权衡、异常处理等方面,帮助用户进一步深入理解。</think>### RabbitMQ 消息顺序消费解决方案 #### 1. **单队列单消费者模式** - **原理**:每个队列只分配一个消费者,避免多线程/多消费者并发消费导致顺序混乱[^3]。 - **实现**: - 生产者通过`routingKey`将同一类消息(如相同订单号)发送到固定队列。 - 消费者以单线程模式消费该队列。 - **示例代码**: ```python channel.queue_declare(queue='order_queue_1') channel.basic_consume(queue='order_queue_1', on_message_callback=callback) ``` - **适用场景**:低吞吐量场景,如订单状态更新[^4]。 #### 2. **多队列+分区键路由** - **原理**:将消息按业务属(如订单ID)路由到不同队列,每个队列由独立消费顺序消费[^4]。 - **实现**: - 生产者根据`routingKey`(如`订单ID % 队列数`)发送到对应队列。 - 每个队列绑定一个单线程消费者。 - **示例配置**: ```python # 生产者端路由逻辑 routing_key = f"order_{order_id % 4}" # 假设4个队列 channel.basic_publish(exchange='', routing_key=routing_key, body=message) ``` - **优点**:横向扩展强,避免单队列能瓶颈[^4]。 #### 3. **消费者内部排序** - **原理**:消费者接收消息后按序列号缓存并排序,再按顺序处理[^4]。 - **实现**: - 消息体中包含递增序列号(如`sequence_id`)。 - 消费者维护本地缓存,确保按顺序提交到业务逻辑。 - **示例代码逻辑**: ```python buffer = {} def callback(message): seq_id = message['sequence_id'] buffer[seq_id] = message while current_seq in buffer: process(buffer.pop(current_seq)) current_seq += 1 ``` - **适用场景**:允许短暂延迟但需全局顺序的场景。 #### 4. **强制单线程消费** - **原理**:关闭消费者并发配置(如`prefetch_count=1`),确保单线程处理。 - **实现**: ```python channel.basic_qos(prefetch_count=1) # 每次只消费一条消息 ``` - **缺点**:吞吐量受限,需权衡能与顺序--- ### 方案对比与选择建议 | 方案 | 吞吐量 | 实现复杂度 | 适用场景 | |---------------------|--------|------------|------------------------------| | 单队列单消费者 | 低 | 简单 | 低频关键业务(如支付回调) | | 多队列+分区键 | 高 | 中等 | 高并发但可分片业务(如电商) | | 消费者内部排序 | 中 | 高 | 需全局严格顺序(如日志处理) | | 强制单线程 | 低 | 简单 | 临时调试或极低频场景 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈亦康

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值