RocketMQ 4.x + Spring Boot 生产级集成方案(完整笔记)

下载安装MQ

  1. 下载 RocketMQ 4.9.7
  2. 解压到 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) | ⚠️ 运维负责 |


八、总结

  1. 永远不要假设自动对象序列化可靠 → 统一用 JSON String
  2. 消费者必须与 Producer 类型匹配
  3. 日志 + 异常处理是可观测性的基础
  4. 顺序消息慎用,失败会阻塞队列

💡 最后建议 :上线前使用 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 级
相关推荐
毕设源码_严学姐2 小时前
计算机毕业设计springboot心理健康辅导系统 高校学生心灵关怀服务平台的设计与实现 校园智慧心理服务系统的设计与实现
spring boot·后端·课程设计
姗姗的鱼尾喵3 小时前
Java 面试内容分享
java·spring boot·面试
杰克尼3 小时前
苍穹外卖--day11
java·数据库·spring boot·mybatis·notepad++
摇滚侠3 小时前
Java SpringBoot 项目,项目启动后执行的方法,有哪些方式实现
java·开发语言·spring boot
RDCJM4 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
steel80885 小时前
SSM与Springboot是什么关系? -----区别与联系
java·spring boot·后端
草履虫建模5 小时前
面试常问 SQL 优化八股文总结:慢查询、索引失效、回表、覆盖索引一次搞懂
java·数据库·spring boot·sql·面试·职场和发展·数据库架构
旷世奇才李先生5 小时前
043校园二手交易平台系统-springboot+vue
java·vue.js·spring boot
⑩-6 小时前
SaaS-Admin-项目场景题
java·数据库·spring boot