【架构实战】RocketMQ实战:分布式消息中间件
一、真实故事:双十一前夜的全链路压测惊魂
2022年双十一前夜,某电商平台的全链路压测进入最终阶段。当模拟订单量打到每秒10万笔时,团队信心满满------毕竟Kafka集群已经经过了两轮扩容,所有配置都经过了优化。
然而凌晨2点,监控大屏突然出现大量红色告警:消息发送延迟从毫秒级飙升至30秒,大量订单超时。更诡异的是,Kafka的生产者并没有报错,send()方法返回了成功,但消息就是没有在预期时间内到达消费者。
团队连夜排查,最终发现是一个"经典"配置陷阱:linger.ms=0(不等待凑批)和compression.type=snappy(CPU密集型压缩)的组合,在高并发场景下触发了CPU瓶颈------压缩线程跟不上发送线程,导致内存缓冲区爆满,消息被静默丢弃。
这个故事揭示了一个残酷的事实:Kafka的高性能是有条件的,一旦某个环节成为瓶颈,整个系统的表现会断崖式下降。 而这,正是RocketMQ诞生的背景------阿里巴巴需要一个专为电商交易场景设计的消息中间件,它不需要Kafka那么极致的吞吐量,但需要在可靠性、事务性、延迟消息等方面做到极致。
接下来,让我们深入RocketMQ的世界。
二、RocketMQ核心概念与架构原理
2.1 RocketMQ的诞生背景与定位
RocketMQ是阿里巴巴在2012年内部开发的分布式消息中间件,2016年捐赠给Apache基金会并于2017年成为Apache顶级项目。
与Kafka相比,RocketMQ的设计目标有显著差异:
| 对比维度 | RocketMQ | Kafka |
|---|---|---|
| 设计目标 | 交易级消息可靠传递 | 海量日志与流处理 |
| 事务消息 | 原生支持(半消息机制) | 需第三方实现 |
| 延迟消息 | 原生支持(18个级别) | 需插件或外部实现 |
| 消息顺序 | 支持严格顺序 | 仅分区有序 |
| 消费模式 | 推(Push)为主 | 拉(Pull)为主 |
| 重复消费控制 | 消息逻辑处理时间 | 手动offset管理 |
| 多租户 | 不支持 | 不支持 |
| 部署复杂度 | 中等 | 高(依赖ZooKeeper) |
2.2 RocketMQ核心术语体系
| 术语 | 解释 |
|---|---|
| Topic(主题) | 消息的一级分类,相当于Kafka的Topic |
| Tag(标签) | 消息的二级分类,RocketMQ特有,用于细粒度过滤 |
| Message Queue(消息队列) | Topic的物理分片,类似于Kafka的Partition |
| Broker | RocketMQ的服务节点,负责消息存储和转发 |
| NameServer | 元数据服务,类似于ZooKeeper,用于服务发现和路由 |
| Producer Group | 生产者分组,同组生产者承担负载均衡 |
| Consumer Group | 消费者分组,同组内消息负载均衡,不同组消息广播 |
| CommitLog | 消息存储的物理文件,所有Topic的消息都追加写入 |
| ConsumeQueue | 消息消费队列,索引文件,加速消息定位 |
| ConsumerQueue | 逻辑消费队列,按Topic和Queue组织 |
2.3 RocketMQ架构图
┌─────────────────────────────────────────────────────────────────────────────┐
│ RocketMQ 架构 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ NameServer-1 │◄──────────────────►│ NameServer-2 │ │
│ │ (注册中心/路由) │ 心跳同步 │ (注册中心/路由) │ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ 路由查询 │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Broker Cluster │ │
│ │ │ │
│ │ Master Broker-1 Master Broker-2 │ │
│ │ ├─ CommitLog ├─ CommitLog │ │
│ │ ├─ ConsumerQueue-A ├─ ConsumerQueue-A │ │
│ │ ├─ ConsumerQueue-B ├─ ConsumerQueue-B │ │
│ │ │ │ │ │
│ │ Slave Broker-1 Slave Broker-2 │ │
│ │ (热备) (热备) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────┐ ┌──────────────────────────┐ │
│ │ Producer Group │ │ Consumer Group │ │
│ │ ┌────────────────────┐ │ │ ┌────────────────────┐ │ │
│ │ │ Producer-1 │ │ │ │ Consumer-1 │ │ │
│ │ │ Producer-2 │ │ │ │ Consumer-2 │ │ │
│ │ │ Producer-3 │ │ │ │ Consumer-3 │ │ │
│ │ └────────────────────┘ │ │ └────────────────────┘ │ │
│ └──────────────────────────┘ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
2.4 RocketMQ存储架构:CommitLog + ConsumeQueue
RocketMQ的消息存储设计是其区别于Kafka的核心亮点。
┌─────────────────────────────────────────────────────────────────┐
│ RocketMQ 存储架构 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ CommitLog (顺序写) │ │
│ │ │ │
│ │ [Msg1][Msg2][Msg3][Msg4][Msg5][Msg6][Msg7][Msg8]... │ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │ │
│ │ 物理文件:/store/commitlog/0000000000 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ ▼ ▼ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ ConsumeQueue-0 │ │ ConsumeQueue-1 │ (按Topic-Queue索引) │
│ │ │ │ │ │
│ │ [offset|size| │ │ [offset|size| │ │
│ │ tag-hash] │ │ tag-hash] │ │
│ │ │ │ │ │
│ │ TopicA @ Queue0 │ │ TopicA @ Queue1 │ │
│ └────────────────┘ └────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ IndexFile (消息索引) │ │
│ │ 支持按Message Key / Unique Key快速定位消息 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
关键设计优势:
- 写放大优化:Kafka每个分区一个物理文件,高并发下文件句柄数爆炸;RocketMQ所有Topic共用CommitLog,大大减少文件句柄
- 顺序写的极致利用:消息总是追加到CommitLog末尾,即使高并发写入也是顺序的
- ConsumeQueue作为索引:消费时先读ConsumeQueue定位,再读CommitLog取消息,实现读写分离
2.5 高可用机制:主从同步
RocketMQ的高可用通过主从同步实现:
properties
# Broker配置(Master节点)
brokerClusterName = DefaultCluster
brokerName = broker-a
brokerId = 0 # 0=Master, >0=Slave
namesrvAddr = nameserver1:9876;nameserver2:9876
listenPort = 10911
storePathRootDir = /store/root
storePathCommitLog = /store/commitlog
# 刷盘策略:ASYNC_FLUSH(异步)或SYNC_FLUSH(同步)
flushDiskType = ASYNC_FLUSH
# 刷盘方式:同步刷盘(SYNC_MASTER)或异步刷盘(ASYNC_MASTER)
brokerRole = ASYNC_MASTER
# 消息副本数
dupSyncBrokerEnable = true
# Broker配置(Slave节点)
brokerId = 1 # Slave的brokerId必须大于0
brokerRole = SLAVE
haListenPort = 10912 # Slave的HA监听端口
三、事务消息:RocketMQ的杀手锏
3.1 为什么需要事务消息?
在分布式系统中,本地事务 + MQ消息的经典组合存在一个根本矛盾:
用户下单:
1. 事务1:创建订单(数据库)
2. 事务2:扣减库存(数据库)
3. 发送MQ消息 ------→ 问题:事务2失败了,但消息可能已经发出去了!
传统解决方案------本地消息表------虽然可行,但需要额外的数据库表和补偿任务,开发成本高。RocketMQ的事务消息机制提供了原生解决方案。
3.2 事务消息原理:半消息机制
RocketMQ事务消息的核心思想是"两阶段提交":
┌─────────────────────────────────────────────────────────────────────┐
│ RocketMQ 事务消息流程 │
│ │
│ 阶段1: 发送半消息 │
│ ┌──────────┐ │
│ │ Producer │ ──→ sendHalfMessage() ──→ RocketMQ │
│ │ │ │ │
│ └──────────┘ ▼ │
│ ┌──────────────┐ │
│ │ CommitLog │ │
│ │ (半消息已存储) │ │
│ │ status=HALF │ │
│ └──────────────┘ │
│ │ │
│ 阶段2: 执行本地事务 ───────────────────────────┘ │
│ │ │
│ ┌──────────┐ │ │
│ │ Producer │ ◄── executeLocalTransaction ──┘ │
│ │ │ ──→ DB事务 ──→ 提交/回滚本地事务 │
│ └──────────┘ │ │
│ ▼ │
│ 阶段3: 提交确认 ┌──────────────┐ │
│ ┌──────────┐ │ 提交/回滚确认 │ │
│ │ Producer │ ──→ commitLocalTransaction ──→│ status=COMMIT│ │
│ │ │ (COMMIT/ROLLBACK) │ 或 DROP │ │
│ └──────────┘ └──────────────┘ │
│ │
│ [补偿机制] ───────────────────────────────────────────────────── │
│ 如果阶段3超时或失败,Broker主动查询Producer的本地事务状态 │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ RocketMQ定时回调 ──→ Producer.checkLocalTransaction() ──→ │ │
│ │ 返回UNKNOW → 等待下次回调 │ │
│ │ 返回COMMIT → 提交消息 │ │
│ │ 返回ROLLBACK → 丢弃消息 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.3 事务消息代码实现
依赖配置
xml
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
yaml
rocketmq:
name-server: nameserver1:9876;nameserver2:9876
producer:
group: order-producer-group
# 事务消息必须开启
transactionCheckInterval: 3000 # 事务状态回查间隔(ms)
transactionCheckTimeout: 30000 # 事务超时时间(ms)
maxMessageSize: 10485760 # 最大消息大小(10MB)
retryTimesWhenSendAsyncFailed: 3 # 异步发送失败重试次数
事务消息生产者
java
@Service
@Slf4j
public class OrderTransactionProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 下单------事务消息模式
*
* 执行流程:
* 1. sendMessageInTransaction() 发送半消息
* 2. 执行本地数据库事务
* 3. 根据事务结果提交COMMIT或ROLLBACK
*/
public String createOrder(OrderCreateRequest request) {
String orderId = IdGenerator.generateOrderId();
String transactionId;
try {
// 构建订单消息
OrderMessage orderMessage = OrderMessage.builder()
.orderId(orderId)
.userId(request.getUserId())
.items(request.getItems())
.totalAmount(request.getTotalAmount())
.createdAt(LocalDateTime.now())
.build();
// 发送事务消息(关键API)
// executeLocalTransaction: 本地事务执行逻辑
// checkLocalTransaction: 事务状态回查逻辑
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-topic:tag-create", // Topic:Tag格式
MessageBuilder.withPayload(orderMessage)
.setHeader("orderId", orderId)
.setHeader("userId", request.getUserId())
.build(),
new TransactionListenerImpl() // 事务监听器
);
transactionId = result.getTransactionId();
log.info("事务消息发送, orderId={}, transactionId={}, status={}",
orderId, transactionId, result.getSendStatus());
// 事务提交失败则抛出异常
if (result.getSendStatus() != SendStatus.SEND_OK) {
throw new OrderCreateException("事务消息发送失败");
}
return orderId;
} catch (Exception e) {
log.error("创建订单异常, request={}", request, e);
throw new OrderCreateException("创建订单失败: " + e.getMessage());
}
}
}
事务监听器实现
java
/**
* 事务监听器
* 实现两个核心方法:
* 1. executeLocalTransaction: 执行本地事务,返回COMMIT/ROLLBACK/UNKNOWN
* 2. checkLocalTransaction: 回查本地事务状态
*/
@Slf4j
@Service
public class TransactionListenerImpl implements TransactionListener {
@Autowired
private OrderService orderService;
@Autowired
private OrderTransactionMapper orderTransactionMapper;
/**
* 执行本地事务
*
* 注意:
* - 这里执行的是真正的业务逻辑(订单创建、库存扣减等)
* - 必须幂等!因为RocketMQ可能多次回调此方法
* - 返回值决定消息的命运:
* COMMIT_MESSAGE → 消息被消费
* ROLLBACK_MESSAGE → 消息被丢弃
* UNKNOWN → 进入回查流程
*/
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String orderId = msg.getHeader("orderId");
long startTime = System.currentTimeMillis();
try {
// 解析消息体
OrderMessage orderMessage = JSON.parseObject(
new String(msg.getBody()), OrderMessage.class);
log.info("执行本地事务, orderId={}, transactionId={}",
orderId, msg.getTransactionId());
// 检查是否已处理(幂等)
OrderTransaction tx = orderTransactionMapper
.selectByTransactionId(msg.getTransactionId());
if (tx != null) {
log.info("事务已执行过, orderId={}, status={}",
orderId, tx.getStatus());
return LocalTransactionState.UNKNOWN;
}
// 执行本地事务(订单创建 + 库存扣减)
boolean success = orderService.createOrderInTransaction(orderMessage);
// 记录事务执行结果
orderTransactionMapper.insert(OrderTransaction.builder()
.transactionId(msg.getTransactionId())
.orderId(orderId)
.status(success ? "COMMIT" : "ROLLBACK")
.executeTime(LocalDateTime.now())
.build());
if (success) {
log.info("本地事务执行成功, orderId={}, cost={}ms",
orderId, System.currentTimeMillis() - startTime);
return LocalTransactionState.COMMIT_MESSAGE;
} else {
log.warn("本地事务执行失败, orderId={}", orderId);
return LocalTransactionState.ROLLBACK_MESSAGE;
}
} catch (Exception e) {
log.error("本地事务执行异常, orderId={}", orderId, e);
return LocalTransactionState.UNKNOWN; // 异常时回查
}
}
/**
* 回查本地事务状态
*
* 触发场景:
* 1. executeLocalTransaction返回UNKNOWN
* 2. RocketMQ长时间未收到COMMIT/ROLLBACK确认
* 3. Broker重启后恢复未决事务
*
* 回查策略:RocketMQ默认每3秒回查一次,最多重试15次
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String transactionId = msg.getTransactionId();
String orderId = msg.getHeader("orderId");
log.debug("回查事务状态, transactionId={}, orderId={}",
transactionId, orderId);
// 查询本地事务表
OrderTransaction tx = orderTransactionMapper
.selectByTransactionId(transactionId);
if (tx == null) {
// 事务记录不存在,可能本地事务还没执行(极端并发情况)
return LocalTransactionState.UNKNOWN;
}
if ("COMMIT".equals(tx.getStatus())) {
return LocalTransactionState.COMMIT_MESSAGE;
} else if ("ROLLBACK".equals(tx.getStatus())) {
return LocalTransactionState.ROLLBACK_MESSAGE;
} else {
// 如果回查次数过多(比如超过5次),直接回滚避免无限等待
if (tx.getRetryCount() > 5) {
log.error("事务回查次数超限, transactionId={}, retryCount={}",
transactionId, tx.getRetryCount());
return LocalTransactionState.ROLLBACK_MESSAGE;
}
return LocalTransactionState.UNKNOWN;
}
}
}
四、顺序消息
4.1 顺序消息的两种类型
RocketMQ支持两种顺序消息:
| 类型 | 说明 | 实现方式 |
|---|---|---|
| 分区顺序 | 同一分区内的消息严格有序,不同分区之间无序 | Producer按MessageQueue发送 |
| 全局顺序 | 所有消息严格有序 | 单队列 + 单Consumer |
4.2 分区顺序消息实现
java
@Service
@Slf4j
public class OrderStatusProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送订单状态消息------保证同一订单的消息有序
*
* 核心策略:
* 使用订单ID作为MessageQueue选择Key
* 同一订单的所有状态消息会进入同一个MessageQueue
* 同一个Consumer消费同一个MessageQueue,保证顺序
*/
public void sendOrderStatusMessage(OrderStatusEvent event) {
String orderId = event.getOrderId();
String body = JSON.toJSONString(event);
// 创建消息
Message<String> message = MessageBuilder.withPayload(body)
.setHeader("orderId", orderId)
.setHeader("status", event.getStatus())
.setHeader("timestamp", event.getTimestamp())
.build();
// 发送参数:使用订单ID作为MessageQueue选择Key
// RocketMQ会根据hash(orderId) % 队列数 选择队列
// 同一订单ID的所有消息会进入同一队列
SendResult result = rocketMQTemplate.syncSendOrderly(
"order-status-topic", // Topic
message, // 消息
orderId, // MessageQueue选择Key
3000 // 超时时间(ms)
);
log.info("发送订单状态消息, orderId={}, status={}, queueOffset={}",
orderId, event.getStatus(), result.getQueueOffset());
}
}
4.3 顺序消息消费者
java
@Component
@Slf4j
public class OrderStatusConsumer {
@Autowired
private OrderStatusService orderStatusService;
/**
* 消费订单状态消息------顺序消费
*
* 注意:
* 1. consumeMode = ConsumeMode.ORDERLY 表示顺序消费
* 2. messageModel = MessageModel.CLUSTERING 表示集群模式(负载均衡)
* 3. consumeThreadMin = consumeThreadMax = 1 保证单线程消费
* 但实际上RocketMQ的顺序消费是在队列维度保证的
* 消费者A消费Queue0,消费者B消费Queue1,各自学循各自队列的顺序
*/
@RocketMQMessageListener(
topic = "order-status-topic",
consumerGroup = "order-status-consumer-group",
consumeMode = ConsumeMode.ORDERLY, // 顺序消费模式
messageModel = MessageModel.CLUSTERING, // 集群模式
consumeThreadMin = 1,
consumeThreadMax = 1, // 单线程消费
maxReconsumeTimes = 3 // 最大重试次数
)
public class OrderStatusMessageListener
implements RocketMQListener<String> {
@Override
public void onMessage(String messageBody) {
OrderStatusEvent event = JSON.parseObject(messageBody,
OrderStatusEvent.class);
String orderId = event.getOrderId();
long startTime = System.currentTimeMillis();
try {
log.info("收到订单状态消息, orderId={}, status={}",
orderId, event.getStatus());
// 处理顺序:PAID → SHIPPED → COMPLETED
switch (event.getStatus()) {
case "PAID":
orderStatusService.handlePaid(event);
break;
case "SHIPPED":
orderStatusService.handleShipped(event);
break;
case "COMPLETED":
orderStatusService.handleCompleted(event);
break;
default:
log.warn("未知状态, orderId={}, status={}",
orderId, event.getStatus());
}
log.info("处理订单状态成功, orderId={}, status={}, cost={}ms",
orderId, event.getStatus(),
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("处理订单状态异常, orderId={}, error={}",
orderId, e.getMessage(), e);
// 顺序消费中,抛出异常会触发重试,但不会跳过当前消息
// 这保证了顺序的严格性
throw new RuntimeException("处理失败,触发重试", e);
}
}
}
}
五、延迟消息
5.1 RocketMQ延迟消息原理
RocketMQ原生支持延迟消息,通过设置消息的延迟级别实现。RocketMQ内置了18个延迟级别:
java
// RocketMQ支持的18个延迟级别(单位:秒)
// 1s, 5s, 10s, 30s, 1m, 2m, 3m, 4m, 5m, 6m, 7m, 8m, 9m, 10m, 20m, 30m, 1h, 2h
// 注意:延迟消息的精度不是非常高(秒级),如果需要精确延迟,建议使用定时任务
延迟消息的存储位置是SCHEDULE_TOPIC_XXXX系统Topic,延迟到期后会被投递到真实的目标Topic。
5.2 延迟消息代码
java
@Service
@Slf4j
public class OrderDelayMessageProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 发送延迟消息------订单超时未支付自动取消
*
* 使用延迟级别:
* Level 2 = 5秒(测试用)
* Level 5 = 1分钟(生产建议)
* Level 16 = 30分钟(大额订单)
*/
public void sendOrderTimeoutMessage(String orderId, int delayLevel) {
OrderTimeoutEvent event = OrderTimeoutEvent.builder()
.orderId(orderId)
.reason("PAYMENT_TIMEOUT")
.createdAt(System.currentTimeMillis())
.build();
Message<OrderTimeoutEvent> message = MessageBuilder
.withPayload(event)
.setHeader("orderId", orderId)
.build();
// 发送延迟消息
// delayLevel从1到18对应不同的延迟时间
SendResult result = rocketMQTemplate.send(
"order-topic", // 目标Topic
message, // 消息
3000, // 超时时间
delayLevel // 延迟级别(核心参数!)
);
log.info("发送延迟取消消息, orderId={}, delayLevel={}, msgId={}",
orderId, delayLevel, result.getMsgId());
}
/**
* 常用延迟级别速查表
*/
public static final Map<String, Integer> DELAY_LEVELS = new LinkedHashMap<>();
static {
DELAY_LEVELS.put("30秒", 1);
DELAY_LEVELS.put("1分钟", 2);
DELAY_LEVELS.put("5分钟", 4);
DELAY_LEVELS.put("30分钟", 16);
DELAY_LEVELS.put("1小时", 17);
DELAY_LEVELS.put("2小时", 18);
}
}
5.3 延迟消息消费
java
@RocketMQMessageListener(
topic = "order-topic",
tag = "tag-timeout",
consumerGroup = "order-timeout-consumer-group"
)
public class OrderTimeoutConsumer
implements RocketMQListener<OrderTimeoutEvent> {
@Autowired
private OrderService orderService;
@Override
public void onMessage(OrderTimeoutEvent event) {
String orderId = event.getOrderId();
log.info("收到订单超时消息, orderId={}, reason={}",
orderId, event.getReason());
try {
// 查询订单状态
Order order = orderService.getOrder(orderId);
if (order == null) {
log.warn("订单不存在, orderId={}", orderId);
return;
}
// 只有未支付的订单才取消
if (OrderStatus.UNPAID.equals(order.getStatus())) {
orderService.cancelOrder(orderId, "超时未支付");
log.info("订单已自动取消, orderId={}", orderId);
} else {
log.info("订单已支付,跳过取消, orderId={}, status={}",
orderId, order.getStatus());
}
} catch (Exception e) {
log.error("处理超时消息异常, orderId={}", orderId, e);
throw e; // 触发重试
}
}
}
六、实战案例:电商订单系统全链路消息架构
6.1 系统整体设计
┌─────────────────────────────────────────────────────────────────────────┐
│ 订单全链路消息架构 │
│ │
│ 用户下单 │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Transaction Producer │ ──事务消息──→ order-topic (半消息) │
│ │ (订单创建+库存扣减) │ │
│ └──────────┬───────────┘ │
│ │ 事务提交 │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ order-topic │ │
│ │ (正常消息) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌────────┼────────┬─────────────┐ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────┐ ┌─────┐ ┌──────┐ ┌────────────┐ │
│ │支付 │ │物流 │ │积分 │ │延迟消息 │ │
│ │消费 │ │消费 │ │消费 │ │(30分钟超时)│ │
│ │者 │ │者 │ │者 │ │ │ │
│ └──┬──┘ └──┬──┘ └──┬───┘ └─────┬──────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 消费者幂等处理 │ │
│ │ (Redis布隆过滤器 + 状态机) │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
6.2 完整代码实现
6.2.1 订单服务
java
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryService inventoryService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderTimeoutService timeoutService;
/**
* 创建订单------事务消息模式
*/
@Override
@Transactional
public String createOrder(OrderCreateRequest request) {
String orderId = IdGenerator.generateOrderId();
// 1. 扣减库存(必须先扣,否则超卖风险大)
boolean inventoryReserved = inventoryService.reserveStock(
request.getUserId(),
request.getItems()
);
if (!inventoryReserved) {
throw new BusinessException("库存不足");
}
// 2. 创建订单
Order order = Order.builder()
.orderId(orderId)
.userId(request.getUserId())
.items(request.getItems())
.totalAmount(request.getTotalAmount())
.status(OrderStatus.UNPAID)
.createdAt(LocalDateTime.now())
.build();
orderMapper.insert(order);
// 3. 发送延迟消息------30分钟超时未支付自动取消
timeoutService.scheduleOrderTimeout(orderId, 5); // Level 5 = 1分钟
log.info("订单创建成功, orderId={}", orderId);
return orderId;
}
/**
* 订单支付成功
*/
@Override
public void payOrder(String orderId, String paymentId) {
Order order = orderMapper.selectById(orderId);
if (order == null) {
throw new BusinessException("订单不存在");
}
if (!OrderStatus.UNPAID.equals(order.getStatus())) {
log.warn("订单状态不正确, orderId={}, status={}",
orderId, order.getStatus());
return;
}
// 更新状态
order.setStatus(OrderStatus.PAID);
order.setPaymentId(paymentId);
order.setPaidAt(LocalDateTime.now());
orderMapper.update(order);
// 发送支付成功消息
OrderPaidEvent event = OrderPaidEvent.builder()
.orderId(orderId)
.paymentId(paymentId)
.userId(order.getUserId())
.totalAmount(order.getTotalAmount())
.paidAt(System.currentTimeMillis())
.build();
rocketMQTemplate.asyncSend("order-event:paid",
MessageBuilder.withPayload(event).build(),
new SendCallback() {
@Override
public void onSuccess(SendResult result) {
log.info("支付事件发送成功, orderId={}, msgId={}",
orderId, result.getMsgId());
}
@Override
public void onException(Throwable e) {
log.error("支付事件发送失败, orderId={}", orderId, e);
}
});
}
}
6.2.2 物流服务消费者
java
@Component
@Slf4j
public class LogisticsConsumer {
@Autowired
private LogisticsService logisticsService;
@Autowired
private IdempotentService idempotentService;
/**
* 消费支付成功消息------触发物流调度
*/
@RocketMQMessageListener(
topic = "order-event",
tag = "paid",
consumerGroup = "logistics-consumer-group"
)
public class OrderPaidListener
implements RocketMQListener<OrderPaidEvent> {
@Override
public void onMessage(OrderPaidEvent event) {
String orderId = event.getOrderId();
String msgId = event.getMsgId();
long startTime = System.currentTimeMillis();
try {
// ========== 幂等检查 ==========
if (idempotentService.isProcessed(msgId)) {
log.info("消息已处理过, msgId={}, orderId={}",
msgId, orderId);
return;
}
log.info("收到支付成功事件, orderId={}, paymentId={}",
orderId, event.getPaymentId());
// ========== 业务处理 ==========
LogisticsResult result = logisticsService.dispatch(orderId);
// ========== 标记已处理 ==========
idempotentService.markProcessed(msgId, "LOGISTICS_DISPATCHED");
log.info("物流调度成功, orderId={}, logisticsId={}, cost={}ms",
orderId, result.getLogisticsId(),
System.currentTimeMillis() - startTime);
} catch (Exception e) {
log.error("物流调度异常, orderId={}", orderId, e);
throw new RuntimeException("物流调度失败", e);
}
}
}
}
6.2.3 幂等服务
java
@Service
@Slf4j
public class IdempotentService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String PROCESSED_KEY_PREFIX = "msg:processed:";
private static final long EXPIRE_SECONDS = 86400; // 24小时过期
/**
* 检查消息是否已处理
*/
public boolean isProcessed(String messageId) {
return Boolean.TRUE.equals(
redisTemplate.hasKey(PROCESSED_KEY_PREFIX + messageId));
}
/**
* 标记消息已处理(原子操作)
*
* 使用Redis SETNX保证幂等:
* - 如果key不存在,SET成功,返回true,消息首次处理
* - 如果key已存在,SET失败,返回false,消息重复
*/
public boolean markProcessed(String messageId, String businessKey) {
String key = PROCESSED_KEY_PREFIX + messageId;
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(key, businessKey,
Duration.ofSeconds(EXPIRE_SECONDS));
return Boolean.TRUE.equals(success);
}
/**
* 双重检查模式(更严格)
* 先检查再标记,使用Redis事务保证原子性
*/
public void processWithIdempotency(String messageId,
Runnable businessLogic) {
String key = PROCESSED_KEY_PREFIX + messageId;
// 使用Redis WATCH保证检查+标记的原子性
redisTemplate.execute(new SessionCallback<Object>() {
@Override
public Object execute(RedisOperations operations)
throws DataAccessException {
operations.watch(key);
if (operations.hasKey(key)) {
operations.unwatch();
log.info("消息已处理,跳过, messageId={}", messageId);
return null;
}
operations.multi();
operations.opsForValue().set(key, "PROCESSING");
List<Object> results = operations.exec();
if (results == null || results.isEmpty()) {
// 事务被打断(key被其他线程修改)
log.info("并发冲突,消息可能被其他线程处理, messageId={}",
messageId);
return null;
}
// 执行业务逻辑
try {
businessLogic.run();
operations.opsForValue().set(key, "COMPLETED",
Duration.ofSeconds(EXPIRE_SECONDS));
} catch (Exception e) {
// 业务失败,删除标记,允许重试
operations.delete(key);
throw e;
}
return null;
}
});
}
}
七、踩坑实录
坑1:事务消息状态回查导致消息重复
症状:同一个订单被处理了两次,导致库存被重复扣减。用户投诉"扣了两次钱"。
根因分析 :事务消息的回查机制在网络抖动时会导致executeLocalTransaction被多次调用,但业务代码没有做幂等处理。
解决方案:
java
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String orderId = msg.getHeader("orderId");
// 幂等检查(必须!)
OrderTransaction existingTx = orderTransactionMapper
.selectByOrderId(orderId);
if (existingTx != null) {
// 已处理过,根据历史状态决定
return "COMMIT".equals(existingTx.getStatus()) ?
LocalTransactionState.COMMIT_MESSAGE :
LocalTransactionState.ROLLBACK_MESSAGE;
}
// 执行本地事务...
boolean success = orderService.createOrderInTransaction(...);
return success ?
LocalTransactionState.COMMIT_MESSAGE :
LocalTransactionState.ROLLBACK_MESSAGE;
}
坑2:顺序消费的死锁陷阱
症状:订单处理线程"卡死",日志显示一直在等待某把锁。消费完全停摆。
根因分析 :顺序消费模式下,如果一个消息的处理依赖外部服务(比如RPC调用),而该RPC服务的超时时间设置过长(30秒),且消费者使用了synchronized做并发控制------当同一把锁被长耗时操作持有时,后续消息全部阻塞。
解决方案:
java
@RocketMQMessageListener(
topic = "order-status-topic",
consumeMode = ConsumeMode.ORDERLY // 注意:顺序模式下不能使用并发锁
)
public void onMessage(OrderStatusEvent event) {
// 顺序消费的正确姿势:
// 1. 不要在业务逻辑中使用分布式锁或长耗时同步调用
// 2. 如果必须依赖外部服务,使用异步 + 状态回查
// 3. 或者将耗时操作移出消息消费链路,用定时任务处理
try {
// 快速处理,不阻塞
processOrderStatus(event);
} catch (Exception e) {
// 抛出异常会触发消息重试,但不会死锁
throw e;
}
}
坑3:延迟消息级别不够精细
症状:需要实现"订单创建后10分钟未支付取消",但Level 5=1分钟,Level 6=2分钟,下一个是Level 7=3分钟,无法精确到10分钟。
根因分析:RocketMQ延迟消息使用固定级别,不支持自定义延迟时间(不支持任意毫秒级延迟)。
解决方案:
方案A:使用定时任务(更精确,推荐)
java
// 定时任务方案(精确到秒级)
@Service
public class OrderTimeoutScheduler {
@Autowired
private OrderMapper orderMapper;
// 每分钟执行
@Scheduled(cron = "0 * * * * ?")
public void checkUnpaidOrders() {
// 查询30分钟前创建且仍未支付的订单
LocalDateTime threshold = LocalDateTime.now().minusMinutes(30);
List<Order> expiredOrders = orderMapper
.findUnpaidOrdersBefore(threshold);
for (Order order : expiredOrders) {
orderService.cancelOrder(order.getOrderId(), "超时未支付");
log.info("超时订单已取消, orderId={}", order.getOrderId());
}
}
}
方案B:使用RocketMQ的延迟消息级别(粗粒度,非精确)
java
// Level 4 = 5分钟,接近10分钟的需求
// 适合对时间精度要求不高的场景
timeoutService.scheduleOrderTimeout(orderId, 4);
坑4:消费端消息堆积的幽灵
症状:消费者运行正常,但lag持续增长,消息越积越多。重启消费者后短暂正常,之后再次堆积。
根因分析 :消费者maxReconsumeTimes设置过大(如10次),当消息反复处理失败时,每次重试间隔指数增长(1s→2s→4s→8s...),导致消息在重试队列中长时间占据位置无法被消费。
解决方案:
java
@RocketMQMessageListener(
topic = "order-event",
maxReconsumeTimes = 3, // 最大重试3次,不要设置过大
// 重试间隔由RocketMQ控制,默认:1s, 2s, 3s, 4s, 5s, 6s, 7s, 8s, 9s, 10s
// 然后进入死信队列
)
public void onMessage(String message) {
try {
process(message);
} catch (Exception e) {
log.error("处理失败,触发重试", e);
throw e; // 抛出异常,触发RocketMQ重试
}
}
同时配置死信队列处理:
java
// 死信队列配置
@Bean
public ConsumerGroupAttributesAttributes deadLetterConsumerGroup() {
// 死信队列的Consumer Group
// 死信队列中的消息需要人工干预或单独的处理逻辑
return null;
}
坑5:NameServer单点故障导致全链路不可用
症状:RocketMQ客户端启动失败,所有消息发送失败。日志显示"connect to nameserver timeout"。
根因分析:只配置了一个NameServer节点,NameServer挂了之后客户端完全失联。
解决方案:
yaml
rocketmq:
name-server: nameserver1:9876;nameserver2:9876;nameserver3:9876
# 多节点配置,任意一个可用即可
同时在代码中添加容错逻辑:
java
@Component
public class RocketMQConnectionHealth {
@Scheduled(fixedDelay = 30000)
public void monitorConnection() {
// 监控连接状态
// 如果NameServer全部不可达,触发告警
}
}
八、总结与思考
核心知识点回顾
- 事务消息是RocketMQ的独门绝技:两阶段提交 + 事务状态回查,在不引入分布式事务框架的前提下,实现了"本地事务 + MQ消息"的强一致性。
- 顺序消息需要从生产到消费的全链路配合 :生产者按MessageQueue选择Key,消费者按
ORDERLY模式消费,同一订单的所有消息进入同一队列。 - 延迟消息使用固定级别:RocketMQ内置18个延迟级别,适合"粗粒度延迟",精确延迟建议用定时任务。
- 幂等是永恒的主题:事务消息回查、顺序消费重试、延迟消息重试------所有可能导致消息重复的场景,都必须有幂等兜底。
- NameServer集群是高可用基础:多节点NameServer配置是生产环境的必须项。
思考题
-
RocketMQ的事务消息和Kafka的幂等生产者都能解决"本地事务成功后消息发送失败"的问题,两者的实现思路有什么本质区别?各自的适用场景是什么?
-
在订单超时取消场景中,使用RocketMQ延迟消息和使用定时任务各有什么优缺点?如果让你设计一个精确到秒级的订单超时取消系统,你会怎么做?
-
RocketMQ的顺序消费在实际业务中经常遇到"前一条消息卡住,后续消息全部等待"的问题。你有什么优化方案?能否设计一个"部分顺序"的消息消费机制?
-
如果让你设计一个消息系统的监控大盘,需要关注哪些核心指标?哪些指标的异常组合最可能预示着系统故障?
个人观点
RocketMQ是阿里巴巴多年双十一大促经验沉淀下来的作品,它的每一个特性都带着浓重的"电商交易"基因。事务消息、延迟消息、顺序消息------这些功能在互联网大厂的订单系统中是刚需,而RocketMQ将这些能力封装成了开箱即用的产品,这是它相对于Kafka最大的差异化优势。
然而,RocketMQ也并非银弹。它的NameServer架构在CAP理论中选择了AP (可用性+分区容忍),这意味着在极端网络分区情况下,客户端可能拿到不一致的路由信息。Kafka使用ZooKeeper/KRaft则天然选择了CP (一致性+分区容忍)。两者没有绝对的优劣,只是取舍不同------电商场景需要高可用(宁可慢一点也不能不可用),日志/流处理场景需要强一致(数据不能丢)。
我的建议是:在大规模消息系统的选型上,不要只看功能特性,更要理解每种中间件的设计哲学和取舍逻辑。技术选型没有银弹,适合业务场景的才是最好的。 同时,无论选择哪种消息中间件,幂等设计、监控告警、故障恢复预案都是不可或缺的------这些是跨越具体技术栈的通用能力。
写在最后
三篇文章写下来,从Kafka到RabbitMQ再到RocketMQ,我们见证了消息中间件的演进历程。每一种技术都有其诞生的时代背景和解决的特定问题。理解这些背景,比记住配置参数更重要。
愿你在消息系统的实践中,少踩坑,多积累,写出经得起生产环境考验的代码。