Linux系统安装RocketMQ并整合到SpringBoot项目
一、基本概念
1.1 NameServer
NameServer是一个Broker与Topic路由的注册中心,支持Broker的动态注册与发现。
主要包括两个功能:
1)Broker管理:接受Broker集群的注册信息并且保存下来作为路由信息的基本数据;提供心跳监测机制,检查Broker是否还 存活。
2)路由信息管理:每个NameServer中都保存着Broker集群的整个路由信息和用于客户端查询的队列信息。Producer和Consumer通过NameServer可以获取整个Broker集群的路由信息,从而进行消息的投递和消费。
路由信息管理包含以下几个功能:
【1】路由注册:
1.1 NameServer通常也是以集群的方式部署,不过,NameServer是无状态的,即NameServer集群中的各个节点间是无差异的,各节点间相互不进行消息通讯。那各节点中的数据是如何进行数据同步的呢?在Broker节点启动时,轮询NameServer列表,与每个NameServer节点建立长链接,发起注册请求。在NameServer内部维护着一个Broker列表,用来动态存储Broker的信息。
1.2 Broker节点为了证明自己是活着的,为了维护与NameServer的长链接,会将最新的信息以心跳包的方式上报给NameServer,每30秒发送一次心跳。心跳包中包含BrokerId、Broker地址(IP+Port)、Broker名称、Broker所属集群名称等等。NameServer在接收到心跳包后,会更新心跳时间戳,记录这个Broker的最新存活时间。
【2】路由剔除:
2.1 由于Broker关机、宕机或网络抖动等原因,NameServer没有收到Broker的心跳,NameServer可能会将其从Broker列表中剔除。
2.2 NameServer中有一个定时任务,每隔10秒就会扫描一次Broker表,查看每一个Broker的最新心跳时间戳距离当前时间是否超过120秒,如果超过,则会判定Broker失效,然后将其从Broker列表中剔除。
【3】路由发现:
3.1 RocketMQ的路由发现采用的是Pull模型。当Topic路由信息出现变化时,NameServer不会主动推送给客户端,而是客户端定时拉取主题最新的路由。默认客户端每30秒会拉取一次最新的路由。
【4】客户端NameServer选择策略:
4.1 客户端在配置时必须要写上NameServer集群的地址,那么客户端到底连接的是那个NameServer节点呢?客户端首先会产生一个随机数,然后在与NameServer节点数量取模,此时得到的就是所要连接的节点索引,然后会进行连接。如果链接失败,则会采用round-robin策略,逐个尝试去连接其他节点。首先采用的是随机策略进行的选择,失败后采用的是轮询策略。
1.2 Broker
Broker充当着消息中转的角色,负责存储消息、转发消息。Broker在RocketMQ系统中负责接收并存储从生产者发送来的消息,同时为消费者的拉取请求作准备。Broker同时也存储着消息相关的元数据,包括消费者组消费进度偏移offset、主题、队列等。
具体工作流程:
1)启动NameServer,NameServer启动后开始监听端口,等待Broker、Producer、Consumer连接。
2)启动Broker,Broker会与所有的NameServer建立并保持长连接,然后每30秒向NameServer定时发送心跳包
3)发送消息前,可以先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,当然,在创建Topic时也会将Topic与Broker的关系写入到NameServer中。不过,这一步是可选的,也可以在发送消息时自动创建Topic。
4)Producer发送消息,启动时先跟NameServer集群中的一台建立长连接,并从NameServer中获取路由信息,即当前发送的Topic信息的Queue与Broker的地址(IP+Port)的映射关系。然后根据算法策略从队列中选择一个Queue,与队列所在的Broker建立长连接从而向Broker发送消息。当然,在获取到路由信息后,Producer会首先将路由信息缓存到本地,再每30秒从NameServer更新一次路由信息。
5)Consumer与Producer类似,跟其中一台NameServer建立长连接,获取其所订阅Topic的路由信息,然后根据算法策略从路由信息中获取到其所要消费的Queue,然后直接跟Broker建立长连接,开始消费其中的消息。Consumer在获取到路由信息后,同样也会每30秒从NameServer更新一次路由信息。不过不同于Producer的是,Consumer还会向Broker发送心跳,以确保Broker的存活状态。
1.3 Message
消息(Message)是指,消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。
1.3 Topic
主题(Topic)表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主体,是RocketMQ进行消息订阅的基本单位,
一个生产者可以同时发送多种Topic的消息,而一个消费者只对某种特定的Topic感兴趣,即只可以订阅和消费一种Topic的消息。
1.4 Tag
标签(Tag)是指为消息设置的标签,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务的目的在同一主题下设置不同标签。标签能够有效的保持代码的清晰度和连贯性,并优化RocketMQ提供的查询系统。消费者可以根据Tag实现不同的子主题的不同消费逻辑,实现更好的扩展性。(Topic是消息的一级分类,Tag是消息的二级分类)
1.5 Queue
队列(Queue)是指存储消息的物理实体。
一个Topic中可以包含多个Queue,每个Queue中存放的就是该Topic的消息;
一个Topic的Queue也被称为一个Topic中消息的分区(Partition);
一个Topic的Queue中的消息只能被一个消费者组中的一个消费者消费;
一个Queue中的消息不允许同一个消费者组中的多个消费者同时消费;
1.6 MessageId/Key
消息标识(MessageId/Key):RocketMQ中每个消息拥有唯一的MessageId,且可以携带具有业务标识的Key,以方便对消息的查询。不过需要注意的是,MessageId有两个:在生产者send()消息时会自动生成一个MessageId(msgId),当消息到达Broker后,Broker也会自动生成一个MessageId(offsetMsgId)。msgId、offsetMsgId与key都称为消息标识。
1)msgId:由producer端生成,其生成规则为:producerIp+进程pid+MessageClientIDSetter类的ClassLoader的hashCode+当前时间+AutomicInteger自增计数器
2)offsetMsgId:由broker端生成,其生成规则为:brokerIp+物理分区的offset(Queue中的偏移量)
3)key:由用户指定的业务相关的唯一标识
1.7 Producer
消息生产者(Producer),负责生产消息。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程中支持快速失败并且低延迟。
RocketMQ中的消息生产者都是以生产者组(Producer Group)的形式出现的。生产者组是同一类生产者的集合,这类Producer发送相同Topic类型的消息。一个生产者组可以同时发送过个主题的消息。
1.8 Consumer
消息消费者(Consumer),负责消费消息。一个消息消费者会从Broker服务器中获取到消息,并对消息进行相关业务处理。
RocketMQ中的消息消费者都是以消费者组(Consumer Group)的形式出现的。消费者组是同一类消费者的集合,这类Consumer消费的是同一个Topic类型的消息。消费者组使得在消息消费你方面,实现负载均衡(将一个Topic中的不同Queue平均分配给同一个Consumer Group中的不同Consumer,注意,并不是将消息负载均衡)和容错(一个Consumer挂了,该Consumer Group中的其他Consumer可以接着消费原Consumer消费的Queue)的目标变得非常容易。
消费者组中的Consumer的数量应该小于等于订阅Topic的Queue数量。如果超出Queue数量,则多出的Consumer将不能消费消息,不过,一个Topic类型的消息可以被多个消费者组同时消费。
注意:
1)消费者组只能消费一个Topic的消息,不能同时消费多个Topic消息
2)一个消费者组中的消费者必须订阅完全相同的Topic
二、准备工作
2.1 安装JDK环境
由于RocketMQ运行需要用到JDK环境,如系统未配置JDK环境则需要自行安装后再接着往下进行,本文将跳过该步骤。
2.2 下载RocketMQ安装包
官方下载地址:https://rocketmq.apache.org/zh/download
RockerMQ的安装包有两种:1.源码包需要先编译构建成二进制可执行文件后执行,2.二进制包是已经编译完成可以直接运行的。
2.3 上传压缩包到Linux系统并解压
#若系统未安装unzip,需要先使用yum安装unzip (已安装的可忽略)
yum install -y unzip
#进入zip包存放的目录(本教程存放在opt目录下)
cd /opt
#解压文件
unzip rocketmq-all-5.1.1-bin-release.zip
#移动并修改文件名到/usr/local/rocketmq
mv rocketmq-all-5.1.1-bin-release /usr/local
cd /usr/local
mv rocketmq-all-5.1.1-bin-release rocketmq
2.4 修改启动配置信息
#修改 runserver.sh
vim /usr/local/rocketmq/bin/runserver.sh
#修改runbroker.sh
vim /usr/local/rocketmq/bin/runbroker.sh
三、启动与停止
3.1 启动NameServer
nohup sh mqnamesrv &
tail -f ~/logs/rocketmqlogs/namesrv.log
3.2 启动Broker
nohup sh mqbroker -n localhost:9876 &
tail -f ~/logs/rocketmqlogs/broker.log
3.3 使用工具测试发送信息
export NAMESRV_ADDR=localhost:9876
sh tools.sh org.apache.rocketmq.example.quickstart.Producer
3.4 使用工具测试接收信息
sh tools.sh org.apache.rocketmq.example.quickstart.Consumer
3.5 停止Server
#关闭broker
sh mqshutdown broker
#关闭namesrv
sh mqshutdown namesrv
四、安装rocketmq-dashboard控制台
4.1 安装Docker
如果对docker的安装还不熟悉的可以查看我的另一篇文章【Linux安装最新版Docker完整教程】,安装完之后再往下看。
4.2 Docker安装最新版rocketmq-dashboard
#拉取最新镜像
docker pull apacherocketmq/rocketmq-dashboard:latest
#启动dashboard
docker run -d --name rmqdashboard -p 8080:8080 \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.0.132:9876 \
-Dcom.rocketmq.sendMessageWithVIPChannel=false" \
apacherocketmq/rocketmq-dashboard:latest
4.3 访问控制台: http://192.168.0.132:8080
五、整合到SpringBoot项目
5.1 创建项目
5.2 pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
5.3 application.properties
server.port=9999
rocketmq.name-server=192.168.0.132:9876
rocketmq.producer.group=producerGroup
5.4 MessageBody
import lombok.Data;
import java.io.Serializable;
@Data
public class MessageBody implements Serializable {
/**
* 消息id
*/
private String messageId;
/**
* 操作时间
*/
private long timestamp;
/**
* 来源 附加信息
*/
private String msgSource;
/**
* 数据
*/
private Object data;
public MessageBody() {
}
public MessageBody(String msgKey, Object data, String msgSource) {
this.messageId = msgKey;
this.data = data ;
this.msgSource = msgSource;
this.timestamp = System.currentTimeMillis();
}
}
5.5 MQService
import com.xiaoankj.rocketmq.entity.MessageBody;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.UUID;
@Component
public class MQService {
private final static Logger logger = LoggerFactory.getLogger(MQService.class);
private enum MSG_TYPE{
ONEWAY, ASYNC, SYNC };
@Resource
public RocketMQTemplate rocketMQTemplate;
/**
* 发送消息,通用
* @param msgType
* @param destination
* @param payload
*/
private void sendMsg(MSG_TYPE msgType, String destination, Object payload, String msgSource){
String msgKey = UUID.randomUUID().toString().replace("-", "");
MessageBody msgBody = new MessageBody(msgKey, payload , msgSource);
Message<MessageBody> message = MessageBuilder.withPayload(msgBody).setHeader("KEYS", msgKey).build();
logger.info(String.format("消息发送 MQService 开始: %s %s", destination, message));
SendResult result = null;
switch (msgType) {
case ONEWAY:
rocketMQTemplate.sendOneWay(destination, message);
break;
case ASYNC:
rocketMQTemplate.asyncSend(destination, message,new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
}
@Override
public void onException(Throwable throwable) {
logger.error("MQService:" + ExceptionUtils.getStackTrace(throwable));
throw new RuntimeException(String.format("消息发送失败 topic_tag:%s", destination ));
}
});
break;
case SYNC:
result = rocketMQTemplate.syncSend(destination, message);
break;
}
logger.info(String.format("消息发送 MQService 结束: msgId: %s dest: %s msg: %s",result != null ? result.getMsgId() : "", destination, message));
}
/**
* 同步发送消息,会确认应答
* @param destination
* @param payload
*/
public void syncSendMsg(String destination, Object payload, String msgSource){
sendMsg(MSG_TYPE.SYNC,destination, payload,msgSource) ;
}
/**
* 同步发送消息,会确认应答
* @param topic
* @param tag
* @param payload
*/
public void syncSendMsg(String topic, String tag, Object payload, String msgSource){
String destination = topic + ":" + tag;
syncSendMsg(destination, payload,msgSource);
}
/**
* 异步消息发送,异步日志确认异常
* @param destination
* @param payload
*/
public void asyncSendMsg(String destination, Object payload, String msgSource){
sendMsg(MSG_TYPE.ASYNC, destination, payload,msgSource);
}
/**
* 异步消息发送,异步日志确认异常
* @param topic
* @param tag
* @param payload
* @return
*/
public void asyncSendMsg(String topic, String tag, Object payload, String msgSource){
String destination = topic + ":" + tag;
asyncSendMsg(destination, payload, msgSource);
}
/**
* 单向发送消息,不关注结果
* @param destination
* @param payload
*/
public void oneWaySendMsg(String destination, Object payload, String msgSource){
sendMsg(MSG_TYPE.ONEWAY, destination, payload, msgSource);
}
/**
* 单向发送消息,不关注结果
* @param topic
* @param tag
* @param payload
*/
public void oneWaySendMsg(String topic, String tag, Object payload, String msgSource){
String destination = topic + ":" + tag;
oneWaySendMsg(destination, payload, msgSource);
}
}
5.6 ComsumerListener
import com.xiaoankj.rocketmq.entity.MessageBody;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
@RocketMQMessageListener(topic = "test-topic",nameServer = "${rocketmq.name-server}",consumerGroup = "${rocketmq.producer.group}", selectorExpression = "test-tag")
@Component
@Slf4j
public class ComsumerListener implements RocketMQListener<MessageBody> {
@Override
public void onMessage(MessageBody messageBody) {
System.out.println(messageBody);
}
}
5.7 ComsumerListener
import com.alibaba.fastjson.JSONObject;
import com.xiaoankj.rocketmq.utils.MQService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping("test")
public class TestController {
@Resource
private MQService mQService;
/**
* 发送消息
* @return
*/
@GetMapping("sendMsg")
public String sendMsg() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", "小明");
jsonObject.put("sex", "男");
jsonObject.put("age", 18);
mQService.syncSendMsg("test-topic", "test-tag", jsonObject, "消息来源~");
return "发送成功!";
}
}
六、测试接口
6.1 启动SpringBoot项目
6.2 浏览器请求测试接口: http://localhost:9999/test/sendMsg