📖 开场:银行转账的故事
想象你要给朋友转账 💰:
操作序列:
1. 检查余额(有1000元) ✅
2. 扣除你的1000元 ✅
3. 增加朋友的1000元 ✅
如果顺序乱了会怎样? 😱
错误序列:
1. 增加朋友的1000元 ✅ ← 钱凭空出现!
2. 检查余额(有1000元) ✅
3. 扣除你的1000元 ✅
结果:朋友多了1000,你少了1000,但总金额不对了!💀
这就是为什么需要顺序消息和事务消息!
- 顺序消息:保证消息按顺序处理 🔢
- 事务消息:保证操作的原子性(要么全成功,要么全失败)⚛️
🎯 Part 1: 顺序消息(Ordered Message)
🤔 什么是顺序消息?
定义:消息按照发送的顺序被消费
示例:
发送顺序:消息1 → 消息2 → 消息3
消费顺序:消息1 → 消息2 → 消息3 ✅
不能是:消息2 → 消息1 → 消息3 ❌
🏢 业务场景
场景1:订单状态流转 📦
订单状态变化:
1. 创建订单
2. 支付订单
3. 发货
4. 确认收货
必须按顺序!不能先发货再支付!😱
场景2:数据库Binlog同步 🗄️
MySQL操作:
1. INSERT user (id=1, name='张三')
2. UPDATE user SET name='李四' WHERE id=1
3. DELETE user WHERE id=1
同步到ES时必须按顺序!
否则:先DELETE再INSERT,数据就错了!💀
场景3:股票交易 📈
交易记录:
1. 买入100股,价格10元
2. 卖出50股,价格12元
3. 买入30股,价格11元
必须按时间顺序处理,否则计算盈亏就错了!
📊 RocketMQ的三种顺序级别
1️⃣ 全局顺序(Global Ordering)⭐
定义:整个Topic的所有消息全局有序
实现:1个Topic只有1个Queue
Topic: global-order-topic
└─ Queue-0 ← 只有一个队列!
发送:M1 → M2 → M3 → M4 → M5
Queue: [M1][M2][M3][M4][M5]
消费:M1 → M2 → M3 → M4 → M5 ✅
生活比喻:
银行只有一个窗口,所有人排一个队,绝对有序!👥
优点:
- ✅ 100%保证顺序
- ✅ 实现简单
缺点:
- ❌ 性能极差(只能串行处理)
- ❌ 无法并发
- ❌ 吞吐量低
适用场景:
- 顺序要求极高且数据量很小的场景
- 一般不推荐使用!
2️⃣ 分区顺序(Partition Ordering)⭐⭐⭐⭐⭐
定义:同一个分区(Queue)内的消息有序
实现:多个Queue,相同业务ID的消息路由到同一个Queue
Topic: order-topic
├─ Queue-0: [订单1-M1][订单1-M2][订单1-M3] ← 订单1的消息
├─ Queue-1: [订单2-M1][订单2-M2][订单2-M3] ← 订单2的消息
└─ Queue-2: [订单3-M1][订单3-M2][订单3-M3] ← 订单3的消息
保证:
- 订单1的消息内部有序 ✅
- 订单2的消息内部有序 ✅
- 订单3的消息内部有序 ✅
- 但订单之间无需有序 😊
生活比喻:
银行有3个窗口,同一个客户的所有业务在同一个窗口办理!🏦
优点:
- ✅ 保证了需要的顺序性
- ✅ 支持并发(不同订单可并行处理)
- ✅ 吞吐量高
- ✅ **最常用的方案!**⭐⭐⭐⭐⭐
缺点:
- ❌ 实现稍复杂
- ❌ 需要合理选择分区Key
3️⃣ 普通消息(无顺序)
定义:消息随机分配到各个Queue,不保证顺序
Topic: normal-topic
├─ Queue-0: [M3][M1][M5]
├─ Queue-1: [M2][M6]
└─ Queue-2: [M4]
完全无序,谁快谁先处理!
适用场景:
- 不需要顺序的场景(日志收集、监控数据)
💻 顺序消息代码实现
生产者:发送顺序消息
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageQueue;
import java.util.List;
public class OrderProducer {
public static void main(String[] args) throws Exception {
// 1. 创建生产者
DefaultMQProducer producer = new DefaultMQProducer("order_producer_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();
// 2. 模拟订单数据
List<OrderStep> orderSteps = buildOrders();
// 3. 发送顺序消息
for (OrderStep orderStep : orderSteps) {
Message msg = new Message(
"OrderTopic", // Topic
orderStep.toString().getBytes()
);
// ⭐ 关键:使用MessageQueueSelector,根据订单ID选择队列
SendResult sendResult = producer.send(
msg,
new MessageQueueSelector() {
@Override
public MessageQueue select(
List<MessageQueue> mqs,
Message msg,
Object arg // 这里是订单ID
) {
// 根据订单ID选择队列
Long orderId = (Long) arg;
int index = (int) (orderId % mqs.size());
return mqs.get(index); // 相同订单ID → 相同队列
}
},
orderStep.getOrderId() // ⭐ 传入订单ID作为分区Key
);
System.out.printf("订单%d-%s 发送到 Queue-%d%n",
orderStep.getOrderId(),
orderStep.getDesc(),
sendResult.getMessageQueue().getQueueId());
}
producer.shutdown();
}
/**
* 构造订单数据
*/
private static List<OrderStep> buildOrders() {
List<OrderStep> orderSteps = new ArrayList<>();
// 订单1的流程
orderSteps.add(new OrderStep(1L, "创建订单"));
orderSteps.add(new OrderStep(1L, "支付"));
orderSteps.add(new OrderStep(1L, "发货"));
orderSteps.add(new OrderStep(1L, "确认收货"));
// 订单2的流程
orderSteps.add(new OrderStep(2L, "创建订单"));
orderSteps.add(new OrderStep(2L, "支付"));
orderSteps.add(new OrderStep(2L, "发货"));
orderSteps.add(new OrderStep(2L, "确认收货"));
// 订单3的流程
orderSteps.add(new OrderStep(3L, "创建订单"));
orderSteps.add(new OrderStep(3L, "支付"));
orderSteps.add(new OrderStep(3L, "发货"));
orderSteps.add(new OrderStep(3L, "确认收货"));
return orderSteps;
}
}
/**
* 订单步骤实体
*/
@Data
@AllArgsConstructor
class OrderStep {
private Long orderId; // 订单ID
private String desc; // 操作描述
}
运行结果:
订单1-创建订单 发送到 Queue-1
订单1-支付 发送到 Queue-1 ← 相同订单,相同队列!
订单1-发货 发送到 Queue-1
订单1-确认收货 发送到 Queue-1
订单2-创建订单 发送到 Queue-2
订单2-支付 发送到 Queue-2
订单2-发货 发送到 Queue-2
订单2-确认收货 发送到 Queue-2
订单3-创建订单 发送到 Queue-0
订单3-支付 发送到 Queue-0
订单3-发货 发送到 Queue-0
订单3-确认收货 发送到 Queue-0
✅ 相同订单ID的消息,都进入了同一个队列!
消费者:顺序消费消息
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class OrderConsumer {
public static void main(String[] args) throws Exception {
// 1. 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
// 2. 订阅Topic
consumer.subscribe("OrderTopic", "*");
// 3. ⭐ 注册顺序消息监听器(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("线程[%s] 消费消息: %s, Queue=%d%n",
Thread.currentThread().getName(),
body,
msg.getQueueId());
// 模拟业务处理
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 返回成功
return ConsumeOrderlyStatus.SUCCESS;
}
});
// 4. 启动消费者
consumer.start();
System.out.println("顺序消费者启动成功!");
}
}
运行结果:
线程[Thread-1] 消费消息: 订单1-创建订单, Queue=1
线程[Thread-1] 消费消息: 订单1-支付, Queue=1 ← 顺序正确!
线程[Thread-1] 消费消息: 订单1-发货, Queue=1
线程[Thread-1] 消费消息: 订单1-确认收货, Queue=1
线程[Thread-2] 消费消息: 订单2-创建订单, Queue=2
线程[Thread-2] 消费消息: 订单2-支付, Queue=2
线程[Thread-2] 消费消息: 订单2-发货, Queue=2
线程[Thread-2] 消费消息: 订单2-确认收货, Queue=2
线程[Thread-3] 消费消息: 订单3-创建订单, Queue=0
线程[Thread-3] 消费消息: 订单3-支付, Queue=0
线程[Thread-3] 消费消息: 订单3-发货, Queue=0
线程[Thread-3] 消费消息: 订单3-确认收货, Queue=0
✅ 每个订单的消息按顺序消费!不同订单可以并行消费!
🔐 顺序消息的底层原理
生产者端
1. 选择Queue
orderId % queueSize = queueId
相同orderId → 相同queueId
2. 发送到指定Queue
相同Queue内的消息是有序的(先进先出)
图示:
Producer
│
┌──────────┼──────────┐
│ │ │
Order1 Order2 Order3
(orderId=1) (orderId=2) (orderId=3)
│ │ │
│ │ │
hash(1)%3=1 hash(2)%3=2 hash(3)%3=0
│ │ │
↓ ↓ ↓
Queue-1 Queue-2 Queue-0
[M1][M2] [M1][M2] [M1][M2]
[M3][M4] [M3][M4] [M3][M4]
消费者端
关键:Queue级别的锁 🔒
1. 消费者拉取消息前,先对Queue加锁
2. 同一时刻,一个Queue只能被一个线程消费
3. 消费完成后,释放锁
4. 其他线程才能消费这个Queue的下一条消息
对比:
| 类型 | 监听器 | 锁机制 | 顺序性 | 性能 |
|---|---|---|---|---|
| 顺序消费 | MessageListenerOrderly | Queue级别锁 | ✅ 有序 | 😐 中等 |
| 并发消费 | MessageListenerConcurrently | 无锁 | ❌ 无序 | ⚡ 高 |
图示:
MessageListenerOrderly(顺序消费):
Queue-0: [M1][M2][M3]
↓
🔒 加锁
↓
Thread-1 消费 M1
↓
Thread-1 消费 M2 ← 同一个线程,顺序处理
↓
Thread-1 消费 M3
↓
🔓 解锁
MessageListenerConcurrently(并发消费):
Queue-0: [M1][M2][M3]
↓
并发消费(无锁)
↙ ↓ ↘
Thread-1 Thread-2 Thread-3
消费M1 消费M2 消费M3 ← 同时消费,可能M2先处理完
⚠️ 顺序消息的注意事项
1. 消费失败的处理
问题:
Queue-0: [M1][M2][M3]
M1消费成功 ✅
M2消费失败 ❌ ← 怎么办?
M3还没消费 ⏳
处理逻辑:
@Override
public ConsumeOrderlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeOrderlyContext context
) {
for (MessageExt msg : msgs) {
try {
processMessage(msg);
return ConsumeOrderlyStatus.SUCCESS;
} catch (Exception e) {
log.error("消费失败", e);
// ⭐ 返回 SUSPEND_CURRENT_QUEUE_A_MOMENT
// 效果:暂停消费这个Queue一段时间,然后重试
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
}
重试机制:
M2消费失败 → 暂停Queue-0消费1秒 → 重试M2
→ 失败 → 暂停Queue-0消费2秒 → 重试M2
→ 失败 → 暂停Queue-0消费4秒 → 重试M2
→ ...
→ 失败次数过多 → 发送到死信队列
影响:
- ⚠️ Queue被阻塞,M3也无法消费
- ⚠️ 其他Queue不受影响
2. 消费者数量要≤Queue数量
4个Queue,5个消费者:
Queue-0 → Consumer-1
Queue-1 → Consumer-2
Queue-2 → Consumer-3
Queue-3 → Consumer-4
Consumer-5 (闲置) ❌
原因:一个Queue只能被一个消费者消费(顺序性要求)
3. 不要在消费代码中加锁
// ❌ 错误:不要自己加锁
synchronized (lock) {
processMessage(msg);
}
// ✅ 正确:RocketMQ已经帮你加锁了(Queue级别)
processMessage(msg);
⚛️ Part 2: 事务消息(Transactional Message)
🤔 什么是事务消息?
定义:保证本地事务和发送消息的原子性
问题场景:
业务流程:下单 + 扣减库存
try {
// 1. 本地事务:扣减库存
db.execute("UPDATE inventory SET stock = stock - 1 WHERE id = 1");
// 2. 发送消息:通知发货
mq.send("通知发货");
db.commit();
} catch (Exception e) {
db.rollback();
}
可能的问题:
问题1:先提交事务,再发消息
1. 提交数据库事务 ✅
2. 发送消息 ❌ 失败(网络故障)
结果:库存扣减了,但发货消息丢失!💀
问题2:先发消息,再提交事务
1. 发送消息 ✅
2. 提交数据库事务 ❌ 失败(违反约束)
结果:发货消息发出了,但库存没扣减!💀
两难境地:
先提交事务 → 消息可能丢失 💀
先发消息 → 事务可能失败 💀
事务消息的解决方案:
保证:
- 本地事务成功 → 消息一定发送 ✅
- 本地事务失败 → 消息一定不发送 ✅
- 要么都成功,要么都失败!
🎯 事务消息的业务场景
场景1:订单和库存 🛒
用户下单:
1. 创建订单(本地事务)
2. 扣减库存(发送消息到库存服务)
要求:订单创建成功,库存消息一定要发出
场景2:积分和优惠券 🎁
用户支付成功:
1. 增加积分(本地事务)
2. 发放优惠券(发送消息)
要求:积分增加了,优惠券消息一定要发出
场景3:账户和账单 💰
转账:
1. A账户扣款(本地事务)
2. 生成转账记录(发送消息)
要求:扣款成功,转账记录消息一定要发出
🔄 事务消息的执行流程
完整流程图:
Producer Broker Consumer
│ │ │
│ 1. 发送半消息(Half) │ │
├─────────────────────────>│ │
│ │ │
│ 2. 半消息存储成功 │ │
│<─────────────────────────┤ │
│ │ │
│ 3. 执行本地事务 │ │
├──┐ │ │
│ │ try { │ │
│ │ db.update(...) │ │
│ │ db.commit() │ │
│ │ } │ │
│<─┘ │ │
│ │ │
│ 4. 提交/回滚事务消息 │ │
├─────────────────────────>│ │
│ COMMIT_MESSAGE ✅ │ │
│ or │ │
│ ROLLBACK_MESSAGE ❌ │ │
│ │ │
│ │ 5. 消息可见(如果提交) │
│ ├────────────────────────>│
│ │ │
│ │ 6. 消费消息
│ │ ├──┐
│ │ │ │ processMessage()
│ │ │<─┘
│ │ │
│ │<────────────────────────┤
│ │ 7. ACK │
│ │ │
如果生产者宕机了怎么办? 🤔
Producer在执行本地事务时宕机 💀
Broker发现半消息长时间没有确认(超时)
Broker主动回查Producer:事务到底成功还是失败?
Producer查询本地事务状态,告诉Broker:
- COMMIT_MESSAGE:提交 ✅
- ROLLBACK_MESSAGE:回滚 ❌
- UNKNOWN:不确定(继续回查)🤔
💻 事务消息代码实现
生产者:发送事务消息
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
public class TransactionProducer {
public static void main(String[] args) throws Exception {
// 1. 创建事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer("transaction_producer_group");
producer.setNamesrvAddr("localhost:9876");
// 2. ⭐ 设置事务监听器
producer.setTransactionListener(new TransactionListener() {
/**
* 执行本地事务
* 半消息发送成功后,Broker会回调这个方法
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println("⭐ 开始执行本地事务...");
try {
// ⭐ 执行本地事务(比如:扣减库存)
String orderId = new String(msg.getBody());
boolean success = executeLocalBusiness(orderId);
if (success) {
System.out.println("✅ 本地事务执行成功,提交消息");
return LocalTransactionState.COMMIT_MESSAGE; // 提交消息
} else {
System.out.println("❌ 本地事务执行失败,回滚消息");
return LocalTransactionState.ROLLBACK_MESSAGE; // 回滚消息
}
} catch (Exception e) {
System.out.println("⚠️ 本地事务异常,回滚消息");
e.printStackTrace();
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
/**
* 检查本地事务状态
* 如果Producer宕机,Broker会回调这个方法进行回查
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("🔍 Broker回查本地事务状态...");
try {
// ⭐ 查询本地事务状态(从数据库查询)
String orderId = new String(msg.getBody());
boolean exists = checkLocalBusinessStatus(orderId);
if (exists) {
System.out.println("✅ 回查结果:事务已提交");
return LocalTransactionState.COMMIT_MESSAGE;
} else {
System.out.println("❌ 回查结果:事务已回滚");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
} catch (Exception e) {
System.out.println("⚠️ 回查异常,返回UNKNOW,稍后继续回查");
e.printStackTrace();
return LocalTransactionState.UNKNOW; // 未知状态,继续回查
}
}
});
// 3. 启动生产者
producer.start();
// 4. 发送事务消息
String orderId = "ORDER_" + System.currentTimeMillis();
Message msg = new Message(
"TransactionTopic",
"TagA",
orderId.getBytes()
);
System.out.println("📤 发送事务消息: " + orderId);
producer.sendMessageInTransaction(msg, null);
// 等待一段时间
Thread.sleep(Integer.MAX_VALUE);
producer.shutdown();
}
/**
* 执行本地业务(模拟)
*/
private static boolean executeLocalBusiness(String orderId) {
System.out.println("💼 执行本地业务:扣减库存,订单ID=" + orderId);
// 模拟数据库操作
try {
// jdbc.execute("UPDATE inventory SET stock = stock - 1");
// jdbc.execute("INSERT INTO orders VALUES (...)");
// jdbc.commit();
Thread.sleep(1000); // 模拟耗时
return true; // 模拟成功
} catch (Exception e) {
// jdbc.rollback();
return false;
}
}
/**
* 检查本地业务状态(模拟)
*/
private static boolean checkLocalBusinessStatus(String orderId) {
System.out.println("🔍 查询本地业务状态:订单ID=" + orderId);
// 从数据库查询订单是否存在
// SELECT COUNT(*) FROM orders WHERE order_id = ?
return true; // 模拟:订单已存在
}
}
消费者:消费事务消息
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
public class TransactionConsumer {
public static void main(String[] args) throws Exception {
// 创建消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction_consumer_group");
consumer.setNamesrvAddr("localhost:9876");
consumer.subscribe("TransactionTopic", "*");
// 注册消息监听器
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(
List<MessageExt> msgs,
ConsumeConcurrentlyContext context
) {
for (MessageExt msg : msgs) {
String orderId = new String(msg.getBody());
System.out.println("✅ 消费事务消息: " + orderId);
// 执行业务逻辑(比如:通知发货)
notifyWarehouse(orderId);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.println("事务消费者启动成功!");
}
private static void notifyWarehouse(String orderId) {
System.out.println("📦 通知仓库发货: " + orderId);
}
}
🔄 事务消息的执行流程(详细版)
正常流程(事务成功)✅
1️⃣ Producer发送半消息
Producer → Broker: "我要发送一条事务消息"
Broker: "好的,我先存储为半消息(Half Message)"
半消息特点:
- 存储在CommitLog中
- 消费者看不到(不可见)
- 标记为"预提交"状态
2️⃣ Broker确认半消息
Broker → Producer: "半消息已存储,你可以执行本地事务了"
3️⃣ Producer执行本地事务
try {
db.update("UPDATE inventory SET stock = stock - 1");
db.commit(); ✅ 成功
}
4️⃣ Producer提交消息
Producer → Broker: "本地事务成功,请提交消息"
5️⃣ Broker标记消息可见
Broker: "收到!我把消息标记为可见"
消息从"半消息"变为"正常消息"
6️⃣ Consumer消费消息
Consumer: "我看到消息了,开始消费"
Consumer: "消费成功!"
异常流程(事务失败)❌
1️⃣ Producer发送半消息
Producer → Broker: "我要发送一条事务消息"
Broker: "好的,我先存储为半消息"
2️⃣ Producer执行本地事务
try {
db.update("UPDATE inventory SET stock = stock - 1");
db.commit(); ❌ 失败(库存不足)
} catch (Exception e) {
// 事务失败
}
3️⃣ Producer回滚消息
Producer → Broker: "本地事务失败,请回滚消息"
4️⃣ Broker删除半消息
Broker: "收到!我删除这条半消息"
5️⃣ Consumer永远看不到这条消息
Consumer: "什么消息?我没看到啊!"
回查流程(Producer宕机)💀
1️⃣ Producer发送半消息
Producer → Broker: "我要发送一条事务消息"
Broker: "好的,我先存储为半消息"
2️⃣ Producer执行本地事务
try {
db.update("UPDATE inventory SET stock = stock - 1");
db.commit(); ✅ 成功
}
3️⃣ Producer准备提交消息时,宕机了!💀
Producer: (宕机)💀
Broker: "Producer怎么没反应了?"
4️⃣ Broker超时,开始回查
Broker等待60秒(默认)
Broker: "Producer还没确认,我主动问问他吧"
Broker → Producer: "你的事务执行成功了吗?"
5️⃣ Producer恢复后,回查本地事务状态
Producer: "让我查查数据库..."
Producer: SELECT COUNT(*) FROM orders WHERE order_id = ?
Producer: "查到了!事务已提交!"
Producer → Broker: "事务已成功,请提交消息"
6️⃣ Broker提交消息
Broker: "好的,我标记消息可见"
7️⃣ Consumer消费消息
Consumer: "我看到消息了,开始消费"
回查机制的参数:
# 回查间隔时间(默认60秒)
transactionCheckInterval=60000
# 最大回查次数(默认15次)
transactionCheckMax=15
# 第一次回查延迟时间(默认6秒)
transactionTimeout=6000
🎯 事务消息的底层原理
半消息(Half Message)的存储
RocketMQ的存储结构:
CommitLog(所有消息的物理存储)
├─ 正常消息:Topic = 实际Topic,可见
├─ 半消息:Topic = RMQ_SYS_TRANS_HALF_TOPIC,不可见 ⭐
└─ Op消息:记录半消息的操作(提交/回滚)
流程:
1. 发送半消息
→ 存储到CommitLog
→ Topic替换为 RMQ_SYS_TRANS_HALF_TOPIC
→ 消费者看不到(因为订阅的是原Topic)
2. 提交消息(COMMIT)
→ 从Half Topic读取原始消息
→ 恢复原始Topic
→ 构建索引,消息变为可见
→ 写入Op消息(标记已提交)
3. 回滚消息(ROLLBACK)
→ 写入Op消息(标记已回滚)
→ 半消息不会被构建索引,永远不可见
图示:
┌────────────────── CommitLog ──────────────────┐
│ │
│ Offset Topic Visible │
│ ------ ----- ------- │
│ 1000 OrderTopic YES │
│ 1001 RMQ_SYS_TRANS_HALF_TOPIC NO ← 半消息
│ 1002 OrderTopic YES │
│ 1003 RMQ_SYS_TRANS_OP_HALF - ← Op消息
│ 1004 OrderTopic YES │
│ │
└───────────────────────────────────────────────┘
消费者订阅 OrderTopic,只能看到 Offset 1000, 1002, 1004
看不到 Offset 1001(半消息)
⚠️ 事务消息的注意事项
1. 本地事务要幂等
场景:
Producer执行本地事务 → 成功
Producer准备提交消息 → 网络故障,提交失败
Broker回查 → Producer再次执行本地事务 → 重复执行!💀
解决:
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// ⭐ 不要再次执行事务,只查询事务状态!
boolean exists = checkTransactionStatus(msg); // 查询数据库
return exists ? COMMIT_MESSAGE : ROLLBACK_MESSAGE;
}
private boolean checkTransactionStatus(MessageExt msg) {
String orderId = new String(msg.getBody());
// ⭐ 查询数据库,判断订单是否已存在
return orderRepository.existsByOrderId(orderId);
}
2. 回查要快速返回
❌ 错误做法:
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 再次执行业务逻辑(错误!)
executeLocalBusiness(msg); // 可能很慢!
return COMMIT_MESSAGE;
}
✅ 正确做法:
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 快速查询状态
boolean committed = isTransactionCommitted(msg); // 查数据库,很快
return committed ? COMMIT_MESSAGE : ROLLBACK_MESSAGE;
}
原因:
- Broker有超时时间(默认6秒)
- 如果回查太慢,Broker会继续回查
- 导致大量重复回查,浪费资源
3. 回查次数有限制
配置:
# 最大回查次数:15次
transactionCheckMax=15
超过15次还是UNKNOW怎么办?
→ 消息被自动回滚(删除)
→ 相当于消息丢失!💀
解决:
- 确保回查逻辑正确
- 确保数据库可用
- 监控回查失败次数
4. 事务消息不支持延迟和批量
限制:
// ❌ 不支持延迟
msg.setDelayTimeLevel(3); // 无效!
// ❌ 不支持批量
producer.send(Arrays.asList(msg1, msg2, msg3)); // 无效!
// ✅ 只能单条发送
producer.sendMessageInTransaction(msg, null);
📊 顺序消息 vs 事务消息
| 特性 | 顺序消息 | 事务消息 |
|---|---|---|
| 解决的问题 | 消息顺序性 | 本地事务 + 发送消息的原子性 |
| 实现方式 | 相同Key进同一个Queue | 半消息 + 事务回查 |
| 性能 | 中等(Queue级别锁) | 较低(需要回查) |
| 可靠性 | 高 | 非常高 |
| 适用场景 | 订单状态流转、Binlog同步 | 订单和库存、积分和优惠券 |
| 监听器 | MessageListenerOrderly | TransactionListener |
| 是否支持批量 | 是 | 否 |
| 是否支持延迟 | 是 | 否 |
🎓 面试题速答
Q1: RocketMQ如何保证消息顺序性?
A:
- 生产者:使用MessageQueueSelector,相同业务ID的消息路由到同一个Queue
- Broker:同一个Queue内的消息FIFO(先进先出)
- 消费者:使用MessageListenerOrderly,Queue级别加锁,顺序消费
核心:相同业务的消息进同一个Queue,Queue内有序!
Q2: 全局顺序和分区顺序的区别?
A:
-
全局顺序:整个Topic只有1个Queue,所有消息全局有序
- 优点:100%有序
- 缺点:性能差,无法并发
-
分区顺序:多个Queue,相同Key的消息进同一个Queue
- 优点:性能高,支持并发
- 缺点:实现稍复杂
- **推荐使用!**⭐
Q3: 顺序消息消费失败怎么办?
A:
返回SUSPEND_CURRENT_QUEUE_A_MOMENT:
- 暂停这个Queue的消费
- 等待一段时间后重试
- 失败次数过多,发送到死信队列
影响:
- ⚠️ 这个Queue被阻塞,后续消息也无法消费
- ✅ 其他Queue不受影响
Q4: 什么是事务消息?
A: 保证本地事务和发送消息的原子性:
- 本地事务成功 → 消息一定发送 ✅
- 本地事务失败 → 消息一定不发送 ✅
实现:
- 发送半消息(Half Message)
- 执行本地事务
- 提交/回滚消息
回查机制:
- Producer宕机 → Broker主动回查事务状态
- 保证消息不丢失
Q5: 事务消息的流程?
A:
1. Producer发送半消息到Broker
2. Broker存储半消息(消费者看不到)
3. Producer执行本地事务
4. 成功 → Producer提交消息 → 消费者可见
5. 失败 → Producer回滚消息 → 消息删除
6. 超时 → Broker回查Producer → 决定提交还是回滚
Q6: 事务消息的回查机制?
A: 触发条件:Producer长时间没有确认消息状态
流程:
- Broker主动回调Producer的
checkLocalTransaction() - Producer查询本地事务状态(从数据库查询)
- 返回COMMIT/ROLLBACK/UNKNOW
- UNKNOW会继续回查,最多15次
注意:
- 回查要快(不要重新执行事务)
- 回查要准(查询数据库状态)
- 超过15次UNKNOW,消息自动回滚
🎯 最佳实践总结
顺序消息 ✅
1. 合理选择分区Key
- 同一业务的消息用相同Key
- 避免热点Key(某个Key消息量特别大)
2. 消费者数量 ≤ Queue数量
- 推荐:消费者数 = Queue数
3. 异常处理
- 失败重试有限次数
- 超过次数发死信队列
- 不要阻塞太久
4. 监控告警
- 监控消费延迟
- 监控重试次数
- 监控死信队列
事务消息 ✅
1. 本地事务要幂等
- 使用唯一ID防止重复
2. 回查要快速
- 只查询状态,不重新执行
- 从数据库查询
3. 回查要准确
- 明确判断事务是否成功
- 避免返回UNKNOW
4. 监控告警
- 监控回查次数
- 监控回查失败率
- 监控事务超时
5. 降级方案
- 回查失败后的补偿
- 人工介入机制
🎬 总结:一张图看懂顺序消息和事务消息
┌─────────────────────────────────────────────────────┐
│ 顺序消息 │
│ │
│ 问题:消息乱序 │
│ 解决:相同Key → 同一Queue → 顺序消费 │
│ │
│ Producer: │
│ ├─ 使用MessageQueueSelector │
│ └─ 相同orderId → 同一Queue │
│ │
│ Consumer: │
│ ├─ 使用MessageListenerOrderly │
│ └─ Queue级别锁,顺序消费 │
│ │
│ 适用场景: │
│ ├─ 订单状态流转 │
│ ├─ Binlog同步 │
│ └─ 需要严格顺序的场景 │
└─────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────┐
│ 事务消息 │
│ │
│ 问题:本地事务和消息发送不一致 │
│ 解决:半消息 + 事务回查 │
│ │
│ 流程: │
│ 1. 发送半消息(消费者看不到) │
│ 2. 执行本地事务 │
│ 3. 提交/回滚消息 │
│ 4. 如果超时,Broker主动回查 │
│ │
│ 适用场景: │
│ ├─ 订单和库存 │
│ ├─ 积分和优惠券 │
│ └─ 需要强一致性的场景 │
└─────────────────────────────────────────────────────┘
🎉 恭喜你!
你已经掌握了RocketMQ的顺序消息和事务消息!🎊
核心要点:
- 顺序消息:相同Key进同一Queue,Queue内有序
- 事务消息:半消息 + 本地事务 + 回查机制
下次面试,这样回答:
"RocketMQ的顺序消息通过MessageQueueSelector实现,相同业务ID的消息路由到同一个Queue,然后使用MessageListenerOrderly顺序消费。
事务消息通过半消息机制实现,先发送半消息(消费者不可见),然后执行本地事务,成功就提交消息,失败就回滚。如果Producer宕机,Broker会主动回查事务状态,保证消息一定能发送或一定不发送。
我们在订单系统中使用了顺序消息保证订单状态流转的顺序性,在库存系统中使用了事务消息保证本地事务和消息发送的一致性。"
面试官:👍 "非常专业!你对RocketMQ理解很深!"
🎈 表情包时间 🎈
学完RocketMQ顺序消息和事务消息后:
之前:
😵 "顺序乱了怎么办?事务和消息不一致怎么办?"
现在:
😎 "顺序消息!事务消息!轻松搞定!"
面试官:
😲 "这小子可以!"
本文完 🎬
记得点赞👍 收藏⭐ 分享🔗
上一篇: 184-Kafka的消息积压如何处理.md
下一篇: 186-RocketMQ的刷盘机制和主从同步.md
作者注:写完这篇,感觉可以开一家"消息中间件咨询公司"了!📮
如果这篇文章对你有帮助,请给我一个Star⭐!版权声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。