Java 整合 RocketMQ 实战:从入门到复杂场景落地

1. 引言

RocketMQ 是阿里巴巴开源的一款高性能、低延迟的分布式消息中间件,经过多年双十一大促的锤炼,在可靠性、吞吐量和功能丰富度上都表现优异。相比 Kafka,RocketMQ 提供了更完善的事务消息、延迟消息、顺序消息和消息过滤能力,非常适合电商、金融、物流等对消息可靠性要求较高的业务场景。

本文将从零开始,带你用 Java 整合 RocketMQ,从环境搭建、基础收发消息,逐步深入到顺序消息、事务消息、延迟消息、批量消息、消息过滤等复杂场景,并给出生产环境的最佳实践。

2. 环境准备

2.1 安装 RocketMQ

推荐使用 Docker 快速启动 RocketMQ 4.9.x 或 5.x 版本:

bash 复制代码
# 拉取镜像
docker pull apache/rocketmq:5.1.4

# 启动 NameServer
docker run -d --name rmqnamesrv -p 9876:9876 apache/rocketmq:5.1.4 sh mqnamesrv

# 启动 Broker
docker run -d --name rmqbroker \
  -p 10911:10911 -p 10909:10909 \
  --link rmqnamesrv \
  -e "NAMESRV_ADDR=rmqnamesrv:9876" \
  apache/rocketmq:5.1.4 sh mqbroker

2.2 引入 Maven 依赖

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-client</artifactId>
    <version>5.1.4</version>
</dependency>

如果使用 Spring Boot,推荐引入官方 starter:

xml 复制代码
<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.3.0</version>
</dependency>

2.3 配置 NameServer 地址

在 application.yml 中配置:

yaml 复制代码
rocketmq:
  name-server: 127.0.0.1:9876
  producer:
    group: order-producer-group
  consumer:
    group: order-consumer-group

3. 基础消息收发

3.1 生产者发送同步消息

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.client.producer.SendResult;

public class SyncProducer {
    public static void main(String[] args) throws Exception {
        // 创建生产者,指定生产者组
        DefaultMQProducer producer = new DefaultMQProducer("order-producer-group");
        // 设置 NameServer 地址
        producer.setNamesrvAddr("127.0.0.1:9876");
        // 启动生产者
        producer.start();

        for (int i = 0; i < 10; i++) {
            String body = "Hello RocketMQ " + i;
            Message msg = new Message("OrderTopic", "TagA", body.getBytes());
            // 发送同步消息,会阻塞等待 Broker 返回结果
            SendResult result = producer.send(msg);
            System.out.printf("发送结果: %s, msgId: %s%n", result.getSendStatus(), result.getMsgId());
        }

        // 关闭生产者
        producer.shutdown();
    }
}

3.2 消费者接收消息

java 复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class SyncConsumer {
    public static void main(String[] args) throws Exception {
        // 创建消费者,指定消费者组
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        // 订阅主题,TagA 表示只消费 TagA 标签的消息,* 表示全部
        consumer.subscribe("OrderTopic", "TagA || TagB");

        // 注册并发消费监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("收到消息: %s, 内容: %s%n",
                            new String(msg.getBody()), msg.getMsgId());
                }
                // 返回 CONSUME_SUCCESS 表示消费成功
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.println("消费者已启动,等待消息...");
    }
}

4. 复杂场景一:顺序消息

顺序消息要求消息按照发送顺序被消费,典型场景如订单状态流转(创建 → 支付 → 发货 → 完成)。RocketMQ 通过消息队列(MessageQueue) 实现顺序性:同一业务 key 的消息发送到同一个队列,消费者按队列顺序消费。

4.1 顺序消息生产者

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.MessageQueueSelector;
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 {
        DefaultMQProducer producer = new DefaultMQProducer("order-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();

        // 模拟一个订单的状态流转
        String orderId = "ORDER_10001";
        String[] states = {"创建订单", "支付成功", "发货", "交易完成"};

        for (String state : states) {
            Message msg = new Message("OrderTopic", "OrderState",
                    (orderId + ":" + state).getBytes());
            // 通过 MessageQueueSelector 将同一订单的消息路由到同一个队列
            SendResult result = producer.send(msg, new MessageQueueSelector() {
                @Override
                public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
                    // 根据订单号取模,保证同一订单进入同一队列
                    Long id = (Long) arg;
                    int index = (int) (id % mqs.size());
                    return mqs.get(index);
                }
            }, Long.parseLong(orderId.split("_")[1]));

            System.out.printf("发送状态: %s, 队列: %s%n", state, result.getMessageQueue().getQueueId());
        }

        producer.shutdown();
    }
}

