📖 开场:理发店的噩梦
想象你去理发店 💇♂️:
正常情况(有序):
第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-2从offset重新开始
→ 顺序可能被打乱!💀
问题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%的消息
结果:
分区0:ORDER-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: 核心:相同业务的消息进入同一个分区,分区内串行消费
三个层面:
- 生产者:使用相同Key发送,配置
max.in.flight.requests=1 - Broker:相同Key的消息路由到同一个分区
- 消费者:单线程消费每个分区,处理成功才提交offset
Q2: 全局有序和局部有序的区别?
A: 全局有序:
- 整个Topic只有1个分区
- 所有消息全局有序
- 性能极差,TPS只有1000-5000
局部有序(推荐):
- 多个分区,相同Key进同一分区
- 同一业务内有序,不同业务可并行
- 性能高,TPS可达50000+
Q3: Kafka如何实现顺序消息?
A:
-
生产者:
- 使用订单ID作为Key
- 配置
max.in.flight.requests=1 - 启用幂等性
enable.idempotence=true
-
消费者:
- 按分区处理消息
- 串行处理同一分区的消息
- 手动提交offset
Q4: RocketMQ如何实现顺序消息?
A:
-
生产者:
- 使用
MessageQueueSelector按业务ID选择Queue - 相同业务ID进同一个Queue
- 使用
-
消费者:
- 使用
MessageListenerOrderly(顺序监听器) - RocketMQ自动对Queue加锁,保证顺序
- 使用
Q5: 顺序消息消费失败怎么办?
A: Kafka:
- 不提交offset,下次重新消费
- ⚠️ 会阻塞整个分区的后续消息
RocketMQ:
- 返回
SUSPEND_CURRENT_QUEUE_A_MOMENT - 暂停这个Queue的消费,稍后重试
- ⚠️ 也会阻塞整个Queue
建议:
- 快速失败,不要无限重试
- 失败次数过多,发送到死信队列
- 人工介入处理
Q6: 如何避免热点Key问题?
A: 热点Key = 某个Key的消息量特别大,导致分区不均衡
解决方案:
- 拆分Key:热点Key按子Key拆分,分散到多个分区
- 增加分区数:从3个扩到20个,稀释热点
- 业务拆分:热点数据走单独的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 → 同一分区 → 串行消费 → 顺序保证!✅
🎉 恭喜你!
你已经完全掌握了消息队列的顺序性保证方案!🎊
核心要点:
- 单分区:全局有序但性能差,不推荐
- 分区有序:局部有序且性能高,最常用⭐⭐⭐⭐⭐
- 关键配置: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 许可协议,转载请注明出处。