🔢 消息队列的顺序性保证方案:让消息排好队!

31 阅读17分钟

📖 开场:理发店的噩梦

想象你去理发店 💇‍♂️:

正常情况(有序)

第1步:洗头 💦
第2步:剪发 ✂️
第3步:吹干 💨

结果:完美发型!✅

如果顺序乱了(无序)

第1步:吹干 💨 ← 吹什么?头发还是干的!
第2步:剪发 ✂️ ← 剪了一地干头发!
第3步:洗头 💦 ← 头发都剪完了才洗?!

结果:理发师被投诉!😱

这就是消息顺序性的重要性!

在消息队列中,某些业务场景必须保证顺序

  • 订单状态流转:创建→支付→发货→完成 📦
  • 数据库操作同步:INSERT→UPDATE→DELETE 🗄️
  • 股票交易记录:按时间顺序处理 📈

顺序错了,业务就乱了! 💀


🤔 为什么会出现乱序问题?

问题1:生产者端乱序 📤

场景1:多线程发送

// 错误示例:多线程并发发送
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 1; i <= 100; i++) {
    final int orderId = i;
    executor.submit(() -> {
        // 多线程并发发送,顺序无法保证!
        producer.send("订单" + orderId);
    });
}

// 结果:订单1、订单5、订单3、订单2... 乱序!💀

原因

  • 多线程并发发送
  • 网络延迟不同
  • 服务器处理速度不同

场景2:异步发送

// 异步发送,顺序无法保证
producer.send(msg1);  // 可能后到达
producer.send(msg2);  // 可能先到达
producer.send(msg3);

// 到达顺序:msg2 → msg3 → msg1 💀

问题2:Broker端乱序 💾

场景1:多分区并行

Topic: orders (3个分区)

消息1 → 分区0 📦
消息2 → 分区1 📦
消息3 → 分区2 📦

消费者并行消费:
消费者1消费分区0:慢 🐌
消费者2消费分区1:快 ⚡
消费者3消费分区2:快 ⚡

实际消费顺序:消息2 → 消息3 → 消息1 💀

原因

  • 多分区并行处理
  • 不同分区的消费速度不同

场景2:Rebalance打乱顺序

Consumer-1正在消费分区0
→ 触发Rebalance
→ 分区0分配给Consumer-2
→ Consumer-2offset重新开始
→ 顺序可能被打乱!💀

问题3:消费者端乱序 📥

场景1:多线程消费

// 错误示例:多线程并发消费
ExecutorService executor = Executors.newFixedThreadPool(10);

consumer.poll(...).forEach(record -> {
    executor.submit(() -> {
        // 多线程并发处理,顺序无法保证!
        processMessage(record);
    });
});

// 线程1处理消息3(快)
// 线程2处理消息1(慢)
// 线程3处理消息2(中等)
// 实际处理顺序:消息3 → 消息2 → 消息1 💀

场景2:重试导致乱序

消息1 → 处理成功 ✅
消息2 → 处理失败 ❌ → 重试
消息3 → 处理成功 ✅

实际顺序:消息1 → 消息3 → 消息2(重试) 💀

🛡️ 顺序消息的保证方案

🎯 核心原则

顺序消息的三大原则

1. 发送有序:同一业务的消息按顺序发送
2. 存储有序:消息按顺序存储在同一个分区
3. 消费有序:消息按顺序被消费

实现关键相同业务的消息进入同一个分区,分区内串行消费


📦 方案1:单分区顺序(全局有序)⭐

原理

整个Topic只有1个分区,所有消息全局有序

Topic: orders (1个分区)
    └─ Partition-0: [M1][M2][M3][M4][M5]

Producer: 按顺序发送
Consumer: 按顺序消费

保证:消息全局有序!✅

生活比喻
银行只有一个窗口,所有人排一个队,绝对有序!🏦


代码实现

Kafka实现

创建单分区Topic

kafka-topics.sh --create \
  --bootstrap-server localhost:9092 \
  --topic global-order-topic \
  --partitions 1 \  # ⭐ 只有1个分区
  --replication-factor 3

生产者

// 不需要特殊配置,直接发送
producer.send(new ProducerRecord<>("global-order-topic", "消息1"));
producer.send(new ProducerRecord<>("global-order-topic", "消息2"));
producer.send(new ProducerRecord<>("global-order-topic", "消息3"));