4.2 顺序消息消费者

java 复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class OrderConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("order-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("OrderTopic", "OrderState");

        // 注册顺序消费监听器
        consumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs,
                                                       ConsumeOrderlyContext context) {
                // 默认自动提交消费进度,可通过 context.setAutoCommit(false) 手动控制
                for (MessageExt msg : msgs) {
                    System.out.printf("顺序消费: %s%n", new String(msg.getBody()));
                }
                return ConsumeOrderlyStatus.SUCCESS;
            }
        });

        consumer.start();
        System.out.println("顺序消费者已启动...");
    }
}

要点:

  • 生产者必须使用 MessageQueueSelector 将同一业务 key 的消息路由到同一队列。
  • 消费者必须使用 MessageListenerOrderly,且消费线程数建议设为 1(consumer.setConsumeThreadMin/Max(1))。
  • 顺序消息的吞吐量会低于并发消费,仅对真正需要顺序的业务使用。

5. 复杂场景二:事务消息

事务消息用于解决本地事务与消息发送的一致性问题,典型场景如:下单后扣减库存、创建订单后发送积分。RocketMQ 事务消息流程:

  1. 生产者发送半消息(对消费者不可见)。
  2. 执行本地事务。
  3. 根据本地事务结果提交或回滚消息。
  4. 若本地事务长时间未返回结果,Broker 会回查事务状态。

5.1 事务消息生产者

java 复制代码
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;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TransactionProducer {
    public static void main(String[] args) throws Exception {
        TransactionMQProducer producer = new TransactionMQProducer("tx-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");

        // 事务回查线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);
        producer.setExecutorService(executor);

        // 事务监听器
        producer.setTransactionListener(new TransactionListener() {
            // 本地事务状态缓存,用于回查
            private final ConcurrentHashMap<String, LocalTransactionState> localTx =
                    new ConcurrentHashMap<>();

            @Override
            public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
                String orderId = (String) arg;
                try {
                    // 执行本地事务,例如:创建订单 + 扣减库存
                    boolean success = createOrderAndDeductStock(orderId);
                    if (success) {
                        // 本地事务成功,提交消息
                        localTx.put(orderId, LocalTransactionState.COMMIT_MESSAGE);
                        return LocalTransactionState.COMMIT_MESSAGE;
                    }
                    // 本地事务失败,回滚消息
                    localTx.put(orderId, LocalTransactionState.ROLLBACK_MESSAGE);
                    return LocalTransactionState.ROLLBACK_MESSAGE;
                } catch (Exception e) {
                    // 异常时返回 UNKNOW,等待 Broker 回查
                    localTx.put(orderId, LocalTransactionState.UNKNOW);
                    return LocalTransactionState.UNKNOW;
                }
            }

            @Override
            public LocalTransactionState checkLocalTransaction(MessageExt msg) {
                // Broker 回查本地事务状态
                String orderId = new String(msg.getBody());
                LocalTransactionState state = localTx.get(orderId);
                if (state != null) {
                    return state;
                }
                // 未查到,可查询数据库确认订单状态
                return queryOrderStatusFromDB(orderId);
            }

            private boolean createOrderAndDeductStock(String orderId) {
                // 模拟本地事务
                return true;
            }

            private LocalTransactionState queryOrderStatusFromDB(String orderId) {
                // 模拟数据库查询
                return LocalTransactionState.COMMIT_MESSAGE;
            }
        });

        producer.start();

        // 发送事务消息
        String orderId = "TX_ORDER_20241022";
        Message msg = new Message("TxOrderTopic", "OrderCreate",
                orderId.getBytes());
        // 第二个参数 arg 会传给 executeLocalTransaction
        producer.sendMessageInTransaction(msg, orderId);

        System.out.println("事务消息已发送,等待本地事务执行...");
        Thread.sleep(5000);
        producer.shutdown();
    }
}

5.2 事务消息消费者

事务消息的消费者与普通消费者完全一致,因为事务消息对消费者是最终可见的:

java 复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
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("tx-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("TxOrderTopic", "*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    System.out.printf("事务消息消费: %s%n", new String(msg.getBody()));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.println("事务消息消费者已启动...");
    }
}

要点:

  • 事务消息只保证最终一致性,不保证实时性。
  • executeLocalTransaction 返回 UNKNOW 时,Broker 会按固定间隔回查(默认 6 次)。
  • 回查逻辑必须能根据消息内容查询到本地事务的真实状态,建议落库记录事务状态。

6. 复杂场景三:延迟消息

