前言 用来学习MQ使用的 demo场景
本文环境:CentOS7/8、JDK8、RocketMQ 5.1.4(ACL1.0稳定版,支持任意时间戳定时消息)
包含:服务器离线安装、JVM内存调优、开启ACL账号密码、启动/停止/重启命令、防火墙放行端口、SpringBoot整合带密码连接、生产者发送定时消息、消费者消费并调用业务接口,适配课程定时推送场景。
一、服务器前置准备
1. 环境要求
- JDK 1.8+ 已配置环境变量
- 服务器开放端口:
9876(NameServer)、10911(Broker)
bash
# 防火墙放行端口
firewall-cmd --add-port=9876/tcp --permanent
firewall-cmd --add-port=10911/tcp --permanent
firewall-cmd --reload
# 查看端口是否放行
firewall-cmd --list-ports
2. 创建安装目录
bash
mkdir -p /usr/local/rocketmq
cd /usr/local/rocketmq
二、下载&解压RocketMQ
1. 下载二进制包
官网下载地址:https://rocketmq.apache.org/dowloading/releases
选择 rocketmq-all-5.1.4-bin-release.zip
上传到服务器 /usr/local/rocketmq 目录
2. 解压
bash
unzip rocketmq-all-5.1.4-bin-release.zip
mv rocketmq-all-5.1.4-bin-release rocketmq5.1.4
cd rocketmq5.1.4
# 定义环境变量(临时生效,永久写入/etc/profile)
export ROCKETMQ_HOME=/usr/local/rocketmq/rocketmq5.1.4
三、修改JVM内存(低配服务器必改,否则启动OOM)
1. 修改NameServer内存
bash
vim bin/runserver.sh
# 找到JAVA_OPT,修改Xms Xmx
JAVA_OPT="${JAVA_OPT} -Xms256m -Xmx256m -Xmn128m"
2. 修改Broker内存
bash
vim bin/runbroker.sh
JAVA_OPT="${JAVA_OPT} -Xms256m -Xmx256m -Xmn128m"
四、开启ACL密码鉴权(设置账号密码)
步骤1:开启Broker ACL开关
bash
vim conf/broker.conf
# 文件末尾追加
aclEnable=true
# 允许自动创建Topic(测试环境)
autoCreateTopicEnable=true
步骤2:配置账号、密码、权限 plain_acl.yml
bash
vim conf/plain_acl.yml
完整配置(业务账号仅允许发送/消费课程推送Topic):
yaml
accounts:
# 管理员账号,运维使用
- accessKey: mq_admin
secretKey: Admin@2026#Rmq
whiteRemoteAddress: 127.0.0.1,服务器内网IP
admin: true
# 业务应用账号(Java项目连接使用,用户名密码)
- accessKey: course_push_app
secretKey: Push@666888
whiteRemoteAddress: 0.0.0.0 # 所有IP可访问,生产填业务服务IP
admin: false
defaultTopicPerm: DENY
defaultGroupPerm: DENY
# 仅允许操作课程推送Topic:发布PUB 订阅SUB
topicPerms:
- "course_push_topic=PUB|SUB"
groupPerms:
- "course-push-consumer-group=SUB"
globalWhiteRemoteAddresses:
- 127.0.0.1
- accessKey = 用户名
- secretKey = 密码
修改完成保存,重启Broker生效
五、RocketMQ 启动/停止/重启完整命令
进入mq根目录执行:cd /usr/local/rocketmq/rocketmq5.1.4
1. 启动顺序:先NameServer,后Broker
启动NameServer
bash
# 后台启动,输出日志到nohup.out
nohup sh bin/mqnamesrv &
# 查看启动日志,出现 boot success 代表成功
tail -f ~/logs/rocketmqlogs/namesrv.log
启动Broker(绑定本机IP)
bash
# 替换为你的服务器公网/内网IP
nohup sh bin/mqbroker -n 127.0.0.1:9876 &
# 查看Broker日志
tail -f ~/logs/rocketmqlogs/broker.log
2. 停止服务(顺序先Broker,后NameServer)
bash
# 停止Broker
sh bin/mqshutdown broker
# 停止NameServer
sh bin/mqshutdown namesrv
3. 重启流程
bash
# 1.停止
sh bin/mqshutdown broker
sh bin/mqshutdown namesrv
# 2.等待5秒
sleep 5
# 3.重新启动
nohup sh bin/mqnamesrv &
sleep 3
nohup sh bin/mqbroker -n 127.0.0.1:9876 &
4. 验证服务连通(内置工具测试)
bash
export NAMESRV_ADDR=127.0.0.1:9876
# 发送测试消息(未开ACL可用,开启ACL会报错,需代码带AK/SK)
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer
sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer
六、SpringBoot Java Demo(带ACL密码,定时消息生产+消费)
1. Maven依赖 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.demo</groupId>
<artifactId>rocketmq-acl-demo</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- SpringBoot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- RocketMQ Starter 带ACL支持 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
<!-- Redis 幂等防重复推送 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JSON序列化 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
</dependencies>
</project>
2. application.yml 配置(核心:配置mq地址+ACL账号密码)
yaml
spring:
application:
name: rocketmq-course-push
redis:
host: 127.0.0.1
port: 6379
# mq 配置
rocketmq:
# 服务器 NameServer 地址,替换为你的服务器IP
name-server: 127.0.0.1:9876
producer:
group: course-push-producer-group
# ACL账号密码(plain_acl.yml配置的业务账号)
access-key: course_push_app
secret-key: Push@666888
send-message-timeout: 5000
consumer:
access-key: course_push_app
secret-key: Push@666888
3. 消息实体类 CoursePushMsg.java
java
package com.demo.entity;
import lombok.Data;
@Data
public class CoursePushMsg {
private Long courseId;
private String title;
private String content;
private Long endTimeStamp; // 课程结束时间戳(毫秒)
}
4. 生产者服务:发送定时消息 CoursePushProducer.java
java
package com.demo.service;
import com.alibaba.fastjson2.JSON;
import com.demo.entity.CoursePushMsg;
import org.apache.rocketmq.client.producer.SendMessageWithTimeStampRequest;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
@Service
public class CoursePushProducer {
@Autowired
private RocketMQTemplate rocketMQTemplate;
private static final String TOPIC = "course_push_topic";
/**
* 发送课程结束定时推送消息(指定精确到期时间戳)
* @param msg 课程消息实体
*/
public void sendCourseTimedMsg(CoursePushMsg msg) {
byte[] body = JSON.toJSONString(msg).getBytes(StandardCharsets.UTF_8);
SendMessageWithTimeStampRequest request = SendMessageWithTimeStampRequest.builder()
.topic(TOPIC)
.body(body)
.deliveryTimeStamp(msg.getEndTimeStamp())
.build();
try {
rocketMQTemplate.syncSendWithTimestamp(request);
System.out.println("定时消息发送成功,课程ID:" + msg.getCourseId());
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("发送MQ定时消息失败");
}
}
}
5. 模拟推送接口客户端 PushApiClient.java
java
package com.demo.client;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
@Component
public class PushApiClient {
@Resource
private RestTemplate restTemplate;
/**
* 业务推送接口:根据课程ID批量推送学员消息
*/
public void pushNotice(Long courseId) {
// 替换为你自己的推送服务接口地址
String url = "http://127.0.0.1:8080/api/push/send?courseId=" + courseId;
String resp = restTemplate.postForObject(url, null, String.class);
System.out.println("调用推送接口完成,返回:" + resp);
}
}
6. 消费者:监听消息、校验课程、调用推送接口 CoursePushConsumer.java
java
package com.demo.consumer;
import com.alibaba.fastjson2.JSON;
import com.demo.client.PushApiClient;
import com.demo.entity.CoursePushMsg;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
@RocketMQMessageListener(
topic = "course_push_topic",
consumerGroup = "course-push-consumer-group"
)
public class CoursePushConsumer implements MessageListenerConcurrently {
@Autowired
private PushApiClient pushApiClient;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext context) {
for (MessageExt msg : msgs) {
try {
String bodyStr = new String(msg.getBody());
CoursePushMsg msgData = JSON.parseObject(bodyStr, CoursePushMsg.class);
Long courseId = msgData.getCourseId();
long msgScheduleTs = msg.getDeliveryTimestamp();
long realEndTs = msgData.getEndTimeStamp();
// 1. 过滤旧消息(修改课程结束时间产生的过期定时消息)
if (Math.abs(msgScheduleTs - realEndTs) > 60 * 1000) {
System.out.println("旧定时消息,丢弃 courseId:" + courseId);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
// 2. Redis幂等,防止重复推送
String redisKey = "push:course:" + courseId;
if (Boolean.TRUE.equals(redisTemplate.hasKey(redisKey))) {
System.out.println("该课程已推送,跳过");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
// 3. 核心逻辑:调用业务推送接口
pushApiClient.pushNotice(courseId);
// 4. 成功后写入幂等标记,7天过期
redisTemplate.opsForValue().set(redisKey, "1", 7, TimeUnit.DAYS);
} catch (Exception e) {
e.printStackTrace();
// 消费异常,MQ自动重试
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
7. 测试接口 Controller TestMqController.java
java
package com.demo.controller;
import com.demo.entity.CoursePushMsg;
import com.demo.service.CoursePushProducer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
@RestController
public class TestMqController {
@Autowired
private CoursePushProducer producer;
/**
* 测试发送定时推送消息
* 访问示例:http://127.0.0.1:8080/send/push?courseId=1001&endTime=2026-06-25 18:30:00
*/
@GetMapping("/send/push")
public String sendPushMsg(@RequestParam Long courseId, @RequestParam String endTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
LocalDateTime localDateTime = LocalDateTime.parse(endTime, formatter);
long timeStamp = localDateTime.toInstant(ZoneOffset.of("+8")).toEpochMilli();
CoursePushMsg msg = new CoursePushMsg();
msg.setCourseId(courseId);
msg.setTitle("课程已结束提醒");
msg.setContent("本节课学习完成,请完成课后作业");
msg.setEndTimeStamp(timeStamp);
producer.sendCourseTimedMsg(msg);
return "定时消息提交成功,到期自动推送学员";
}
}
8. 启动类 RocketMqApplication.java
java
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class RocketMqApplication {
public static void main(String[] args) {
SpringApplication.run(RocketMqApplication.class, args);
}
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
七、业务场景说明(课程修改结束时间解决方案)
- 新增课程:保存课程endTime到数据库,调用接口发送定时消息
- 修改课程结束时间:更新数据库endTime,重新调用发送接口生成新定时消息;旧消息到期消费时,时间不匹配直接丢弃,不会重复推送
- 取消课程推送:数据库增加pushStatus=2标记,消费时查询状态直接跳过推送逻辑
八、常见踩坑问题
- 连接报错ACL权限不足
- 检查yml的access-key、secret-key和plain_acl.yml完全一致
- 确认topicPerms配置了PUB|SUB权限
- 定时消息不生效
- RocketMQ版本必须5.x以上才支持
syncSendWithTimestamp任意时间戳定时
- RocketMQ版本必须5.x以上才支持
- 服务器外网无法连接
- 防火墙放行9876、10911端口
- broker启动时绑定公网IP:
nohup sh bin/mqbroker -n 服务器公网IP:9876 &
- 重复推送
- Redis幂等标记 + 数据库推送状态双重兜底
- 启动内存不足
- 修改runserver.sh、runbroker.sh的Xms/Xmx降低内存
九、总结
- 生产环境必须开启ACL设置账号密码,禁止裸奔无鉴权
- RocketMQ定时消息无法删除,采用「数据库存真实时间,消费过滤旧消息」方案适配课程修改时间场景
- SpringBoot starter自动携带ACL鉴权信息,无需手动编写RPC钩子,开发极简
- 启停顺序:先NameServer后Broker;停止顺序相反,重启前必须完整关闭进程