// 结果:按顺序存储到唯一的分区中

消费者

// Kafka默认就是顺序消费单个分区
consumer.subscribe(Arrays.asList("global-order-topic"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    
    for (ConsumerRecord<String, String> record : records) {
        // ⭐ 串行处理,保证顺序
        processMessage(record);
    }
}

// 结果:按顺序消费 M1 → M2 → M3 → M4 → M5 ✅

RocketMQ实现

创建单Queue Topic

# 默认创建4个Queue,需要修改为1个
sh mqadmin updateTopic \
  -n localhost:9876 \
  -t global-order-topic \
  -r 1 \  # ⭐ 读队列数=1
  -w 1    # ⭐ 写队列数=1

生产者

// 不指定Queue,默认轮询(但只有1个Queue,所以没问题)
producer.send(msg1);
producer.send(msg2);
producer.send(msg3);

消费者

consumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(
        List<MessageExt> msgs,
        ConsumeOrderlyContext context
    ) {
        for (MessageExt msg : msgs) {
            // ⭐ 顺序消费
            processMessage(msg);
        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
});

优缺点

优点

  • 100%保证全局有序
  • ✅ 实现极其简单
  • ✅ 无需额外逻辑

缺点

  • 性能极差! 只能串行处理,无法并发 🐌
  • 吞吐量低! 单分区成为瓶颈
  • 可用性差! 单分区故障,整个Topic不可用
  • 不适合高并发场景!

性能数据

单分区 TPS: 1000-5000 消息/秒
多分区 TPS: 50000+ 消息/秒

差距:10-50倍!😱

适用场景

  • 数据量极小的场景
  • 顺序要求极高且性能要求不高的场景
  • 一般不推荐使用!

📦 方案2:分区有序(局部有序)⭐⭐⭐⭐⭐

原理

相同业务Key的消息进入同一个分区,分区内有序

Topic: orders (3个分区)

订单1的消息 → 分区0: [1-创建][1-支付][1-发货][1-完成]
订单2的消息 → 分区1: [2-创建][2-支付][2-发货][2-完成]
订单3的消息 → 分区2: [3-创建][3-支付][3-发货][3-完成]

保证:
- 订单1的消息内部有序 ✅
- 订单2的消息内部有序 ✅
- 订单3的消息内部有序 ✅
- 但订单之间无需有序 😊

生活比喻
银行有3个窗口,同一个客户的所有业务在同一个窗口办理,
不同客户可以并行办理!🏦

核心

partition = hash(key) % partitionCount

相同key → 相同partition → 顺序保证

Kafka实现

生产者:按Key分区

import org.apache.kafka.clients.producer.*;
import java.util.Properties;

public class OrderedProducer {
    
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        
        // ⭐ 重要:保证幂等性和顺序性
        props.put("enable.idempotence", true);  // 幂等性
        props.put("max.in.flight.requests.per.connection", 1);  // 单连接只允许1个未确认请求
        props.put("acks", "all");
        
        KafkaProducer<String, String> producer = new KafkaProducer<>(props);
        
        // 模拟订单数据
        List<OrderEvent> events = buildOrderEvents();
        
        for (OrderEvent event : events) {
            // ⭐ 关键:使用订单ID作为Key
            ProducerRecord<String, String> record = new ProducerRecord<>(
                "orders",
                event.getOrderId(),  // ⭐ Key = 订单ID
                event.toJson()       // Value = 消息内容
            );
            
            // 同步发送(保证顺序)
            RecordMetadata metadata = producer.send(record).get();
            
            System.out.printf("订单[%s]-%s 发送到分区[%d], offset[%d]%n",
                event.getOrderId(), event.getEvent(),
                metadata.partition(), metadata.offset());
        }
        
        producer.close();
    }
    
    /**
     * 构造订单事件
     */
    private static List<OrderEvent> buildOrderEvents() {
        List<OrderEvent> events = new ArrayList<>();
        
        // 订单1的事件流
        events.add(new OrderEvent("ORDER-001", "创建订单"));
        events.add(new OrderEvent("ORDER-001", "支付"));
        events.add(new OrderEvent("ORDER-001", "发货"));
        events.add(new OrderEvent("ORDER-001", "确认收货"));
        
        // 订单2的事件流
        events.add(new OrderEvent("ORDER-002", "创建订单"));
        events.add(new OrderEvent("ORDER-002", "支付"));
        events.add(new OrderEvent("ORDER-002", "发货"));
        events.add(new OrderEvent("ORDER-002", "确认收货"));
        
        // 订单3的事件流
        events.add(new OrderEvent("ORDER-003", "创建订单"));
        events.add(new OrderEvent("ORDER-003", "支付"));
        events.add(new OrderEvent("ORDER-003", "发货"));
        events.add(new OrderEvent("ORDER-003", "确认收货"));
        
        return events;
    }
}