延迟消息用于「延迟一定时间后再消费」的场景,如:订单超时未支付自动取消、定时提醒。RocketMQ 支持 18 个延迟级别,不支持任意秒级延迟:

延迟级别 延迟时间
1 1s
2 5s
3 10s
4 30s
5 1min
... ...
18 2h

6.1 发送延迟消息

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;

public class DelayProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("delay-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();

        Message msg = new Message("DelayTopic", "OrderTimeout",
                "订单超时检查".getBytes());
        // 设置延迟级别 4,即延迟 30 秒
        msg.setDelayTimeLevel(4);

        producer.send(msg);
        System.out.println("延迟消息已发送,30 秒后消费者才能收到");

        producer.shutdown();
    }
}

6.2 消费延迟消息

java 复制代码
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;

public class DelayConsumer {
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("delay-consumer-group");
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("DelayTopic", "*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
                                                            ConsumeConcurrentlyContext context) {
                for (MessageExt msg : msgs) {
                    // 打印消息的存储时间和当前时间,验证延迟效果
                    long delay = System.currentTimeMillis() - msg.getStoreTimestamp();
                    System.out.printf("收到延迟消息: %s, 已延迟 %d ms%n",
                            new String(msg.getBody()), delay);
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });

        consumer.start();
        System.out.println("延迟消息消费者已启动...");
    }
}

要点:

  • 延迟级别是固定的 18 档,无法自定义秒级延迟;如需秒级延迟,可自行实现「定时任务 + 普通消息」方案。
  • 延迟消息在 Broker 端会先写入延迟队列,到期后再投递到目标 Topic,因此会占用额外的存储。

7. 复杂场景四:批量消息与消息过滤

7.1 批量发送消息

批量发送能显著提升吞吐量,但单批消息总大小不能超过 4MB(默认):

java 复制代码
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;

import java.util.ArrayList;
import java.util.List;

public class BatchProducer {
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("batch-producer-group");
        producer.setNamesrvAddr("127.0.0.1:9876");
        producer.start();

        List<Message> messages = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            Message msg = new Message("BatchTopic", "TagA",
                    ("批量消息 " + i).getBytes());
            messages.add(msg);
        }

        // 批量发送
        producer.send(messages);
        System.out.println("批量消息发送完成");

        producer.shutdown();
    }
}

7.2 消息过滤

RocketMQ 支持两种过滤方式:Tag 过滤 和 SQL92 属性过滤。

**Tag 过滤

8. 复杂场景五:支付回调(微信支付 + 支付宝支付)

支付回调是电商系统中典型的异步场景:微信支付和支付宝支付成功后,支付平台会通过 HTTP 回调通知商户系统。由于回调可能重复发送、顺序不定、且需要保证幂等,非常适合用 RocketMQ 做削峰填谷 + 异步解耦 + 可靠消费。

8.1 整体架构

#mermaid-svg-PeoTyfMuFiCtp7X5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PeoTyfMuFiCtp7X5 .error-icon{fill:#552222;}#mermaid-svg-PeoTyfMuFiCtp7X5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PeoTyfMuFiCtp7X5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .marker.cross{stroke:#333333;}#mermaid-svg-PeoTyfMuFiCtp7X5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PeoTyfMuFiCtp7X5 p{margin:0;}#mermaid-svg-PeoTyfMuFiCtp7X5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .cluster-label text{fill:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .cluster-label span{color:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .cluster-label span p{background-color:transparent;}#mermaid-svg-PeoTyfMuFiCtp7X5 .label text,#mermaid-svg-PeoTyfMuFiCtp7X5 span{fill:#333;color:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .node rect,#mermaid-svg-PeoTyfMuFiCtp7X5 .node circle,#mermaid-svg-PeoTyfMuFiCtp7X5 .node ellipse,#mermaid-svg-PeoTyfMuFiCtp7X5 .node polygon,#mermaid-svg-PeoTyfMuFiCtp7X5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .rough-node .label text,#mermaid-svg-PeoTyfMuFiCtp7X5 .node .label text,#mermaid-svg-PeoTyfMuFiCtp7X5 .image-shape .label,#mermaid-svg-PeoTyfMuFiCtp7X5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-PeoTyfMuFiCtp7X5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .rough-node .label,#mermaid-svg-PeoTyfMuFiCtp7X5 .node .label,#mermaid-svg-PeoTyfMuFiCtp7X5 .image-shape .label,#mermaid-svg-PeoTyfMuFiCtp7X5 .icon-shape .label{text-align:center;}#mermaid-svg-PeoTyfMuFiCtp7X5 .node.clickable{cursor:pointer;}#mermaid-svg-PeoTyfMuFiCtp7X5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .arrowheadPath{fill:#333333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PeoTyfMuFiCtp7X5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PeoTyfMuFiCtp7X5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PeoTyfMuFiCtp7X5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PeoTyfMuFiCtp7X5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .cluster text{fill:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 .cluster span{color:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PeoTyfMuFiCtp7X5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PeoTyfMuFiCtp7X5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-PeoTyfMuFiCtp7X5 .icon-shape,#mermaid-svg-PeoTyfMuFiCtp7X5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PeoTyfMuFiCtp7X5 .icon-shape p,#mermaid-svg-PeoTyfMuFiCtp7X5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PeoTyfMuFiCtp7X5 .icon-shape .label rect,#mermaid-svg-PeoTyfMuFiCtp7X5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PeoTyfMuFiCtp7X5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PeoTyfMuFiCtp7X5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PeoTyfMuFiCtp7X5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 微信支付回调
回调接收接口
支付宝异步通知
校验签名
发送支付结果消息
RocketMQ Broker
支付结果消费者
更新订单状态
发送积分/短信等后续消息

