【架构实战】RocketMQ实战:分布式消息中间件

【架构实战】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全部不可达,触发告警
    }
}

八、总结与思考

核心知识点回顾

  1. 事务消息是RocketMQ的独门绝技:两阶段提交 + 事务状态回查,在不引入分布式事务框架的前提下,实现了"本地事务 + MQ消息"的强一致性。
  2. 顺序消息需要从生产到消费的全链路配合 :生产者按MessageQueue选择Key,消费者按ORDERLY模式消费,同一订单的所有消息进入同一队列。
  3. 延迟消息使用固定级别:RocketMQ内置18个延迟级别,适合"粗粒度延迟",精确延迟建议用定时任务。
  4. 幂等是永恒的主题:事务消息回查、顺序消费重试、延迟消息重试------所有可能导致消息重复的场景,都必须有幂等兜底。
  5. NameServer集群是高可用基础:多节点NameServer配置是生产环境的必须项。

思考题

  1. RocketMQ的事务消息和Kafka的幂等生产者都能解决"本地事务成功后消息发送失败"的问题,两者的实现思路有什么本质区别?各自的适用场景是什么?

  2. 在订单超时取消场景中,使用RocketMQ延迟消息和使用定时任务各有什么优缺点?如果让你设计一个精确到秒级的订单超时取消系统,你会怎么做?

  3. RocketMQ的顺序消费在实际业务中经常遇到"前一条消息卡住,后续消息全部等待"的问题。你有什么优化方案?能否设计一个"部分顺序"的消息消费机制?

  4. 如果让你设计一个消息系统的监控大盘,需要关注哪些核心指标?哪些指标的异常组合最可能预示着系统故障?

个人观点

RocketMQ是阿里巴巴多年双十一大促经验沉淀下来的作品,它的每一个特性都带着浓重的"电商交易"基因。事务消息、延迟消息、顺序消息------这些功能在互联网大厂的订单系统中是刚需,而RocketMQ将这些能力封装成了开箱即用的产品,这是它相对于Kafka最大的差异化优势。

然而,RocketMQ也并非银弹。它的NameServer架构在CAP理论中选择了AP (可用性+分区容忍),这意味着在极端网络分区情况下,客户端可能拿到不一致的路由信息。Kafka使用ZooKeeper/KRaft则天然选择了CP (一致性+分区容忍)。两者没有绝对的优劣,只是取舍不同------电商场景需要高可用(宁可慢一点也不能不可用),日志/流处理场景需要强一致(数据不能丢)。

我的建议是:在大规模消息系统的选型上,不要只看功能特性,更要理解每种中间件的设计哲学和取舍逻辑。技术选型没有银弹,适合业务场景的才是最好的。 同时,无论选择哪种消息中间件,幂等设计、监控告警、故障恢复预案都是不可或缺的------这些是跨越具体技术栈的通用能力。


写在最后

三篇文章写下来,从Kafka到RabbitMQ再到RocketMQ,我们见证了消息中间件的演进历程。每一种技术都有其诞生的时代背景和解决的特定问题。理解这些背景,比记住配置参数更重要。

愿你在消息系统的实践中,少踩坑,多积累,写出经得起生产环境考验的代码。

相关推荐
报错小能手1 小时前
分布式讲解—分布式事务解决方案 刚性(2PC、3PC、XA协议)
分布式
Cosolar2 小时前
智能体 Agent 完全拆解:架构、组件与实战指南
人工智能·架构·大模型·agent·智能体
OceanBase数据库官方博客2 小时前
现代数据架构:一套技术栈统一 TP、AP 与 AI
架构·oceanbase
刀法如飞2 小时前
Palantir技术原理深度分析:Ontology 存储结构与读写方式
人工智能·算法·架构
heimeiyingwang2 小时前
【架构实战】RabbitMQ实战:企业级消息可靠传递
架构·rabbitmq·ruby
__土块__2 小时前
AI Agent MCP架构设计与技术实现全面解析
ai·架构·agent·mcp·技术实现
ze^02 小时前
Day02 Web应用&架构类别&源码类别&镜像容器&建站模板&编译封装&前后端分离
前端·web安全·架构·安全架构
在繁华处3 小时前
轻棋局(五):多棋种统一架构
架构
Anastasiozzzz3 小时前
万字深度实战!AI Agent 接入万物的底层密码:MCP 协议传输机制与开发指南(下篇)
java·开发语言·数据库·人工智能·ai·架构