@Data
@AllArgsConstructor
class OrderEvent {
    private String orderId;
    private String event;
    
    public String toJson() {
        return String.format("{"orderId":"%s","event":"%s"}", orderId, event);
    }
}

运行结果

订单[ORDER-001]-创建订单 发送到分区[1], offset[0]
订单[ORDER-001]-支付 发送到分区[1], offset[1]       ← 同一分区!
订单[ORDER-001]-发货 发送到分区[1], offset[2]       ← 同一分区!
订单[ORDER-001]-确认收货 发送到分区[1], offset[3]   ← 同一分区!

订单[ORDER-002]-创建订单 发送到分区[2], offset[0]
订单[ORDER-002]-支付 发送到分区[2], offset[1]
订单[ORDER-002]-发货 发送到分区[2], offset[2]
订单[ORDER-002]-确认收货 发送到分区[2], offset[3]

订单[ORDER-003]-创建订单 发送到分区[0], offset[0]
订单[ORDER-003]-支付 发送到分区[0], offset[1]
订单[ORDER-003]-发货 发送到分区[0], offset[2]
订单[ORDER-003]-确认收货 发送到分区[0], offset[3]

相同订单ID的消息进入同一个分区!


消费者:单线程消费每个分区

import org.apache.kafka.clients.consumer.*;
import java.time.Duration;
import java.util.*;

public class OrderedConsumer {
    
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("group.id", "order-consumer-group");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("enable.auto.commit", false);
        
        // ⭐ 重要:设置单次拉取少量消息,避免处理时间过长
        props.put("max.poll.records", 10);
        
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Arrays.asList("orders"));
        
        try {
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
                
                // ⭐ 按分区处理消息(保证分区内有序)
                for (TopicPartition partition : records.partitions()) {
                    List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
                    
                    System.out.printf("=== 处理分区[%d]的消息(共%d条)===%n",
                        partition.partition(), partitionRecords.size());
                    
                    // ⭐ 串行处理同一分区的消息(保证顺序)
                    for (ConsumerRecord<String, String> record : partitionRecords) {
                        try {
                            processMessage(record);
                            
                            System.out.printf("✅ 消费成功:分区[%d], offset[%d], key[%s], value[%s]%n",
                                record.partition(), record.offset(),
                                record.key(), record.value());
                            
                        } catch (Exception e) {
                            System.err.printf("❌ 消费失败:分区[%d], offset[%d], error: %s%n",
                                record.partition(), record.offset(), e.getMessage());
                            
                            // ⭐ 处理失败,不提交offset,下次重新消费
                            throw e;
                        }
                    }
                }
                
                // ⭐ 所有消息处理成功后,才提交offset
                consumer.commitSync();
            }
        } finally {
            consumer.close();
        }
    }
    
    /**
     * 处理消息
     */
    private static void processMessage(ConsumerRecord<String, String> record) throws Exception {
        // 业务处理逻辑
        Thread.sleep(100);  // 模拟处理时间
    }
}

运行结果

=== 处理分区[0]的消息(共4条)===
✅ 消费成功:分区[0], offset[0], key[ORDER-003], value[{"orderId":"ORDER-003","event":"创建订单"}]
✅ 消费成功:分区[0], offset[1], key[ORDER-003], value[{"orderId":"ORDER-003","event":"支付"}]
✅ 消费成功:分区[0], offset[2], key[ORDER-003], value[{"orderId":"ORDER-003","event":"发货"}]
✅ 消费成功:分区[0], offset[3], key[ORDER-003], value[{"orderId":"ORDER-003","event":"确认收货"}]

