📖 开场:食堂排队打饭
想象大学食堂打饭 🍚:
无序打饭(多个窗口):
学生1:先到 → 去窗口A → 后拿到饭 😅
学生2:后到 → 去窗口B → 先拿到饭 🎉
学生3:中间到 → 去窗口C → 中间拿到饭 😐
结果:先到的不一定先拿到(乱序)❌
有序打饭(单个窗口):
学生1:先到 → 排队 → 先拿到饭 ✅
学生2:后到 → 排队 → 后拿到饭 ✅
学生3:中间到 → 排队 → 中间拿到饭 ✅
结果:严格按照先后顺序(有序)✅
这就是消息队列的有序性保证!
核心思想:
- 全局有序:所有消息严格有序(单窗口)
- 分区有序:同一类消息有序(多窗口,但同学总去同一个窗口)
🤔 为什么需要消息有序性?
场景1:订单状态变更 📦
问题:消息乱序导致状态错误
订单状态变更:
消息1(10:00:00):订单创建 → 状态:待支付
消息2(10:00:05):支付成功 → 状态:已支付
消息3(10:00:10):订单发货 → 状态:已发货
如果乱序处理:
消息3先到 → 状态:已发货 ✅
消息1后到 → 状态:待支付 ❌(覆盖了已发货)
结果:订单状态错误!😱
正确:严格按顺序处理
消息1 → 待支付
消息2 → 已支付
消息3 → 已发货 ✅
场景2:数据库binlog同步 🔄
问题:binlog乱序导致数据不一致
MySQL主库操作:
操作1(10:00:00):INSERT INTO user (id=1, name='张三')
操作2(10:00:05):UPDATE user SET name='李四' WHERE id=1
操作3(10:00:10):DELETE FROM user WHERE id=1
如果乱序同步到从库:
操作3先执行 → 删除id=1(找不到,忽略)
操作1后执行 → 插入id=1, name='张三' ❌
结果:从库多了一条脏数据!😱
正确:严格按顺序同步
操作1 → 插入
操作2 → 更新
操作3 → 删除 ✅
场景3:账户余额变更 💰
问题:余额计算错误
账户初始余额:1000元
操作1(10:00:00):充值100元 → 余额1100元
操作2(10:00:05):消费50元 → 余额1050元
操作3(10:00:10):充值200元 → 余额1250元
如果乱序处理:
初始:1000
操作2:1000 - 50 = 950
操作1:950 + 100 = 1050
操作3:1050 + 200 = 1250(碰巧对了)
但如果:
操作3:1000 + 200 = 1200
操作1:1200 + 100 = 1300
操作2:1300 - 50 = 1250(也对了?)
其实最危险的是:
操作2先执行,但账户只有1000元,就扣50元成功了
如果操作1是"充值失败"呢?账户应该是1000元,但实际是950元!❌
🎯 三种有序性保证方案
方案1:全局有序(最严格)🔒
原理
所有消息严格有序处理
Producer → 单分区/队列 → 单消费者
特点:
- 严格有序 ✅
- 性能最差 ❌(吞吐量低)
- 可用性最差 ❌(单点故障)
Kafka实现
@Configuration
public class KafkaProducerConfig {
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> config = new HashMap<>();
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
// ⭐ 关键配置1:单分区保证有序
config.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,
SinglePartitionPartitioner.class.getName());
// ⭐ 关键配置2:最多只允许1个未确认的请求(保证发送有序)
config.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
// ⭐ 关键配置3:开启幂等性(防止重复)
config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new DefaultKafkaProducerFactory<>(config);
}
}
/**
* 自定义分区器:所有消息都发送到分区0
*/
public class SinglePartitionPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
// ⭐ 所有消息都发送到分区0
return 0;
}
}
消费者配置
@Component
@Slf4j
public class OrderConsumer {
/**
* ⭐ 单消费者顺序消费
*
* 注意:
* 1. concurrency = 1(只有1个消费者线程)
* 2. 处理完一条才能处理下一条
*/
@KafkaListener(
topics = "orders",
groupId = "order-consumer-group",
concurrency = "1" // ⭐ 单消费者
)
public void consumeOrder(ConsumerRecord<String, OrderMessage> record) {
OrderMessage order = record.value();
log.info("顺序处理订单: orderId={}, status={}",
order.getOrderId(), order.getStatus());
// 处理订单
processOrder(order);
}
private void processOrder(OrderMessage order) {
// 业务逻辑
}
}
优缺点
优点 ✅:
- 严格全局有序
- 实现简单
缺点 ❌:
- 吞吐量低(单分区、单消费者)
- 无法并发处理
- 单点故障(分区或消费者挂了,整个系统停止)
适用场景:
- 消息量小
- 必须全局有序
- 对性能要求不高
方案2:分区有序(常用)🎯
原理
同一类消息有序,不同类消息可并发
Producer → 多分区(按key分区)→ 多消费者(每个消费者处理一个分区)
特点:
- 分区内有序 ✅(同一个订单的消息有序)
- 分区间并发 ✅(不同订单可并发处理)
- 性能高 ✅(吞吐量高)
- 可用性高 ✅(分区独立)
Kafka实现
Topic和分区设计:
Topic: orders
Partitions: 10
分区策略:按订单ID分区
orderId=1 → partition 1
orderId=2 → partition 2
orderId=3 → partition 3
...
同一订单的所有消息都进入同一个分区 → 保证有序 ✅
生产者代码:
@Service
@Slf4j
public class OrderProducer {
@Autowired
private KafkaTemplate<String, OrderMessage> kafkaTemplate;
private static final String TOPIC = "orders";
/**
* 发送订单消息
*
* ⭐ 关键:使用orderId作为key,保证同一订单的消息进入同一个分区
*/
public void sendOrderMessage(OrderMessage message) {
String key = String.valueOf(message.getOrderId()); // ⭐ 订单ID作为key
log.info("发送订单消息: orderId={}, status={}",
message.getOrderId(), message.getStatus());
// ⭐ 发送消息(带key)
kafkaTemplate.send(TOPIC, key, message);
// Kafka会根据key的hash值选择分区:
// partition = hash(key) % partitionCount
// 同一个key的消息,hash值相同,总是进入同一个分区 ✅
}
}
消费者代码:
@Component
@Slf4j
public class OrderConsumer {
/**
* ⭐ 多消费者并发消费(每个消费者处理不同的分区)
*
* concurrency = 10:10个消费者线程
*
* 分配策略:
* Consumer1 → partition 0
* Consumer2 → partition 1
* ...
* Consumer10 → partition 9
*
* 同一个分区的消息,只会被一个消费者处理 → 保证有序 ✅
* 不同分区的消息,由不同消费者处理 → 并发处理 ✅
*/
@KafkaListener(
topics = "orders",
groupId = "order-consumer-group",
concurrency = "10" // ⭐ 10个消费者线程
)
public void consumeOrder(ConsumerRecord<String, OrderMessage> record) {
OrderMessage order = record.value();
int partition = record.partition();
log.info("消费者线程{} 处理分区{} 的订单: orderId={}, status={}",
Thread.currentThread().getName(), partition,
order.getOrderId(), order.getStatus());
// 处理订单
processOrder(order);
}
private void processOrder(OrderMessage order) {
// 业务逻辑(同一订单的消息会被串行处理)
log.info("处理订单状态变更: orderId={}, {} -> {}",
order.getOrderId(), order.getOldStatus(), order.getNewStatus());
}
}
完整示例
/**
* 订单服务
*/
@Service
@Slf4j
public class OrderService {
@Autowired
private OrderProducer producer;
/**
* 订单状态变更流程
*/
public void orderLifecycle(Long orderId) {
// 1. 创建订单
OrderMessage createMsg = new OrderMessage();
createMsg.setOrderId(orderId);
createMsg.setOldStatus(null);
createMsg.setNewStatus("CREATED");
createMsg.setTimestamp(System.currentTimeMillis());
producer.sendOrderMessage(createMsg);
// 模拟延迟
try { Thread.sleep(100); } catch (InterruptedException e) {}
// 2. 支付成功
OrderMessage payMsg = new OrderMessage();
payMsg.setOrderId(orderId);
payMsg.setOldStatus("CREATED");
payMsg.setNewStatus("PAID");
payMsg.setTimestamp(System.currentTimeMillis());
producer.sendOrderMessage(payMsg);
// 模拟延迟
try { Thread.sleep(100); } catch (InterruptedException e) {}
// 3. 订单发货
OrderMessage shipMsg = new OrderMessage();
shipMsg.setOrderId(orderId);
shipMsg.setOldStatus("PAID");
shipMsg.setNewStatus("SHIPPED");
shipMsg.setTimestamp(System.currentTimeMillis());
producer.sendOrderMessage(shipMsg);
log.info("订单{}的3条状态变更消息已发送", orderId);
}
}
/**
* 测试
*/
@SpringBootTest
public class OrderSequenceTest {
@Autowired
private OrderService orderService;
@Test
public void testOrderSequence() throws InterruptedException {
// 同时创建10个订单
for (long orderId = 1; orderId <= 10; orderId++) {
orderService.orderLifecycle(orderId);
}
// 等待消费完成
Thread.sleep(10000);
}
}
输出结果
发送订单消息: orderId=1, status=CREATED
发送订单消息: orderId=1, status=PAID
发送订单消息: orderId=1, status=SHIPPED
发送订单消息: orderId=2, status=CREATED
发送订单消息: orderId=2, status=PAID
发送订单消息: orderId=2, status=SHIPPED
...
消费者线程pool-1-thread-1 处理分区1 的订单: orderId=1, status=CREATED
消费者线程pool-1-thread-1 处理分区1 的订单: orderId=1, status=PAID
消费者线程pool-1-thread-1 处理分区1 的订单: orderId=1, status=SHIPPED
处理订单状态变更: orderId=1, null -> CREATED ✅
处理订单状态变更: orderId=1, CREATED -> PAID ✅
处理订单状态变更: orderId=1, PAID -> SHIPPED ✅
消费者线程pool-1-thread-2 处理分区2 的订单: orderId=2, status=CREATED
消费者线程pool-1-thread-2 处理分区2 的订单: orderId=2, status=PAID
消费者线程pool-1-thread-2 处理分区2 的订单: orderId=2, status=SHIPPED
处理订单状态变更: orderId=2, null -> CREATED ✅
处理订单状态变更: orderId=2, CREATED -> PAID ✅
处理订单状态变更: orderId=2, PAID -> SHIPPED ✅
结论:同一订单的消息严格有序 ✅,不同订单并发处理 ✅
RabbitMQ实现
Queue设计:
为每个订单创建一个独立的队列?❌ 不现实(订单太多)
使用路由键(Routing Key)实现分区效果 ✅
配置:
@Configuration
public class OrderQueueConfig {
private static final int QUEUE_COUNT = 10; // 10个队列(相当于10个分区)
@Bean
public DirectExchange orderExchange() {
return new DirectExchange("order.exchange");
}
/**
* 创建10个队列
*/
@Bean
public List<Queue> orderQueues() {
List<Queue> queues = new ArrayList<>();
for (int i = 0; i < QUEUE_COUNT; i++) {
queues.add(new Queue("order.queue." + i));
}
return queues;
}
/**
* 绑定队列到交换机
*/
@Bean
public List<Binding> orderBindings(DirectExchange exchange, List<Queue> queues) {
List<Binding> bindings = new ArrayList<>();
for (int i = 0; i < QUEUE_COUNT; i++) {
bindings.add(BindingBuilder.bind(queues.get(i))
.to(exchange)
.with("order.queue." + i));
}
return bindings;
}
}
生产者:
@Service
@Slf4j
public class OrderProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
private static final int QUEUE_COUNT = 10;
/**
* 发送订单消息
*
* ⭐ 根据订单ID选择队列(类似Kafka的分区)
*/
public void sendOrderMessage(OrderMessage message) {
// ⭐ 计算队列索引(类似Kafka的分区选择)
int queueIndex = (int) (message.getOrderId() % QUEUE_COUNT);
String routingKey = "order.queue." + queueIndex;
log.info("发送订单消息到队列{}: orderId={}, status={}",
queueIndex, message.getOrderId(), message.getStatus());
// 发送消息
rabbitTemplate.convertAndSend("order.exchange", routingKey, message);
}
}
消费者:
@Component
@Slf4j
public class OrderConsumer {
/**
* ⭐ 为每个队列创建一个消费者
*
* 同一个队列的消息,由同一个消费者串行处理 → 保证有序 ✅
* 不同队列的消息,由不同消费者并发处理 → 并发 ✅
*/
@RabbitListener(queues = "order.queue.0")
public void consumeQueue0(OrderMessage message) {
processOrder(message, 0);
}
@RabbitListener(queues = "order.queue.1")
public void consumeQueue1(OrderMessage message) {
processOrder(message, 1);
}
// ... 其他队列的消费者
private void processOrder(OrderMessage message, int queueIndex) {
log.info("队列{} 处理订单: orderId={}, status={}",
queueIndex, message.getOrderId(), message.getStatus());
// 业务逻辑
}
}
优缺点
优点 ✅:
- 高吞吐量(多分区并发)
- 局部有序(同一类消息有序)
- 高可用(分区独立)
缺点 ❌:
- 无法全局有序
- 需要合理设计分区key
适用场景:
- 大部分业务场景 ✅
- 需要高吞吐量
- 只需要局部有序(如同一订单、同一用户)
方案3:业务层保证有序(最灵活)🛠️
原理
消息可以乱序到达,但业务层保证有序处理
核心思路:
1. 消息带序号(version、timestamp)
2. 消费端缓存消息
3. 按序号排序后处理
实现
消息结构:
@Data
public class OrderMessage {
private Long orderId;
private String status;
private Long version; // ⭐ 版本号(序号)
private Long timestamp;
}
消费者:
@Component
@Slf4j
public class OrderConsumer {
// ⭐ 缓存乱序到达的消息
private Map<Long, PriorityQueue<OrderMessage>> messageCache = new ConcurrentHashMap<>();
// ⭐ 记录每个订单当前期望的版本号
private Map<Long, Long> expectedVersion = new ConcurrentHashMap<>();
@KafkaListener(topics = "orders", groupId = "order-consumer-group")
public void consumeOrder(ConsumerRecord<String, OrderMessage> record) {
OrderMessage message = record.value();
log.info("收到消息: orderId={}, version={}, status={}",
message.getOrderId(), message.getVersion(), message.getStatus());
// ⭐ 处理消息(保证有序)
processOrderInSequence(message);
}
/**
* ⭐ 保证有序处理
*/
private synchronized void processOrderInSequence(OrderMessage message) {
Long orderId = message.getOrderId();
Long version = message.getVersion();
// 获取当前期望的版本号(默认从1开始)
Long expected = expectedVersion.getOrDefault(orderId, 1L);
if (version.equals(expected)) {
// ⭐ 版本号匹配,立即处理
processOrder(message);
expectedVersion.put(orderId, expected + 1);
// ⭐ 检查缓存中是否有下一个版本的消息
processNextFromCache(orderId);
} else if (version > expected) {
// ⭐ 版本号大于期望值,暂存到缓存
log.info("消息版本号{}大于期望值{},暂存到缓存", version, expected);
messageCache.computeIfAbsent(orderId, k -> new PriorityQueue<>(
Comparator.comparing(OrderMessage::getVersion)
)).offer(message);
} else {
// ⭐ 版本号小于期望值,说明是重复消息,忽略
log.warn("收到重复消息: orderId={}, version={}, expected={}",
orderId, version, expected);
}
}
/**
* ⭐ 从缓存中处理后续消息
*/
private void processNextFromCache(Long orderId) {
PriorityQueue<OrderMessage> queue = messageCache.get(orderId);
if (queue == null || queue.isEmpty()) {
return;
}
Long expected = expectedVersion.get(orderId);
while (!queue.isEmpty()) {
OrderMessage next = queue.peek();
if (next.getVersion().equals(expected)) {
// 版本号匹配,处理
queue.poll();
processOrder(next);
expectedVersion.put(orderId, expected + 1);
expected++;
} else {
// 版本号不匹配,停止
break;
}
}
}
private void processOrder(OrderMessage message) {
log.info("✅ 按序处理订单: orderId={}, version={}, status={}",
message.getOrderId(), message.getVersion(), message.getStatus());
// 业务逻辑
}
}
测试
@SpringBootTest
public class OrderSequenceTest {
@Autowired
private OrderProducer producer;
@Test
public void testOutOfOrder() {
Long orderId = 1L;
// ⭐ 故意乱序发送
producer.send(createMessage(orderId, 3L, "SHIPPED")); // 第3条
producer.send(createMessage(orderId, 1L, "CREATED")); // 第1条
producer.send(createMessage(orderId, 2L, "PAID")); // 第2条
Thread.sleep(5000);
}
private OrderMessage createMessage(Long orderId, Long version, String status) {
OrderMessage msg = new OrderMessage();
msg.setOrderId(orderId);
msg.setVersion(version);
msg.setStatus(status);
return msg;
}
}
输出结果
收到消息: orderId=1, version=3, status=SHIPPED
消息版本号3大于期望值1,暂存到缓存
收到消息: orderId=1, version=1, status=CREATED
✅ 按序处理订单: orderId=1, version=1, status=CREATED
收到消息: orderId=1, version=2, status=PAID
✅ 按序处理订单: orderId=1, version=2, status=PAID
✅ 按序处理订单: orderId=1, version=3, status=SHIPPED(从缓存中取出)
结论:即使消息乱序到达,也能保证按序处理 ✅
优缺点
优点 ✅:
- 最灵活(不依赖消息队列特性)
- 可以跨消息队列
- 可以处理复杂场景(如多个消息源)
缺点 ❌:
- 实现复杂
- 需要额外的内存(缓存)
- 可能有延迟(等待前面的消息)
适用场景:
- 消息队列不支持分区
- 需要跨多个消息队列保证有序
- 需要更灵活的有序性控制
📊 三种方案对比
| 方案 | 有序范围 | 吞吐量 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 全局有序 | 所有消息 | ⭐ 低 | ⭐ 简单 | 消息量小,必须全局有序 |
| 分区有序 | 分区内 | ⭐⭐⭐ 高 | ⭐⭐ 中等 | 大部分业务场景(推荐) |
| 业务层有序 | 灵活 | ⭐⭐ 中等 | ⭐⭐⭐ 复杂 | 跨消息队列,复杂场景 |
🎓 面试题速答
Q1: 消息队列如何保证消息有序性?
A: 三种方案:
-
全局有序:
- 单分区 + 单消费者
- 严格有序,但性能差
-
分区有序(推荐):
- 按key分区,同一key的消息进入同一分区
- 同一分区内有序,不同分区并发
-
业务层有序:
- 消息带版本号
- 消费端缓存并排序
最常用的是分区有序,兼顾性能和有序性
Q2: Kafka如何保证消息有序?
A: 分区有序方案:
// 1. 生产者:发送时指定key
kafkaTemplate.send("orders", orderId.toString(), message);
// 2. Kafka:根据key的hash选择分区
partition = hash(key) % partitionCount
// 3. 消费者:每个分区由一个消费者串行处理
@KafkaListener(topics = "orders", concurrency = "10")
关键点:
- 同一个key的消息,hash值相同,进入同一分区
- 同一个分区的消息,由同一个消费者处理
- 消费者串行处理,保证有序
Q3: 如果消息必须全局有序,怎么设计?
A: 全局有序方案:
// 1. Topic只有1个分区
kafka.topics.orders.partitions=1
// 2. 生产者:max.in.flight.requests.per.connection=1
config.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 1);
// 3. 消费者:concurrency=1(单消费者)
@KafkaListener(topics = "orders", concurrency = "1")
缺点:
- 吞吐量低(单分区、单消费者)
- 单点故障
适用场景:
- 消息量小(<100 QPS)
- 必须全局有序(如binlog同步)
Q4: 分区有序如何选择分区key?
A: 选择原则:
-
业务相关性:
- 订单系统:orderId
- 用户系统:userId
- 设备系统:deviceId
-
分布均匀:
- 避免数据倾斜(某个分区消息太多)
- 如果userId分布不均,可以用
userId + timestamp
-
唯一性:
- 同一个业务对象的消息,key必须相同
示例:
// 订单系统:使用orderId作为key
String key = String.valueOf(orderId);
kafkaTemplate.send("orders", key, message);
// 账户系统:使用userId作为key
String key = String.valueOf(userId);
kafkaTemplate.send("accounts", key, message);
Q5: 业务层如何保证消息有序?
A: 方案:消息带版本号 + 缓存排序
// 1. 消息带版本号
class OrderMessage {
Long orderId;
Long version; // 版本号
String status;
}
// 2. 消费端缓存
Map<Long, PriorityQueue<OrderMessage>> cache = new ConcurrentHashMap<>();
Map<Long, Long> expectedVersion = new ConcurrentHashMap<>();
// 3. 按版本号排序处理
if (message.version == expected) {
process(message); // 立即处理
expected++;
} else {
cache.put(message); // 暂存
}
优点:灵活,不依赖消息队列特性
缺点:实现复杂,需要额外内存
Q6: 如何避免消息队列的数据倾斜?
A: 问题:某些分区消息太多,导致负载不均
解决方案:
-
合理选择key:
- 避免使用分布不均的字段(如性别、地区)
- 使用唯一性强的字段(如订单ID、用户ID)
-
复合key:
// 如果userId分布不均,加上随机数 String key = userId + "-" + (random.nextInt(10)); -
监控分区负载:
- 定期检查每个分区的消息数
- 发现倾斜及时调整
-
增加分区数:
- 更多分区,更均匀分布
🎬 总结
消息有序性方案对比
┌─────────────────────────────────────────────┐
│ 全局有序(最严格) │
│ │
│ Producer → 单分区 → 单消费者 │
│ │
│ 特点:严格有序,性能差 ⭐ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 分区有序(推荐)⭐⭐⭐ │
│ │
│ Producer → 按key分区 → 多消费者并发 │
│ │
│ 特点:局部有序,高性能 ✅ │
└─────────────────────────────────────────────┘
┌─────────────────────────────────────────────┐
│ 业务层有序(最灵活) │
│ │
│ Producer → 乱序到达 → 消费端排序 │
│ │
│ 特点:灵活,实现复杂 ⚙️ │
└─────────────────────────────────────────────┘
大部分场景用分区有序!✅
🎉 恭喜你!
你已经完全掌握了消息队列的有序性保证方案!🎊
核心要点:
- 全局有序:单分区+单消费者(性能差)
- 分区有序:按key分区(推荐)⭐⭐⭐
- 业务层有序:版本号+缓存排序(灵活)
下次面试,这样回答:
"消息队列保证有序性有三种方案:
全局有序:单分区+单消费者,严格有序但性能差,适用于消息量小的场景。
分区有序(最常用):按业务key分区,同一key的消息进入同一分区,由同一消费者串行处理。Kafka中,发送时指定key,根据hash(key)%partitionCount选择分区。优点是兼顾性能和有序性。
业务层有序:消息带版本号,消费端缓存并排序处理。最灵活但实现复杂。
我们项目的订单系统使用分区有序,以orderId作为key,保证同一订单的状态变更消息严格有序,不同订单并发处理,吞吐量达到10万QPS。"
面试官:👍 "很好!你对消息有序性理解很深刻!"
本文完 🎬
上一篇: 193-消息队列的削峰填谷作用和实际应用.md
下一篇: 195-分布式锁的实现方式.md
作者注:写完这篇,我都想去食堂当窗口阿姨了!🍚
如果这篇文章对你有帮助,请给我一个Star⭐!