核心思路:

  1. 支付平台回调到达后,先校验签名 并快速应答(微信/支付宝要求 5 秒内返回成功,否则会重试)。
  2. 将支付结果封装为消息发送到 RocketMQ,立即返回成功给支付平台。
  3. 消费者异步处理订单状态更新、积分发放、短信通知等业务,天然支持重试 和幂等。

8.2 回调接收接口(Spring Boot)

java 复制代码
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/pay/callback")
public class PayCallbackController {

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 微信支付回调
     */
    @PostMapping("/wechat")
    public String wechatCallback(@RequestBody String xmlBody) {
        try {
            // 1. 校验微信签名(此处省略具体验签逻辑)
            // boolean valid = WechatPayUtil.verifySign(xmlBody);
            // if (!valid) {
            //     return "<xml><return_code>FAIL</return_code><return_msg>签名失败</return_msg></xml>";
            // }

            // 2. 解析回调内容,提取订单号和支付结果
            String orderId = parseOrderIdFromWechat(xmlBody);
            String tradeStatus = parseTradeStatusFromWechat(xmlBody);

            // 3. 发送支付结果消息到 RocketMQ
            Map<String, Object> payload = new HashMap<>();
            payload.put("orderId", orderId);
            payload.put("channel", "WECHAT");
            payload.put("tradeStatus", tradeStatus);
            payload.put("rawData", xmlBody);

            rocketMQTemplate.convertAndSend("PayResultTopic", payload);

            // 4. 立即返回成功,避免支付平台重试
            return "<xml><return_code>SUCCESS</return_code><return_msg>OK</return_msg></xml>";
        } catch (Exception e) {
            // 返回失败,支付平台会稍后重试
            return "<xml><return_code>FAIL</return_code><return_msg>处理异常</return_msg></xml>";
        }
    }

    /**
     * 支付宝异步通知
     */
    @PostMapping("/alipay")
    public String alipayCallback(@RequestBody String formBody) {
        try {
            // 1. 校验支付宝签名(此处省略具体验签逻辑)
            // boolean valid = AlipayUtil.verifySign(formBody);
            // if (!valid) {
            //     return "failure";
            // }

            // 2. 解析回调内容
            String orderId = parseOrderIdFromAlipay(formBody);
            String tradeStatus = parseTradeStatusFromAlipay(formBody);

            // 3. 发送支付结果消息到 RocketMQ
            Map<String, Object> payload = new HashMap<>();
            payload.put("orderId", orderId);
            payload.put("channel", "ALIPAY");
            payload.put("tradeStatus", tradeStatus);
            payload.put("rawData", formBody);

            rocketMQTemplate.convertAndSend("PayResultTopic", payload);

            // 4. 支付宝要求返回 success,否则会重试
            return "success";
        } catch (Exception e) {
            return "failure";
        }
    }

    private String parseOrderIdFromWechat(String xmlBody) {
        // 解析微信回调 XML,提取商户订单号
        return "ORDER_20241022001";
    }

    private String parseTradeStatusFromWechat(String xmlBody) {
        // 解析微信回调 XML,提取交易状态
        return "SUCCESS";
    }

    private String parseOrderIdFromAlipay(String formBody) {
        // 解析支付宝表单,提取商户订单号
        return "ORDER_20241022001";
    }

    private String parseTradeStatusFromAlipay(String formBody) {
        // 解析支付宝表单,提取交易状态
        return "TRADE_SUCCESS";
    }
}

8.3 支付结果消费者(幂等处理)