=== 处理分区[1]的消息(共4条)===
✅ 消费成功:分区[1], offset[0], key[ORDER-001], value[{"orderId":"ORDER-001","event":"创建订单"}]
✅ 消费成功:分区[1], offset[1], key[ORDER-001], value[{"orderId":"ORDER-001","event":"支付"}]
✅ 消费成功:分区[1], offset[2], key[ORDER-001], value[{"orderId":"ORDER-001","event":"发货"}]
✅ 消费成功:分区[1], offset[3], key[ORDER-001], value[{"orderId":"ORDER-001","event":"确认收货"}]

=== 处理分区[2]的消息(共4条)===
✅ 消费成功:分区[2], offset[0], key[ORDER-002], value[{"orderId":"ORDER-002","event":"创建订单"}]
✅ 消费成功:分区[2], offset[1], key[ORDER-002], value[{"orderId":"ORDER-002","event":"支付"}]
✅ 消费成功:分区[2], offset[2], key[ORDER-002], value[{"orderId":"ORDER-002","event":"发货"}]
✅ 消费成功:分区[2], offset[3], key[ORDER-002], value[{"orderId":"ORDER-002","event":"确认收货"}]

每个订单的消息按顺序消费!


RocketMQ实现

生产者:使用MessageQueueSelector

import org.apache.rocketmq.client.producer.*;
import org.apache.rocketmq.common.message.*;

public class RocketMQOrderedProducer {
    
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
        producer.setNamesrvAddr("localhost:9876");
        producer.start();
        
        List<OrderEvent> events = buildOrderEvents();
        
        for (OrderEvent event : events) {
            Message msg = new Message(
                "OrderTopic",
                event.toJson().getBytes()
            );
            
            // ⭐ 使用MessageQueueSelector按订单ID选择队列
            SendResult sendResult = producer.send(
                msg,
                new MessageQueueSelector() {
                    @Override
                    public MessageQueue select(
                        List<MessageQueue> mqs,
                        Message msg,
                        Object arg  // 这里是订单ID
                    ) {
                        String orderId = (String) arg;
                        // ⭐ 根据订单ID选择队列
                        int index = Math.abs(orderId.hashCode()) % mqs.size();
                        return mqs.get(index);
                    }
                },
                event.getOrderId()  // ⭐ 传入订单ID作为参数
            );
            
            System.out.printf("订单[%s]-%s 发送到Queue[%d]%n",
                event.getOrderId(), event.getEvent(),
                sendResult.getMessageQueue().getQueueId());
        }
        
        producer.shutdown();
    }
}

消费者:使用MessageListenerOrderly

import org.apache.rocketmq.client.consumer.*;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;

public class RocketMQOrderedConsumer {
    
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");
        consumer.setNamesrvAddr("localhost:9876");
        consumer.subscribe("OrderTopic", "*");
        
        // ⭐ 使用MessageListenerOrderly(顺序监听器)
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(
                List<MessageExt> msgs,
                ConsumeOrderlyContext context
            ) {
                // ⭐ 这里的消息是有序的!
                for (MessageExt msg : msgs) {
                    String body = new String(msg.getBody());
                    System.out.printf("✅ 顺序消费:Queue[%d], Offset[%d], Body[%s]%n",
                        msg.getQueueId(), msg.getQueueOffset(), body);
                    
                    try {
                        processMessage(msg);
                    } catch (Exception e) {
                        // ⭐ 处理失败,暂停消费这个Queue
                        return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                    }
                }
                
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });
        
        consumer.start();
        System.out.println("顺序消费者启动成功!");
    }
    
    private static void processMessage(MessageExt msg) throws Exception {
        // 业务处理
        Thread.sleep(100);
    }
}

优缺点

优点

  • 既保证了顺序,又支持并发! ⭐⭐⭐⭐⭐
  • ✅ 性能高(多分区并行处理)
  • ✅ 可扩展(可以增加分区和消费者)
  • 最常用的方案!

缺点

  • ❌ 实现稍复杂(需要选择合适的Key)
  • ❌ 只能保证局部有序(同一Key内有序)
  • ❌ 热点Key会成为瓶颈

性能数据

