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 按延迟级别重试,不要手动无限重试。
相关推荐
aibo8603081 小时前
基于 Spring Boot + Vue3 的菜谱分享网站设计与实现
java·spring boot·后端
凤山老林1 小时前
Spring Boot 集成 TOTP 双因素认证与恢复码机制
java·spring boot·后端
小冯不疯1 小时前
物料主数据同步实战:从金蝶云星空到小满OKKICRM的单一策略深度拆解
java·大数据·数据库
vipxieliang1 小时前
Spring Boot 测试策略:单元测试、切片测试与集成测试
java·spring boot·后端
估值探索者1 小时前
【Python量化技术指标实战 04】布林带通道突破,怎么知道是真突破还是假突破?
java·开发语言·python·接口·股票数据api接口
未秃头的程序猿1 小时前
同一个商户号,V2 免密代扣和 V3 分账的证书怎么共存?
java·后端·微信
captain3761 小时前
Spring Boot的配置文件
java·spring boot·后端
captain3761 小时前
Spring Boot日志
java·数据库·spring boot
Rabitebla2 小时前
【Linux】 权限篇(三):深入理解 umask 位运算与粘滞位的设计思想
java·linux·服务器·c++·笔记·学习