安装RocketMQ,并集成到Springboot
RocketMQ 是什么
RocketMQ是一个分布式的消息中间件,具有低延迟、高性能、高可靠性,兆级能力和灵活的可扩展性。
安装RocketMQ
这里选择安装当前最新的发布版本:4.9.0
安装到CentOS 7
我使用的Cent OS 版本为 7.9.2009,64位。
(一) 依赖的运行环境
- 64位Linux操作系统
- 64位 JDK 1.8+
- 为Broker服务准备4G 以上的磁盘空间
(二) 下载安装包并解压
官网下载地址:https://rocketmq.apache.org/release_notes/release-notes-4.9.0/
选择Binary,进入下载页面,下载zip压缩包,并复制到CentOS的/home 目录下
执行以下命令:
cd /home
unzip ./rocketmq-all-4.9.0-bin-release.zip -d /usr/local
cd /usr/local
mv ./rocketmq-all-4.9.0-bin-release/ ./rocketmq
cd ./rocketmq
ls
可以看到解压后的rocketmq目录
benchmark bin conf lib LICENSE NOTICE README.md
(三) 修改启动脚本并启动
由于启动脚本中的JVM参数设置的内存太大,超出了我CentOS虚拟机分配的内存,所以启动之前先修改脚本中的JVM参数。
-
启动NameServer
进入bin目录,编辑 runserver.shcd ./bin/ vim runserver.sh
找到 JAVA_OPT= 开头的这一行,修改其中Xms、Xmx、Xmn,然后保存并退出vim编辑器
JAVA_OPT="${JAVA_OPT} -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
启动NameServer
nohup ./bin/mqnamesrv &
查看网络端口,看到有9876端口就说明启动成功了
netstat -ntlp Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp6 0 0 :::9876 :::* LISTEN 5554/java
-
启动broker
编辑 runbroker.shvim runbroker.sh
找到 JAVA_OPT="${JAVA_OPT} -server 开头的这一行,修改其中Xms、Xmx、Xmn,然后保存并退出vim编辑器
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g"
启动Broker
nohup ./mqbroker -n localhost:9876 &
查看网络端口,看到有10911、10912这两个端口就说明启动成功了
netstat -ntlp Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp6 0 0 :::9876 :::* LISTEN 5554/java tcp6 0 0 :::10909 :::* LISTEN 5602/java tcp6 0 0 :::10911 :::* LISTEN 5602/java tcp6 0 0 :::10912 :::* LISTEN 5602/java
集成RocketMQ到Spring Boot
我们现有的项目是基于Spring Boot搭建的,要在现有项目中使用RocketMQ,并让Spring托管生产者、消费者。目前了解到两种比较合适的方案。
方法一:手动实现
这里演示一下如何在现有的Spring Boot项目中,通过@Bean注解、@PostConstruct注解将RocketMQ的生产者、消费者托管到Spring Boot并使用它们。
(一) 引入依赖,添加RocketMQ相关配置
POM文件引入依赖rocketmq-client
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.9.0</version>
</dependency>
application.yml配置文件添加rocketmq相关配置项
rocketmq:
groupName: enterprise_cloud_log # 发送同一类消息的设置为同一个group,保证唯一。默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示
namesrvAddr: 192.168.255.128:9876 # mq的nameserver地址
producer:
maxMessageSize: 4096 # 消息最大长度 默认1024*4(4M)
sendMsgTimeout: 3000 # 发送消息超时时间,默认3000
retryTimesWhenSendFailed: 2 # 发送消息失败重试次数,默认2
consumer:
consumeThreadMin: 5 # 消费者线程最小数量
consumeThreadMax: 32 # 消费者线程最大数量
consumeMessageBatchMaxSize: 1 # 设置一次消费消息的条数,默认为1条
定义一个RocketMqConfigProperties类,映射yml文件中的配置项
@Data
@Component
@ConfigurationProperties(prefix = "rocketmq")
public class RocketMqConfigProperties {
private String groupName = "default";
private boolean isEnable = false;
private String namesrvAddr = "localhost:9876";
private int producerMaxMessageSize = 1024;
private int producerSendMsgTimeout = 2000;
private int producerRetryTimesWhenSendFailed = 2;
private int consumerConsumeThreadMin = 5;
private int consumerConsumeThreadMax = 30;
private int consumerConsumeMessageBatchMaxSize = 1;
}
定义一个RocketMqConfig类,用于根据配置项生成生产者和消费者
@Slf4j
@Configuration
@EnableAutoConfiguration
public class RocketMqConfig {
@Resource
ApplicationContext applicationContext;
@Resource
RocketMqConfigProperties rocketMqConfigProperties;
// 生成生产者和消费者
// ……
}
(二) 集成RocketMQ生产者
在RocketMqConfig中添加一个方法,用于注入一个默认的生产者
/**
* 注入一个默认的生产者
*
* @return
* @throws MQClientException
*/
@Bean
public DefaultMQProducer defaultMQProducer() throws MQClientException {
if (StringUtils.isEmpty(rocketMqConfigProperties.getGroupName())) {
throw new MQClientException(-1, "groupName is blank");
}
if (StringUtils.isEmpty(rocketMqConfigProperties.getNamesrvAddr())) {
throw new MQClientException(-1, "nameServerAddr is blank");
}
DefaultMQProducer producer;
producer = new DefaultMQProducer(rocketMqConfigProperties.getGroupName());
producer.setNamesrvAddr(rocketMqConfigProperties.getNamesrvAddr());
// 自动创建mq集群中不存在的topic
producer.setCreateTopicKey(TopicValidator.AUTO_CREATE_TOPIC_KEY_TOPIC);
// 如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName
// producer.setInstanceName(instanceName);
producer.setMaxMessageSize(rocketMqConfigProperties.getProducerMaxMessageSize());
producer.setSendMsgTimeout(rocketMqConfigProperties.getProducerSendMsgTimeout());
// 如果发送消息失败,设置重试次数,默认为2次
producer.setRetryTimesWhenSendFailed(rocketMqConfigProperties.getProducerRetryTimesWhenSendFailed());
try {
producer.start();
log.info("producer is start! groupName:{}, namesrvAddr:{}", rocketMqConfigProperties.getGroupName(),
rocketMqConfigProperties.getNamesrvAddr());
} catch (MQClientException e) {
log.error(String.format("producer is error {}", e.getMessage(), e));
throw e;
}
return producer;
}
添加一个Controller,用于调用生产者,并添加一条消息到RocketMQ
@RestController
@RequestMapping("/log")
@Slf4j
public class LogController {
@Resource
DefaultMQProducer defaultMQProducer;
@RequestMapping("add")
public String add(@RequestParam("log") String value){
Message msg = null;
try {
// 创建一条消息
msg = new Message("TopicTest", "tags1", value.getBytes(RemotingHelper.DEFAULT_CHARSET));
// 发送消息到一个Broker
SendResult sendResult = defaultMQProducer.send(msg);
// 通过sendResult返回消息是否成功送达
log.warn(sendResult.toString());
return "success";
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return "fail";
}
}
(三) 集成RocketMQ消费者
添加AbstractConsumerConfig类,这是一个抽象的消费者配置类
@Data
public abstract class AbstractConsumerConfig {
// 订阅的主题
private String topic;
// 订阅哪些tag
private String tags;
// listener
private MessageListener messageListener;
// 消费者
private MQPushConsumer mqPushConsumer;
// 自定义一个消费者标题,方便调试时候查看
private String consumerTitle;
/**
* 设置topic、tags、title这三个基本信息
* @param topic 订阅的主题
* @param tags 订阅的标签.只支持“逻辑或”运算符的表达式 例如 "tag1 || tag2 || tag3"
* 如果未 null 或 表达式“*”,表示订阅所有的tag
* @param title initConfig
*/
protected void initConfig(String topic, String tags, String title) {
this.topic = topic;
this.tags = tags;
this.consumerTitle = title;
}
/**
* 设置消息监听器
* @param messageListener 消息监听器
*/
public void registerMessageListener(MessageListener messageListener) {
this.messageListener = messageListener;
}
/**
* 初始化消费者
*/
public abstract void init();
}
添加LogConsumerConfig,实现init()方法
@Component
@Slf4j
public class LogConsumerConfig extends AbstractConsumerConfig {
private static final String DEFAULT_TOPIC = "TopicTest", DEFAULT_TAGS = "*", DEFAULT_TITLE = "logConsumer";
@Override
public void initConfig(String topic, String tags, String title) {
super.initConfig(topic, tags, title);
}
@Override
public void init() {
// 设置主题,标签与消费者标题
if(StringUtils.isEmpty(this.getTopic())) {
this.setTopic(DEFAULT_TOPIC);
}
if(StringUtils.isEmpty(this.getTags())) {
this.setTags(DEFAULT_TAGS);
}
if(StringUtils.isEmpty(this.getConsumerTitle())) {
this.setConsumerTitle(DEFAULT_TITLE);
}
//消费者具体执行逻辑
registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
msgs.forEach(msg -> {
// 消费消息的具体逻辑
log.info("consumer get a message. body: {}", new String(msg.getBody()));
});
// 标记该消息已经被成功消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
}
}
RocketMqConfig类下添加以下两个方法,用于获取消费者配置对象,并使用消费者配置对象创建、启动消费者
/**
* 启动时获取所有托管在Spring的消费者配置对象,并根据这些配置对象创建消费者
*/
@PostConstruct
public void initConsumer() {
Map<String, AbstractConsumerConfig> configMap = applicationContext.getBeansOfType(AbstractConsumerConfig.class);
if (configMap == null || configMap.size() == 0) {
log.info("init rocket consumer 0");
}
for (Map.Entry<String, AbstractConsumerConfig> entry : configMap.entrySet()) {
AbstractConsumerConfig config = entry.getValue();
config.init();
createAndStartConsumer(config);
log.info("init success consumer title {} , toips {} , tags {}", config.getConsumerTitle(), config.getTopic(),
config.getTags());
}
}
/**
* 通过消费者配置创建消费者,并start
* @param consumerConfig 消费者配置对象
*/
public void createAndStartConsumer(AbstractConsumerConfig consumerConfig) {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(this.rocketMqConfigProperties.getGroupName());
consumer.setNamesrvAddr(this.rocketMqConfigProperties.getNamesrvAddr());
consumer.setConsumeThreadMin(this.rocketMqConfigProperties.getConsumerConsumeThreadMin());
consumer.setConsumeThreadMax(this.rocketMqConfigProperties.getConsumerConsumeThreadMax());
consumer.registerMessageListener(consumerConfig.getMessageListener());
// 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费 如果非第一次启动,那么按照上次消费的位置继续消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 设置消费模型,集群还是广播,默认为集群
consumer.setMessageModel(MessageModel.CLUSTERING);
// 设置一次消费消息的条数,默认为1条
consumer.setConsumeMessageBatchMaxSize(this.rocketMqConfigProperties.getConsumerConsumeMessageBatchMaxSize());
try {
consumer.subscribe(consumerConfig.getTopic(), consumerConfig.getTags());
consumer.start();
consumerConfig.setMqPushConsumer(consumer);
} catch (MQClientException e) {
log.error("info consumer title {}", consumerConfig.getConsumerTitle(), e);
}
}
(四)测试
启动项目,在浏览器中输入 http://localhost:8080/log/add?log=ThisIsFirstLog,添加一条消息
浏览器显示返回信息
查看控制台,控制台打印出以下信息
2021-07-29 16:12:31.887 WARN 3000 --- [nio-8080-exec-2] com.example.controller.LogController : SendResult [sendStatus=SEND_OK, msgId=7F0000010BB818B4AAC293AC710C0007, offsetMsgId=C0A8FF8000002A9F00000000000006F8, messageQueue=MessageQueue [topic=TopicTest, brokerName=localhost.localdomain, queueId=2], queueOffset=3]
2021-07-29 16:12:31.890 INFO 3000 --- [MessageThread_3] c.e.config.rocketmq.LogConsumerConfig : consumer get a message. body: ThisIsFirstLog
消费者已经成功消费了这一条消息
方法二:使用rocketmq-spring-boot-starter
apache上有个叫做rocketmq-spring的项目,将RocketMQ和Spring Boot的集成,封装成了一个rocketmq-spring-boot-starter jar包。借助这个包,我们能简单的通过配置文件、注解和一些必要代码在现有的Spring Boot项目中使用RocketMQ。
(一) 引入依赖,添加RocketMQ相关配置
POM文件引入依赖rocketmq-spring-boot-starter
现在rocketmq-spring-boot-starter的版本是2.2.0,依赖rocketmq-client版本是4.8.0。经测试,服务器上安装了4.9.0的RocketMQ服务,依然可以正常使用。
<!-- 最新版本2.2.0,对应rocketmq-client版本是4.8.0 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
(二) 集成RocketMQ生产者
在application.yml配置文件中,添加生产者的相关配置
server:
port: 8081 # http服务端口
rocketmq:
name-server: 192.168.255.128:9876 # mq的nameserver地址
producer:
group: enterprise_cloud_log # 发送同一类消息的设置为同一个group,保证唯一。默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示
sendMessageTimeout: 300000 # 发送消息超时时间
添加一个MsgController类,之后调用通过http接口去使用生产者发送消息
rocketmq-spring-boot-starter 提供了一个RocketMQTemplate类的对象。RocketMQTemplate自动装配DefaultMQProducer对象,并使用它即发送消息。
因此我们只需要引入RocketMQTemplate的对象,就能发送消息到RocketMQ:
@RestController
@RequestMapping("/msg")
@Slf4j
public class MsgController {
@Resource
private RocketMQTemplate rocketMQTemplate;
@RequestMapping("/send")
public Object send() {
// 同步发送消息,底层调用的syncSend方法
rocketMQTemplate.convertAndSend("TopicTest", "Hello, World! I was sent by convertAndSend");
// 发送一个Spring Message
rocketMQTemplate.send("TopicTest", MessageBuilder.withPayload("Hello, World! I'm from spring message").build());
// 异步发送消息
Map<String,Object> obj = new HashMap<>();
obj.put("id",1);
obj.put("name","Long");
rocketMQTemplate.asyncSend("TopicTest", obj, new SendCallback() {
@Override
public void onSuccess(SendResult var1) {
log.warn("async onSucess SendResult={}", var1);
}
@Override
public void onException(Throwable var1) {
log.warn("async onException Throwable={}", var1);
}
});
// 有序发送消息 - sync
rocketMQTemplate.syncSendOrderly("OrderlyTopicTest",MessageBuilder.withPayload("Message 1. I was sent by syncSendOrderly").build(),"hashkey1");
// 有序发送消息 - async
rocketMQTemplate.asyncSendOrderly("OrderlyTopicTest",MessageBuilder.withPayload("Message 2. I was sent by asyncSendOrderly").build(),"hashkey1",new SendCallback() {
@Override
public void onSuccess(SendResult var1) {
log.warn("async orderly onSucess SendResult={}", var1);
}
@Override
public void onException(Throwable var1) {
log.warn("async orderly onException Throwable={}", var1);
}
});
// 注意: 只要调用了rocketMQTemplate的destroy方法, 就无法使用这个rocketMQTemplate发送消息了。注入的rocketMQTemplate需要特别注意。
//rocketMQTemplate.destroy();
return "success";
}
}
(三) 集成RocketMQ消费者
在application.yml配置文件中,配置rocketmq的name-server即可
rocketmq:
name-server: 192.168.255.128:9876 # mq的nameserver地址
rocketmq-spring提供了一个RocketMQListener接口,我们通过实现这个接口以及其中的onMessage方法,来生成消费者。消费者订阅的主题、消费者分组、消费方式(并行、顺序)、TAG表达式等,都通过注解去配置。
@Slf4j
@Service
@RocketMQMessageListener(topic = "TopicTest", consumerGroup = "my-consumer_test-topic-2",consumeMode = ConsumeMode.CONCURRENTLY)
public class MyConcurrentlyConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String msg) {
log.info("concurrently consumer get a message. body: {}", msg);
}
}
(四)测试
启动项目,在浏览器中输入 http://localhost:8081/msg/send,添加了一些消息
查看控制台,控制台打印出以下信息
2021-08-03 10:46:30.619 INFO 29752 --- [MessageThread_1] c.e.c.rocketmq.MyConcurrentlyConsumer : concurrently consumer get a message. body: {"name":"Long","id":1}
2021-08-03 10:46:30.619 INFO 29752 --- [MessageThread_2] c.e.c.rocketmq.MyConcurrentlyConsumer : concurrently consumer get a message. body: Hello, World! I was sent by convertAndSend
2021-08-03 10:46:30.633 INFO 29752 --- [MessageThread_3] c.e.c.rocketmq.MyConcurrentlyConsumer : concurrently consumer get a message. body: Hello, World! I'm from spring message