3个分区,3个消费者:TPS 15000-30000 消息/秒
10个分区,10个消费者:TPS 50000-100000 消息/秒

适用场景

  • 大部分业务场景! ⭐⭐⭐⭐⭐
  • 订单处理、用户行为追踪、数据同步等

⚠️ 顺序消息的注意事项

1. 生产者配置

Kafka生产者配置

Properties props = new Properties();

// ⭐ 关键配置1:启用幂等性
props.put("enable.idempotence", true);

// ⭐ 关键配置2:单连接只允许1个未确认请求
// 防止重试导致乱序
props.put("max.in.flight.requests.per.connection", 1);

// ⭐ 关键配置3:确认级别
props.put("acks", "all");

// ⭐ 关键配置4:重试次数
props.put("retries", Integer.MAX_VALUE);

为什么max.in.flight.requests.per.connection=1?

场景:发送3条消息

max.in.flight.requests=5(默认):
  发送消息1 → 发送消息2 → 发送消息3(不等消息1确认)
  如果消息1失败重试 → 顺序变成:消息2、消息3、消息1 💀

max.in.flight.requests=1:
  发送消息1 → 等待确认 → 发送消息2 → 等待确认 → 发送消息3
  保证顺序:消息1、消息2、消息3 ✅

RocketMQ生产者配置

DefaultMQProducer producer = new DefaultMQProducer("producer_group");

// ⭐ 关键配置:发送超时时间
producer.setSendMsgTimeout(10000);  // 10秒

// ⭐ 关键配置:失败重试次数
producer.setRetryTimesWhenSendFailed(3);

2. 消费者配置

Kafka消费者配置

Properties props = new Properties();

// ⭐ 关键配置1:单次拉取少量消息
props.put("max.poll.records", 10);  // 避免处理时间过长

// ⭐ 关键配置2:手动提交offset
props.put("enable.auto.commit", false);

// ⭐ 关键配置3:增大超时时间
props.put("max.poll.interval.ms", 300000);  // 5分钟
props.put("session.timeout.ms", 30000);  // 30秒

RocketMQ消费者配置

DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");

// ⭐ 关键配置1:消费线程数(顺序消费时建议=Queue数量)
consumer.setConsumeThreadMin(3);
consumer.setConsumeThreadMax(3);

// ⭐ 关键配置2:单次消费消息数
consumer.setConsumeMessageBatchMaxSize(1);  // 顺序消费建议=1

// ⭐ 关键配置3:消费超时时间
consumer.setConsumeTimeout(15);  // 15分钟

3. 错误处理

消费失败的处理

Kafka

try {
    processMessage(record);
    consumer.commitSync();  // 成功才提交
} catch (Exception e) {
    log.error("消费失败", e);
    // 不提交offset,下次重新消费
    // 注意:这会阻塞整个分区!
}

RocketMQ

@Override
public ConsumeOrderlyStatus consumeMessage(
    List<MessageExt> msgs,
    ConsumeOrderlyContext context
) {
    try {
        processMessage(msgs.get(0));
        return ConsumeOrderlyStatus.SUCCESS;
    } catch (Exception e) {
        log.error("消费失败", e);
        // ⭐ 返回SUSPEND:暂停消费这个Queue,稍后重试
        return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
    }
}

注意

  • ⚠️ 一条消息失败,会阻塞整个分区/Queue的后续消息!
  • ⚠️ 需要快速失败,不要无限重试
  • ⚠️ 失败次数过多,发送到死信队列

4. 热点Key问题

问题

100个订单,但其中订单ORDER-HOT占了80%的消息

结果:
分区0ORDER-HOT的消息(80000条)← 很慢!🐌
分区1:其他订单的消息(10000条)
分区2:其他订单的消息(10000条)

分区0成为瓶颈!

解决方案

方案1:拆分Key

// 热点订单按子订单ID拆分
String key = isHotOrder(orderId) 
    ? orderId + "-" + (index % 10)  // 拆分成10个子Key
    : orderId;

producer.send(new ProducerRecord<>("orders", key, value));

方案2:动态调整分区数

# 增加分区数
kafka-topics.sh --alter \
  --bootstrap-server localhost:9092 \
  --topic orders \
  --partitions 20  # 从3个扩到20个

