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 事务消息流程:
- 生产者发送半消息(对消费者不可见)。
- 执行本地事务。
- 根据本地事务结果提交或回滚消息。
- 若本地事务长时间未返回结果,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
支付结果消费者
更新订单状态
发送积分/短信等后续消息
核心思路:
- 支付平台回调到达后,先校验签名 并快速应答(微信/支付宝要求 5 秒内返回成功,否则会重试)。
- 将支付结果封装为消息发送到 RocketMQ,立即返回成功给支付平台。
- 消费者异步处理订单状态更新、积分发放、短信通知等业务,天然支持重试 和幂等。
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 按延迟级别重试,不要手动无限重试。