java 复制代码
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@RocketMQMessageListener(topic = "PayResultTopic", consumerGroup = "pay-result-consumer-group")
public class PayResultConsumer implements RocketMQListener<Map<String, Object>> {

    // 已处理订单缓存,用于幂等判断(生产环境建议用 Redis)
    private final ConcurrentHashMap<String, Boolean> processedOrders = new ConcurrentHashMap<>();

    @Override
    public void onMessage(Map<String, Object> payload) {
        String orderId = (String) payload.get("orderId");
        String channel = (String) payload.get("channel");
        String tradeStatus = (String) payload.get("tradeStatus");

        // 1. 幂等校验:同一订单只处理一次
        if (processedOrders.putIfAbsent(orderId, Boolean.TRUE) != null) {
            System.out.printf("订单 %s 已处理过,跳过(幂等)%n", orderId);
            return;
        }

        try {
            // 2. 只处理支付成功的消息
            if ("SUCCESS".equals(tradeStatus) || "TRADE_SUCCESS".equals(tradeStatus)) {
                // 3. 更新订单状态为已支付
                updateOrderStatus(orderId, "PAID");

                // 4. 发送后续消息:发放积分、发送短信通知等
                sendFollowUpMessages(orderId, channel);
            }

            System.out.printf("支付回调处理完成: 订单=%s, 渠道=%s, 状态=%s%n",
                    orderId, channel, tradeStatus);
        } catch (Exception e) {
            // 处理失败,移除幂等标记,让 RocketMQ 重试
            processedOrders.remove(orderId);
            throw new RuntimeException("支付结果处理失败,等待重试", e);
        }
    }

    private void updateOrderStatus(String orderId, String status) {
        // 更新数据库订单状态
        System.out.printf("更新订单 %s 状态为 %s%n", orderId, status);
    }

    private void sendFollowUpMessages(String orderId, String channel) {
        // 发送积分消息、短信通知消息到其他 Topic
        System.out.printf("发送订单 %s 的后续消息(积分/短信),渠道: %s%n", orderId, channel);
    }
}

8.4 关键设计要点

为什么用 RocketMQ 处理支付回调?

  • 快速应答:支付平台要求 5 秒内返回成功,直接落库可能超时,先发消息再异步处理可保证快速应答。
  • 削峰填谷:大促期间支付回调可能瞬间激增,RocketMQ 能缓冲流量高峰,避免数据库被打垮。
  • 可靠重试:消费者处理失败时,RocketMQ 会自动重试(默认 16 次),保证最终一致性。
  • 幂等保障:支付平台会重复回调,消费者必须做幂等处理,避免重复更新订单。

注意事项:

  • 回调接口必须先验签再处理,防止伪造回调。
  • 消费者处理逻辑必须幂等,可用订单号 + 支付流水号做去重。
  • 支付结果消息建议使用事务消息,保证「本地订单状态更新」与「消息发送」的一致性。
  • 消费者处理失败时抛出异常,让 RocketMQ 按延迟级别重试,不要手动无限重试。
相关推荐
知守观32 分钟前
同一个团队两套分包:DDD 四层落地后,domain 层最大的类还是 5885 行
java·后端·领域驱动设计
文丑颜不良啊1 小时前
LeetCode2541~2560题目解析
java·leetcode
瑞雪兆丰年兮1 小时前
JavaWeb-day11-SpringBootWeb案例(文件上传)
java·运维·服务器·javaweb·oss
Highcharts.js1 小时前
Highcharts智能数据分析平台Orbit 多语言配置技术文档
java·前端·javascript·highcharts·orbit·智能数据分析·设置文档
Rain的Java大神之路2 小时前
设计一套生产级 API 接口:从契约、安全、幂等到限流,我踩过的坑都在这了
java·后端·面试
SimonKing2 小时前
零配置也能白嫖前沿大模型!dsh-our-free-model插件真香警告
java·后端·程序员
nice_lcj5202 小时前
【从零理解 Spring AI · 15】让检索更准:先宽召回,再精排序
java·后端·rag·spring ai·ai应用开发·检索优化·重排序
杨凯凡3 小时前
Spring Boot 链路追踪实战:Micrometer、TraceId 与异步线程上下文传播
java·spring boot·后端
计算机毕设定制辅导-无忧学长3 小时前
《基于Spring Boot的迷游哈游戏综合服务平台的开发》
java·vue.js·spring boot·mysql·迷游哈游戏综合服务平台
非优秀程序员12 小时前
第04章 邮箱大战,163 与 QQ 的暗战|《互联网产品恩仇录》
java·vue.js·架构