方案3:业务拆分

热点订单走单独的Topic:
- hot-orders(专门处理热点订单)
- normal-orders(处理普通订单)

📊 方案对比总结

方案顺序保证性能复杂度适用场景推荐度
单分区全局有序 ✅🐌 极低😊 简单小数据量
分区有序局部有序 ✅⚡⚡⚡ 高😐 中等大部分场景⭐⭐⭐⭐⭐

🎓 面试题速答

Q1: 如何保证消息的顺序性?

A: 核心:相同业务的消息进入同一个分区,分区内串行消费

三个层面

  1. 生产者:使用相同Key发送,配置max.in.flight.requests=1
  2. Broker:相同Key的消息路由到同一个分区
  3. 消费者:单线程消费每个分区,处理成功才提交offset

Q2: 全局有序和局部有序的区别?

A: 全局有序

  • 整个Topic只有1个分区
  • 所有消息全局有序
  • 性能极差,TPS只有1000-5000

局部有序(推荐):

  • 多个分区,相同Key进同一分区
  • 同一业务内有序,不同业务可并行
  • 性能高,TPS可达50000+

Q3: Kafka如何实现顺序消息?

A:

  1. 生产者

    • 使用订单ID作为Key
    • 配置max.in.flight.requests=1
    • 启用幂等性enable.idempotence=true
  2. 消费者

    • 按分区处理消息
    • 串行处理同一分区的消息
    • 手动提交offset

Q4: RocketMQ如何实现顺序消息?

A:

  1. 生产者

    • 使用MessageQueueSelector按业务ID选择Queue
    • 相同业务ID进同一个Queue
  2. 消费者

    • 使用MessageListenerOrderly(顺序监听器)
    • RocketMQ自动对Queue加锁,保证顺序

Q5: 顺序消息消费失败怎么办?

A: Kafka

  • 不提交offset,下次重新消费
  • ⚠️ 会阻塞整个分区的后续消息

RocketMQ

  • 返回SUSPEND_CURRENT_QUEUE_A_MOMENT
  • 暂停这个Queue的消费,稍后重试
  • ⚠️ 也会阻塞整个Queue

建议

  • 快速失败,不要无限重试
  • 失败次数过多,发送到死信队列
  • 人工介入处理

Q6: 如何避免热点Key问题?

A: 热点Key = 某个Key的消息量特别大,导致分区不均衡

解决方案

  1. 拆分Key:热点Key按子Key拆分,分散到多个分区
  2. 增加分区数:从3个扩到20个,稀释热点
  3. 业务拆分:热点数据走单独的Topic

🎯 最佳实践总结

生产者最佳实践 ✅

Properties props = new Properties();

// ⭐ 顺序性配置
props.put("enable.idempotence", true);  // 幂等性
props.put("max.in.flight.requests.per.connection", 1);  // 单连接1个请求
props.put("acks", "all");  // 等待所有ISR确认
props.put("retries", Integer.MAX_VALUE);  // 无限重试

// 性能配置
props.put("batch.size", 16384);  // 16KB批量大小
props.put("linger.ms", 10);  // 等待10ms积攒批量

KafkaProducer<String, String> producer = new KafkaProducer<>(props);

// ⭐ 使用业务ID作为Key
ProducerRecord<String, String> record = new ProducerRecord<>(
    "orders",
    orderId,  // ⭐ Key = 订单ID
    orderData
);

// 同步发送(保证顺序)
producer.send(record).get();

消费者最佳实践 ✅

Properties props = new Properties();

props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "order-consumer-group");
props.put("enable.auto.commit", false);  // 手动提交

// ⭐ 顺序性配置
props.put("max.poll.records", 10);  // 单次拉取少量消息
props.put("max.poll.interval.ms", 300000);  // 5分钟超时

KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("orders"));

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));
    
    // ⭐ 按分区处理
    for (TopicPartition partition : records.partitions()) {
        List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
        
        // ⭐ 串行处理
        for (ConsumerRecord<String, String> record : partitionRecords) {
            try {
                processMessage(record);
            } catch (Exception e) {
                log.error("处理失败", e);
                throw e;  // 不提交offset,下次重试
            }
        }
    }
    
    // ⭐ 所有消息处理成功,才提交offset
    consumer.commitSync();
}

