下载安装MQ
- 下载 RocketMQ 4.9.7
- 解压到
E:\soft\rocketmq-4.9.7
✅ RocketMQ 4.9.7 正确启动步骤(仅需两步)
✅ 1. 启动 NameServer
cd E:\soft\rocketmq-all-4.9.7-bin-release\bin
start mqnamesrv.cmd
✅ 2. 启动 Broker
start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true
创建普通 Topic(无需任何属性)
# Windows
mqadmin.cmd updateTopic -n 127.0.0.1:9876 -c DefaultCluster -t delay_queue_topic
mqadmin.cmd updateTopic -n 127.0.0.1:9876 -c DefaultCluster -t normal_hobby_topic
mqadmin.cmd updateTopic -n 127.0.0.1:9876 -c DefaultCluster -t normal_hobby_order_topic
# Linux / macOS
./mqadmin updateTopic -n 127.0.0.1:9876 -c DefaultCluster -t delay_queue_topic
./mqadmin updateTopic -n 127.0.0.1:9876 -c DefaultCluster -t normal_hobby_topic
./mqadmin updateTopic -n 127.0.0.1:9876 -c DefaultCluster -t normal_hobby_order_topic
✅ 就这样!不需要 -a +message.type=xxx 参数。
✅ 验证 Topic 是否创建成功
# 查看所有 Topic
mqadmin.cmd topicList -n 127.0.0.1:9876
# 查看某个 Topic 路由信息
mqadmin.cmd topicRoute -n 127.0.0.1:9876 -t delay_queue_topic
只要能查到,就说明创建成功。
一、Maven 依赖(pom.xml)
<!-- RocketMQ Spring Boot Starter (兼容 RocketMQ 4.x) -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
⚠️ 注意:不要混用 rocketmq-client 原生包,starter 已包含。
二、配置文件(application.yml)
# RocketMQ NameServer 地址(必填)
rocketmq:
name-server: ${ROCKETMQ_NAMESRV:127.0.0.1:9876}
# 自定义 MQ 配置(推荐按业务模块隔离 Topic/Group/Tag)
mq:
delayQueue:
topic: delay_queue_topic
tag: delay_queue_topic
group: delay_queue_group
normalHobbyTopic:
topic: normal_hobby_topic
tag: normal_hobby_topic
group: normal_hobby_group
normalHobbyOrderTopic:
topic: normal_hobby_order_topic
tag: normal_hobby_order_topic
group: normal_hobby_order_group
✅ 最佳实践:
- 使用
${}占位符,支持环境变量覆盖(如 K8s ConfigMap) - Consumer Group 按业务+消费模式命名,便于监控
三、配置类(Java Config)
1. 配置属性绑定(MqProperties.java)
package com.example.study.controller.rocketMQ4.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 绑定 application.yml 中的 mq.* 配置
* 用于代码中动态获取 Topic/Group/Tag(非必须,但提升可维护性)
*/
@Data
@Component
@ConfigurationProperties(prefix = "mq")
public class MqProperties {
private DelayQueue delayQueue = new DelayQueue();
private NormalHobbyTopic normalHobbyTopic = new NormalHobbyTopic();
private NormalHobbyOrderTopic normalHobbyOrderTopic = new NormalHobbyOrderTopic();
@Data
public static class DelayQueue {
private String topic;
private String tag;
private String group;
}
@Data
public static class NormalHobbyTopic {
private String topic;
private String tag;
private String group;
}
@Data
public static class NormalHobbyOrderTopic {
private String topic;
private String tag;
private String group;
}
}
🔍 注:实际消费端通过 ${} 注解引用配置,此 Bean 主要用于 Producer 动态拼接 destination。
2. RocketMQTemplate 自定义配置(可选)
package com.example.study.controller.rocketMQ4.config;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自定义 RocketMQTemplate(通常不需要,starter 已自动配置)
* 仅当需要修改 producer group、重试策略等时使用
*/
@Configuration
public class RocketMQConfig {
@Value("${rocketmq.name-server}")
private String nameServerAddr;
// ⚠️ 一般不建议自定义 RocketMQTemplate!
// starter 默认已创建名为 rocketMQTemplate 的 Bean
// 除非你有特殊需求(如多实例),否则删除此配置类更安全
@Bean
public RocketMQTemplate rocketMQTemplate() {
RocketMQTemplate template = new RocketMQTemplate();
DefaultMQProducer producer = new DefaultMQProducer("CUSTOM_PRODUCER_GROUP");
producer.setNamesrvAddr(nameServerAddr);
producer.setRetryTimesWhenSendFailed(2); // 发送失败重试 2 次
template.setProducer(producer);
return template;
}
}
✅ 建议 :删除此配置类 ,直接使用 starter 自动注入的 RocketMQTemplate,避免重复初始化。
四、消息实体(Hobby.java)
package com.example.study.controller.rocketMQ4.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 消息载体 POJO
* 要求:
* - 无参构造函数(Jackson 反序列化需要)
* - Getter/Setter(Lombok @Data 提供)
*/
@Data
public class Hobby {
@Schema(description = "爱好名称")
private String name;
@Schema(description = "爱好详情")
private String describe;
public Hobby() {}
public Hobby(String name, String describe) {
this.name = name;
this.describe = describe;
}
}
五、生产者(Producer Controller)
package com.example.study.controller.rocketMQ4;
import com.example.study.controller.rocketMQ4.entity.Hobby;
import io.swagger.v3.oas.annotations.Operation;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/rocketmq")
public class RocketMQProducerController {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private ObjectMapper objectMapper;
// 普通消息配置
@Value("${mq.normalHobbyTopic.topic}")
private String normalHobbyTopic;
@Value("${mq.normalHobbyTopic.tag}")
private String normalHobbyTag;
// 顺序消息配置
@Value("${mq.normalHobbyOrderTopic.topic}")
private String normalHobbyOrderTopic;
@Value("${mq.normalHobbyOrderTopic.tag}")
private String normalHobbyOrderTag;
// 延迟消息配置
@Value("${mq.delayQueue.topic}")
private String delayQueueTopic;
@Value("${mq.delayQueue.tag}")
private String delayQueueTag;
// ==================== 单条发送 ====================
@Operation(summary = "发送单条普通消息(无序)")
@GetMapping("/sendNormal")
public String sendNormal() {
try {
Hobby hobby = new Hobby("读书", "每天读 1 小时");
String jsonPayload = objectMapper.writeValueAsString(hobby);
String destination = normalHobbyTopic + ":" + normalHobbyTag;
rocketMQTemplate.convertAndSend(destination, jsonPayload);
log.info("✅ 普通消息发送成功 | destination={}, payload={}", destination, jsonPayload);
return "普通消息已发送";
} catch (Exception e) {
log.error("❌ 普通消息发送失败", e);
throw new RuntimeException("发送失败", e);
}
}
@Operation(summary = "发送单条顺序消息(按 shardingKey 保证顺序)")
@GetMapping("/sendOrderly")
public String sendOrderly() {
try {
Hobby hobby = new Hobby("健身", "每周 3 次");
String jsonPayload = objectMapper.writeValueAsString(hobby);
String destination = normalHobbyOrderTopic + ":" + normalHobbyOrderTag;
//🎯 记住:shardingKey = 业务顺序单元的唯一ID
//所有使用相同 shardingKey 的消息,会被发送到同一个 MessageQueue,并被同一个消费者线程串行消费 ------ 从而保证严格 FIFO 顺序。
String shardingKey = "user_123";
SendResult result = rocketMQTemplate.syncSendOrderly(destination, jsonPayload, shardingKey);
log.info("✅ 顺序消息发送成功 | key={}, msgId={}", shardingKey, result.getMsgId());
return "顺序消息已发送(key=" + shardingKey + ")| msgId=" + result.getMsgId();
} catch (Exception e) {
log.error("❌ 顺序消息发送失败", e);
throw new RuntimeException("发送失败", e);
}
}
@Operation(summary = "发送单条延迟消息(delayLevel=3,延迟 10 秒)")
@GetMapping("/sendDelay")
public String sendDelay() {
try {
Hobby hobby = new Hobby("睡觉", "晚上 11 点");
String jsonPayload = objectMapper.writeValueAsString(hobby);
String destination = delayQueueTopic + ":" + delayQueueTag;
org.springframework.messaging.Message<String> message =
MessageBuilder.withPayload(jsonPayload).build();
SendResult result = rocketMQTemplate.syncSend(destination, message, 3000, 3);
log.info("✅ 延迟消息发送成功 | delayLevel=3 (10s), msgId={}", result.getMsgId());
return "延迟消息已发送,10秒后消费 | msgId=" + result.getMsgId();
} catch (Exception e) {
log.error("❌ 延迟消息发送失败", e);
throw new RuntimeException("发送失败", e);
}
}
// ==================== 批量发送 ====================
@Operation(summary = "批量发送普通消息(无序,真实批量)")
@GetMapping("/sendBatchNormal")
public String sendBatchNormal() {
try {
DefaultMQProducer producer = rocketMQTemplate.getProducer();
List<Message> messages = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Hobby hobby = new Hobby("批量读书-" + i, "第 " + i + " 次");
byte[] body = objectMapper.writeValueAsBytes(hobby);
Message msg = new Message(normalHobbyTopic, normalHobbyTag, body);
messages.add(msg);
}
SendResult result = producer.send(messages);
log.info("✅ 批量普通消息发送成功 | count=5, msgId={}", result.getMsgId());
return "批量普通消息已发送(5条)| msgId=" + result.getMsgId();
} catch (Exception e) {
log.error("❌ 批量普通消息发送失败", e);
throw new RuntimeException("发送失败", e);
}
}
@Operation(summary = "批量发送顺序消息(模拟:循环发送单条顺序消息)")
@GetMapping("/sendBatchOrderly")
public String sendBatchOrderly() {
StringBuilder result = new StringBuilder("顺序批量消息发送结果:\n");
for (int i = 0; i < 3; i++) {
try {
Hobby hobby = new Hobby("批量健身-" + i, "第 " + i + " 次");
String jsonPayload = objectMapper.writeValueAsString(hobby);
String destination = normalHobbyOrderTopic + ":" + normalHobbyOrderTag;
//🎯 记住:shardingKey = 业务顺序单元的唯一ID
//所有使用相同 shardingKey 的消息,会被发送到同一个 MessageQueue,并被同一个消费者线程串行消费 ------ 从而保证严格 FIFO 顺序。
SendResult sendResult = rocketMQTemplate.syncSendOrderly(destination, jsonPayload, "user_123");
result.append("msgId=").append(sendResult.getMsgId()).append("\n");
log.info("✅ 顺序批量子消息发送成功 | index={}, msgId={}", i, sendResult.getMsgId());
} catch (Exception e) {
String errorMsg = "Error index=" + i + ": " + e.getMessage();
result.append(errorMsg).append("\n");
log.error("❌ 顺序批量子消息发送失败 | index={}", i, e);
}
}
return result.toString();
}
@Operation(summary = "模拟批量发送延迟消息(RocketMQ 不支持延迟批量)")
@GetMapping("/sendBatchDelay")
public String sendBatchDelay() {
StringBuilder result = new StringBuilder("延迟批量消息发送结果:\n");
for (int i = 0; i < 3; i++) {
try {
Hobby hobby = new Hobby("批量睡觉-" + i, "第 " + i + " 次");
String jsonPayload = objectMapper.writeValueAsString(hobby);
String destination = delayQueueTopic + ":" + delayQueueTag;
org.springframework.messaging.Message<String> message =
MessageBuilder.withPayload(jsonPayload).build();
SendResult sendResult = rocketMQTemplate.syncSend(destination, message, 3000, 3);
result.append("msgId=").append(sendResult.getMsgId()).append("\n");
log.info("✅ 延迟批量子消息发送成功 | index={}, msgId={}", i, sendResult.getMsgId());
} catch (Exception e) {
String errorMsg = "Error index=" + i + ": " + e.getMessage();
result.append(errorMsg).append("\n");
log.error("❌ 延迟批量子消息发送失败 | index={}", i, e);
}
}
return result.toString();
}
}
✅ 关键改进:
- 添加 详细日志(INFO 成功,ERROR 失败)
- try-catch 包裹,防止 HTTP 500 且丢失错误信息
- Tag 与 Topic 分离 (
topic:tag更清晰)
六、消费者(Consumer)
所有消费者必须实现 RocketMQListener<String>,因为 Producer 发送的是 JSON 字符串!
1. 普通消息消费者(并发)
package com.example.study.controller.rocketMQ4.consumer;
import com.example.study.controller.rocketMQ4.entity.Hobby;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@Slf4j
@Service
@RocketMQMessageListener(
topic = "${mq.normalHobbyTopic.topic}",
consumerGroup = "${mq.normalHobbyTopic.group}",
selectorExpression = "${mq.normalHobbyTopic.tag}"
)
public class NormalHobbyConsumer implements RocketMQListener<MessageExt> { // ← 泛型改为 MessageExt
@Autowired
private ObjectMapper objectMapper;
@Override
public void onMessage(MessageExt message) {
String msgId = message.getMsgId();
String body = new String(message.getBody(), StandardCharsets.UTF_8);
try {
Hobby hobby = objectMapper.readValue(body, Hobby.class);
log.info("✅ [普通消息] 消费成功 | msgId={}, name='{}', describe='{}'",
msgId, hobby.getName(), hobby.getDescribe());
// TODO 业务逻辑(注意幂等性!)
} catch (Exception e) {
log.error("❌ [普通消息] 消费失败 | msgId={}, body={}", msgId, body, e);
// 抛出异常 → 触发 RocketMQ 重试(默认最多 16 次)
throw new RuntimeException("消费失败", e);
}
}
}
2. 顺序消息消费者(串行)
package com.example.study.controller.rocketMQ4.consumer;
import com.example.study.controller.rocketMQ4.entity.Hobby;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@Slf4j
@Service
@RocketMQMessageListener(
topic = "${mq.delayQueue.topic}",
consumerGroup = "${mq.delayQueue.group}",
selectorExpression = "${mq.delayQueue.tag}"
)
public class DelayHobbyConsumer implements RocketMQListener<MessageExt> { // ← 泛型改为 MessageExt
@Autowired
private ObjectMapper objectMapper;
@Override
public void onMessage(MessageExt message) {
try {
// 获取 msgId(关键!)
String msgId = message.getMsgId();
// 获取消息体
String body = new String(message.getBody(), StandardCharsets.UTF_8);
Hobby hobby = objectMapper.readValue(body, Hobby.class);
log.info("⏰ [延迟消息] 消费成功 | msgId={}, name='{}', describe='{}'",
msgId, hobby.getName(), hobby.getDescribe());
// TODO 延迟后业务(如订单超时取消)
} catch (Exception e) {
String msgId = message != null ? message.getMsgId() : "unknown";
log.error("❌ [延迟消息] 消费失败 | msgId={}, message={}", msgId,
message != null ? new String(message.getBody(), StandardCharsets.UTF_8) : "", e);
throw new RuntimeException("延迟消费失败", e);
}
}
}
3. 延迟消息消费者
package com.example.study.controller.rocketMQ4.consumer;
import com.example.study.controller.rocketMQ4.entity.Hobby;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@Slf4j
@Service
@RocketMQMessageListener(
topic = "${mq.delayQueue.topic}",
consumerGroup = "${mq.delayQueue.group}",
selectorExpression = "${mq.delayQueue.tag}"
)
public class DelayHobbyConsumer implements RocketMQListener<MessageExt> { // ← 泛型改为 MessageExt
@Autowired
private ObjectMapper objectMapper;
@Override
public void onMessage(MessageExt message) {
try {
// 获取 msgId(关键!)
String msgId = message.getMsgId();
// 获取消息体
String body = new String(message.getBody(), StandardCharsets.UTF_8);
Hobby hobby = objectMapper.readValue(body, Hobby.class);
log.info("⏰ [延迟消息] 消费成功 | msgId={}, name='{}', describe='{}'",
msgId, hobby.getName(), hobby.getDescribe());
// TODO 延迟后业务(如订单超时取消)
} catch (Exception e) {
String msgId = message != null ? message.getMsgId() : "unknown";
log.error("❌ [延迟消息] 消费失败 | msgId={}, message={}", msgId,
message != null ? new String(message.getBody(), StandardCharsets.UTF_8) : "", e);
throw new RuntimeException("延迟消费失败", e);
}
}
}
✅ 消费者统一要点:
- 接收
String - 手动
objectMapper.readValue() - 异常抛出以触发重试
- 日志包含
[类型]前缀,便于 grep
七、生产环境 Checklist ✅
|--------------------------------------|----------|
| 项目 | 是否完成 |
| ✅ 所有消费者接收 String 并手动反序列化 | ✔️ |
| ✅ Producer 发送 JSON 字符串 | ✔️ |
| ✅ 配置通过 ${} 引用,支持环境变量 | ✔️ |
| ✅ 日志包含上下文(msgId/payload/topic) | ✔️ |
| ✅ 异常被捕获并记录,同时抛出以触发重试 | ✔️ |
| ✅ 顺序消费者设置 maxReconsumeTimes 避免永久阻塞 | ✔️ |
| ✅ 业务逻辑具备幂等性(需自行实现) | ⚠️ 开发者负责 |
| ✅ 监控消费延迟、失败率(需接入 Prometheus) | ⚠️ 运维负责 |
八、总结
- 永远不要假设自动对象序列化可靠 → 统一用 JSON String
- 消费者必须与 Producer 类型匹配
- 日志 + 异常处理是可观测性的基础
- 顺序消息慎用,失败会阻塞队列
💡 最后建议 :上线前使用 RocketMQ Dashboard 或命令行工具验证消息是否正常收发。
📌 附:RocketMQ 延迟级别参考(Broker 配置)
messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
delayLevel=3→ 10 秒- 最大支持 18 级