Spring Boot 接入阿里云 RocketMQ 完整指南
一、核心概念
1.1 实例(Instance)
消息服务的顶层容器,按环境隔离。ID 格式:MQ_INST_<账号ID>_<标识>
阿里云账号
├── 实例-dev (MQ_INST_xxx_BXsxZrYe) → 开发环境
└── 实例-prod (MQ_INST_xxx_BX3z6RD9) → 生产环境
1.2 Topic
消息通道,Producer 发到 Topic,Consumer 从 Topic 消费。
1.3 GroupId
消费者组标识。同组内多实例竞争消费(一条消息只被一个实例消费)。
阿里云硬性要求 :同一 GroupId 下所有消费者必须订阅完全相同 的 Topic 集合,否则报 subscription not consistent。
1.4 Tag
消息二级标签,Consumer 可按 Tag 过滤。
1.5 Endpoint
接入地址。本地开发用公网,服务器部署用内网:
| 类型 | 格式 | 场景 |
|---|---|---|
| 公网 | http://MQ_INST_xxx.mq-internet-access.mq-internet.aliyuncs.com:80 |
本地开发 |
| 内网 | http://MQ_INST_xxx.cn-xxxx.mq-internal.aliyuncs.com:8080 |
同地域ECS |
1.6 AccessKey / SecretKey
身份认证凭证。在阿里云 RAM 中创建,建议用子账号,最小权限原则。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、阿里云控制台操作步骤
2.1 创建实例
- 登录阿里云 → 搜索"消息队列 RocketMQ" → 创建实例
- 选择地域(如华北1青岛)、规格
- 获取实例 ID 和 Endpoint
2.2 创建 Topic
实例详情 → Topic 管理 → 创建 Topic:
- 名称:
my-app-import-check-dev - 消息类型:普通消息
2.3 创建 GroupId
实例详情 → Group 管理 → 创建 Group:
- 名称必须以
GID-或GID_开头 - 如
GID-MY-APP-IMPORT-CHECK
2.4 获取 AccessKey
右上角头像 → AccessKey 管理 → 创建 RAM 子账号 → 授权 AliyunMQFullAccess
三、YAML 配置:数组格式 vs Map 格式
3.1 数组格式
yaml
rocketmq:
consumers:
- topic: my-app-import-check-dev
groupId: GID-MY-APP-CHECK
tag: app-tag
- topic: my-app-import-save-dev
groupId: GID-MY-APP-SAVE
tag: app-tag
代码引用:${rocketmq.consumers[0].tag}、${rocketmq.consumers[1].tag}
特点:
- Java 中映射为
List<ConsumerProperties> - 适合程序动态遍历创建消费者
- 顺序依赖(
[0]/[1]必须和配置顺序一致)
3.2 Map 格式
yaml
rocketmq:
consumers:
import-check:
topic: my-app-import-check-dev
groupId: GID-MY-APP-CHECK
tag: app-tag
import-save:
topic: my-app-import-save-dev
groupId: GID-MY-APP-SAVE
tag: app-tag
代码引用:${rocketmq.consumers.import-check.tag}
特点:
- Java 中映射为
Map<String, ConsumerProperties> - 按名称引用,不依赖顺序
- 语义更清晰
3.3 关键区别
| 维度 | 数组格式 | Map 格式 |
|---|---|---|
| 引用方式 | [0]、[1](位置) |
.name(名称) |
| 可读性 | 低(依赖顺序) | 高(见名知义) |
| 扩展性 | 插入/删除影响索引 | 独立不互相影响 |
| 框架注册 | 遍历 List 逐个创建 | 遍历 Map.entrySet 逐个创建 |
3.4 一个数组元素订阅多 Topic vs 多个独立 GroupId
方式A:一个消费者订阅多 Topic
yaml
consumers:
- topic: topicA|topicB|topicC
groupId: GID-ALL-IN-ONE
- 优点:配置简单,只需一个 GroupId
- 缺点:无法对不同 Topic 设置不同的并发度/重试策略
方式B:多个消费者各自独立
yaml
consumers:
check:
topic: topicA
groupId: GID-CHECK
save:
topic: topicB
groupId: GID-SAVE
export:
topic: topicC
groupId: GID-EXPORT
- 优点:各自独立控制,不会互相影响
- 缺点:需要创建多个 GroupId
四、完整代码示例
4.1 依赖
xml
<dependency>
<groupId>com.aliyun.openservices</groupId>
<artifactId>ons-client</artifactId>
<version>1.8.8.1.Final</version>
</dependency>
4.2 配置属性类
java
package com.example.mq.config;
import java.util.Map;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "rocketmq")
public class RocketMqProperties {
private ProducerConfig producer;
private Map<String, ConsumerConfig> consumers;
@Data
public static class ProducerConfig {
private String accessKey;
private String secretKey;
private String endpoint;
private String tag;
private String instanceId;
}
@Data
public static class ConsumerConfig {
private String accessKey;
private String secretKey;
private String endpoint;
private String tag;
private String topic;
private String groupId;
private String protocol;
private String instanceId;
}
}
4.3 Producer 配置
java
package com.example.mq.config;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.Producer;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ProducerConfiguration {
@Bean(initMethod = "start", destroyMethod = "shutdown")
public Producer producer(RocketMqProperties config) {
Properties properties = new Properties();
properties.put(PropertyKeyConst.AccessKey, config.getProducer().getAccessKey());
properties.put(PropertyKeyConst.SecretKey, config.getProducer().getSecretKey());
properties.put(PropertyKeyConst.NAMESRV_ADDR, config.getProducer().getEndpoint());
if (config.getProducer().getInstanceId() != null) {
properties.put(PropertyKeyConst.INSTANCE_ID, config.getProducer().getInstanceId());
}
return ONSFactory.createProducer(properties);
}
}
4.4 Consumer 自动注册(核心------支持 Map 格式多消费者)
java
package com.example.mq.config;
import com.aliyun.openservices.ons.api.Consumer;
import com.aliyun.openservices.ons.api.MessageListener;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.example.mq.listener.MqListener;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;
@Slf4j
@Configuration
public class ConsumerConfiguration {
@Autowired
private RocketMqProperties config;
@Autowired
private List<MqListener> listeners;
private List<Consumer> consumers = new ArrayList<>();
@PostConstruct
public void init() {
if (config.getConsumers() == null) return;
// 遍历 Map 格式的消费者配置
for (Map.Entry<String, RocketMqProperties.ConsumerConfig> entry
: config.getConsumers().entrySet()) {
String name = entry.getKey();
RocketMqProperties.ConsumerConfig cc = entry.getValue();
Properties properties = new Properties();
properties.put(PropertyKeyConst.AccessKey, cc.getAccessKey());
properties.put(PropertyKeyConst.SecretKey, cc.getSecretKey());
properties.put(PropertyKeyConst.NAMESRV_ADDR, cc.getEndpoint());
properties.put(PropertyKeyConst.GROUP_ID, cc.getGroupId());
if (cc.getInstanceId() != null) {
properties.put(PropertyKeyConst.INSTANCE_ID, cc.getInstanceId());
}
Consumer consumer = ONSFactory.createConsumer(properties);
// 订阅 Topic(支持 | 分隔多个)
String[] topics = cc.getTopic().split("\\|");
for (String topic : topics) {
// 找到负责这个 Topic 的 Listener
MqListener listener = findListener(topic.trim());
if (listener != null) {
consumer.subscribe(topic.trim(), cc.getTag(), (message, context) -> {
try {
listener.consume(message.getBody());
return com.aliyun.openservices.ons.api.Action.CommitMessage;
} catch (Exception e) {
log.error("消费异常, topic={}, msgId={}", topic, message.getMsgID(), e);
return com.aliyun.openservices.ons.api.Action.ReconsumeLater;
}
});
log.info("消费者[{}]订阅 topic={}, tag={}, groupId={}",
name, topic.trim(), cc.getTag(), cc.getGroupId());
}
}
consumer.start();
consumers.add(consumer);
log.info("消费者[{}]启动成功", name);
}
}
@PreDestroy
public void destroy() {
for (Consumer consumer : consumers) {
consumer.shutdown();
}
}
private MqListener findListener(String topic) {
for (MqListener listener : listeners) {
if (topic.equals(listener.topic())) {
return listener;
}
}
return null;
}
}
4.5 Listener 接口
java
package com.example.mq.listener;
/**
* 消息监听器接口.
* 每个业务实现此接口,通过 topic() 方法告诉框架自己负责哪个 Topic.
*/
public interface MqListener {
/** 消费消息. */
void consume(byte[] body);
/** 返回负责的 Topic 名称. */
String topic();
}
4.6 具体 Listener 实现
java
package com.example.mq.listener;
import com.example.entity.TaskMessage;
import com.example.handler.TaskHandler;
import com.example.handler.TaskHandlerRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ImportCheckListener implements MqListener {
@Autowired
private TaskHandlerRegistry registry;
@Value("${xxx.mq.import.check}")
private String topic;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void consume(byte[] body) {
try {
TaskMessage task = objectMapper.readValue(body, TaskMessage.class);
log.info("校验消费开始, bizType={}, id={}", task.getBizType(), task.getId());
TaskHandler handler = registry.getHandler(task.getBizType());
handler.check(task);
} catch (Exception e) {
throw new RuntimeException("校验消费异常", e);
}
}
@Override
public String topic() {
return topic;
}
}
package com.example.mq.listener;
import com.example.entity.TaskMessage;
import com.example.handler.TaskHandlerRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class ImportSaveListener implements MqListener {
@Autowired
private TaskHandlerRegistry registry;
@Value("${xxx.mq.import.save}")
private String topic;
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public void consume(byte[] body) {
try {
TaskMessage task = objectMapper.readValue(body, TaskMessage.class);
log.info("导入消费开始, bizType={}, id={}", task.getBizType(), task.getId());
registry.getHandler(task.getBizType()).save(task);
} catch (Exception e) {
throw new RuntimeException("导入消费异常", e);
}
}
@Override
public String topic() {
return topic;
}
}
4.7 Producer 发送工具
java
package com.example.mq;
import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.Producer;
import com.example.entity.TaskMessage;
import com.example.mq.config.RocketMqProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TaskMqProducer {
@Autowired
private Producer producer;
@Autowired
private RocketMqProperties config;
@Value("${xxx.mq.import.check}")
private String checkTopic;
@Value("${xxx.mq.import.save}")
private String saveTopic;
private final ObjectMapper objectMapper = new ObjectMapper();
public void sendCheck(TaskMessage task) {
send(checkTopic, task);
}
public void sendConfirm(TaskMessage task) {
send(saveTopic, task);
}
private void send(String topic, TaskMessage task) {
try {
byte[] body = objectMapper.writeValueAsBytes(task);
Message message = new Message(topic, config.getProducer().getTag(), body);
message.setKey(String.valueOf(task.getId()));
producer.send(message);
log.info("MQ发送成功, topic={}, id={}", topic, task.getId());
} catch (Exception e) {
throw new RuntimeException("MQ发送失败", e);
}
}
}
4.8 业务处理器(路由目标)
java
package com.example.handler;
import com.example.entity.TaskMessage;
public interface TaskHandler {
String getBizType();
void check(TaskMessage task);
void save(TaskMessage task);
}
package com.example.handler;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class TaskHandlerRegistry {
@Autowired
private List<TaskHandler> allHandlers;
private Map<String, TaskHandler> map = new HashMap<>();
@PostConstruct
public void init() {
for (TaskHandler h : allHandlers) {
map.put(h.getBizType(), h);
}
}
public TaskHandler getHandler(String bizType) {
TaskHandler h = map.get(bizType);
if (h == null) throw new RuntimeException("未找到处理器: " + bizType);
return h;
}
}
package com.example.handler.impl;
import com.example.entity.TaskMessage;
import com.example.handler.TaskHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class OrderHandler implements TaskHandler {
@Override
public String getBizType() { return "ORDER"; }
@Override
public void check(TaskMessage task) {
log.info("订单校验, id={}", task.getId());
}
@Override
public void save(TaskMessage task) {
log.info("订单入库, id={}", task.getId());
}
}
五、常见问题排查
| 错误 | 原因 | 解决 |
|---|---|---|
| No route info of this topic | Topic 未创建/endpoint 不匹配 | 确认阿里云上 Topic 存在且 instanceId 正确 |
| subscription not consistent | 同一 GroupId 的多个实例订阅了不同 Topic | 确保所有实例代码一致,或使用独立 GroupId |
| 消费者零消费 | Consumer 未注册成功 | 检查启动日志中是否有"订阅成功",确认 GroupId 已创建 |
| AUTH_FAILED | AK/SK 错误或无权限 | 确认 RAM 用户有 MQ 权限 |
@Value 注入失败 |
yml 格式和代码路径不匹配 | 数组用 [0],Map 用 .key |