分区数量规划 📊

分区数量建议:

1. 根据吞吐量估算:
   分区数 = 期望TPS / 单分区TPS
   例如:期望100000 TPS,单分区10000 TPS
   → 需要10个分区

2. 根据消费者数量:
   分区数 >= 消费者数
   建议:分区数 = 消费者数 × 2(预留扩展空间)

3. 不要太多:
   - 单个Topic不超过100个分区
   - 分区太多,Rebalance会很慢

4. 热点Key考虑:
   - 如果有热点Key,适当增加分区数

🎬 总结:一张图看懂顺序消息

                消息顺序性保证全景图

┌──────────────────────────────────────────────────┐
│              Producer(生产者)                   │
│                                                  │
│  配置:                                           │
│  ├─ enable.idempotence=true                     │
│  ├─ max.in.flight.requests=1                    │
│  └─ acks=all                                    │
│                                                  │
│  发送:                                           │
│  ├─ 订单1 → Key="ORDER-001" → 分区0             │
│  ├─ 订单2 → Key="ORDER-002" → 分区1             │
│  └─ 订单3 → Key="ORDER-003" → 分区2             │
│                                                  │
│  ⭐ 相同Key → 同一分区                           │
└─────────────────┬────────────────────────────────┘
                  │
                  ↓
┌──────────────────────────────────────────────────┐
│               Broker(存储)                     │
│                                                  │
│  Topic: orders (3个分区)                         │
│  ├─ 分区0: [1-创建][1-支付][1-发货][1-完成]      │
│  ├─ 分区1: [2-创建][2-支付][2-发货][2-完成]      │
│  └─ 分区2: [3-创建][3-支付][3-发货][3-完成]      │
│                                                  │
│  ⭐ 同一分区内,消息FIFO有序                     │
└─────────────────┬────────────────────────────────┘
                  │
                  ↓
┌──────────────────────────────────────────────────┐
│              Consumer(消费者)                   │
│                                                  │
│  配置:                                           │
│  ├─ enable.auto.commit=false                    │
│  ├─ max.poll.records=10                         │
│  └─ 手动提交offset                              │
│                                                  │
│  消费:                                           │
│  ├─ Consumer-1 → 分区0(串行消费)              │
│  ├─ Consumer-2 → 分区1(串行消费)              │
│  └─ Consumer-3 → 分区2(串行消费)              │
│                                                  │
│  ⭐ 单线程消费每个分区,保证顺序                 │
└──────────────────────────────────────────────────┘

         相同Key → 同一分区 → 串行消费 → 顺序保证!✅

🎉 恭喜你!

你已经完全掌握了消息队列的顺序性保证方案!🎊

核心要点

  1. 单分区:全局有序但性能差,不推荐
  2. 分区有序:局部有序且性能高,最常用⭐⭐⭐⭐⭐
  3. 关键配置:max.in.flight.requests=1, 手动提交offset

下次面试,这样回答

"消息队列保证顺序性的核心是:相同业务的消息进入同一个分区,分区内串行消费。

生产者端,我们使用订单ID作为消息Key,配置max.in.flight.requests=1防止重试导致乱序,启用幂等性保证消息不重复。

消费者端,我们按分区处理消息,单线程串行消费每个分区的消息,处理成功才手动提交offset。

这种方案既保证了顺序性,又支持多分区并行处理,性能可以达到50000+ TPS。

我们项目中使用这个方案处理订单状态流转,效果很好。"

面试官:👍 "回答得非常专业!你对顺序消息理解很深刻!"


🎈 表情包时间 🎈

       学完消息顺序性保证后:

              之前:
         😱 "消息乱序了!先发货后支付!"
         
              现在:
         😎 "相同Key同一分区,稳得一批!"
         
              消息:
         📦 "我要排队!我要排队!"
              
              你:
         👨‍💼 "都给我排好队!按顺序来!"

本文完 🎬

记得点赞👍 收藏⭐ 分享🔗

上一篇: 187-如何保证消息的幂等消费.md
下一篇: 189-如何设计一个高可用的消息队列架构.md


作者注:写完这篇,我去理发店都要确认顺序了!💇‍♂️
如果这篇文章对你有帮助,请给我一个Star⭐!

版权声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。