Linux下用RocketMQ实现服务消息总线
文章目录
前言
产品越来越复杂,微服务越来越繁杂。
如何控制好各服务节点的业务同步,就成了一个必须要面对的问题。
比如:人员离职,那在各个业务系统下的权限清理,组织关系,甚至内部IM通讯的状态、群组的清退等等,以往要拿一张单子在各个部门走一遍,除了财务和运维,其他是没有实物交接的,就造成了人力无力的浪费。
于是,便需要引入一个机制,实现一站式服务,我们称其为:“一键byebye“。
考虑以后架构中对于边缘硬件的接入,这个机制便要考虑推而广之,不能因为一个硬件问题的协调不及时,影响整个系统。
经过考虑和权衡,总线的设计思想基本占据了优势。
一、选型
基于系统的套接字和封装,消息总线的底层采用RocketMQ。
基于未来业务的考虑,消息协议使用MQTT 5.0标准封装,QoS等级为2。
二、准备工作
1.部署rocketMQ及踩过的坑
官方文档写的很详细,不做赘述。链接直达:https://rocketmq.apache.org/docs/quick-start/
下文以CentOS 7为背景,Windows环境大同小异。
再说一下可能会跳的坑:
1.1 防火墙:
部署启动后,可能出现外网无法访问的情况,防火墙的端口除了默认可见的如:9876和10911以外,还要根据服务判断一下其他开放的端口,比如10909,10912等。
基本命令如下:
放开端口10911:
firewall-cmd --zone=public --add-port=10911/tcp --permanent
重启防火墙:
systemctl restart firewalld.service
查看已生效端口:
firewall-cmd --list-ports
1.2 Docker
Docker没有官方镜像,网上的镜像也都是基于旧版封装的,坑很多。有动手能力可以自己封装docker镜像,初探的话,还是老老实实在本地先跑通。
1.3 项目依赖
rocketMQ脱胎于阿里巴巴,所以springCloud官方有造好的轮子,maven直接引用:
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-bus-rocketmq</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
1.4 RocketMQ配置
./bin/runbroker.sh和./bin/runserver.sh里面的默认配置很占内存。
修改里面-Xms8g、 -Xmx8g、 -Xmn2g这些配置,改为合适的内存初始化大小。
1.5 RocketMQ服务启停
rocketMQ broker启动时会检查系统环境参数配置是否有NAMESRV_ADDR,有与没有都会引发一些问题,为避免环境的配置问题,可以更新一下系统环境参数:
export NAMESRV_ADDR=localhost:9876
2.通讯
设计实现的效果基本如下:
如何更好地利用rocketMQ的机制来实现呢?
将rocketMQ的特点结合微服务的节点信息,就可以很好的实现分服务,分节点的分发消息,并且控制好消息的消费。
RocketMQ有两种消费模式:BROADCASTING广播模式,CLUSTERING集群模式,默认的是 集群消费模式。
模式的设定在消费端完成,MessageModel.BROADCASTING和MessageModel.CLUSTERING,后文会给出简单的例子。
RocketMQ中的topic、group、tag的概念,网上讲的不多。
结合消费模式,总的来说:
- 广播模式下,同一个topic里的每一个消费者都能完整接收所有消息;
- 集群模式下,同一个topic里,
如果消费者的group一样,消息被各消费节点分掉,即消息1被A消费,那消息1就不会到B,B可能收到的只能是消息2及以后的消息;
如果消费者的group不一样,那消息会被每个消费者完整的消费一遍。
基于以上特性,topic就是上图中各种颜色的管道,group就是一个管道上的不同龙头,也就是同一个业务的不同节点。所以,开工!
2.1、创建Topic
命令行:
./bin/mqadmin updateTopic -b localhost:10911 -t TopicTest
2.2、 代码如下:
消费者1:
public class Consumer {
public static void main(String[] args) throws MQClientException {
//声明并初始化一个consumer
//需要一个consumer group名字作为构造方法的参数,这里为consumer1
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer1");
//同样也要设置NameServer地址
consumer.setNamesrvAddr("192.168.11.128:9876");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//设置consumer所订阅的Topic和Tag,*代表全部的Tag
consumer.subscribe("TopicTest", "*");
//设置一个Listener,主要进行消息的逻辑处理
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt messageExt : msgs) {
String messageBody = new String(messageExt.getBody());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(
new Date())+"消费响应:msgId : " + messageExt.getMsgId() + ", msgBody : " + messageBody);//输出消息内容
}
//返回消费状态
//CONSUME_SUCCESS 消费成功
//RECONSUME_LATER 消费失败,需要稍后重新消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//调用start()方法启动consumer
consumer.start();
System.out.println("Consumer1 Started.");
}
}
消费者2:
public class Consumer2 {
public static void main(String[] args) throws MQClientException {
// 声明并初始化一个consumer
// 需要一个consumer group名字作为构造方法的参数,这里为consumer2
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer2");
// 同样也要设置NameServer地址
consumer.setNamesrvAddr("192.168.11.128:9876");
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 设置consumer所订阅的Topic和Tag,*代表全部的Tag
consumer.subscribe("TopicTest", "*");
// 设置一个Listener,主要进行消息的逻辑处理
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for (MessageExt messageExt : msgs) {
String messageBody = new String(messageExt.getBody());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())
+ "2--------消费响应:msgId : " + messageExt.getMsgId() +
", msgBody : " + messageBody);// 输出消息内容
}
// 返回消费状态
// CONSUME_SUCCESS 消费成功
// RECONSUME_LATER 消费失败,需要稍后重新消费
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 调用start()方法启动consumer
consumer.start();
System.out.println("Consumer2 Started.");
}
}
生产者:
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
//声明并初始化一个producer
//需要一个producer group名字作为构造方法的参数,这里为producer1
DefaultMQProducer producer = new DefaultMQProducer("producer1");
//设置NameServer地址,此处应改为实际NameServer地址,多个地址之间用;分隔
//NameServer的地址必须有,但是也可以通过环境变量的方式设置,不一定非得写死在代码里
producer.setNamesrvAddr("192.168.11.128:9876");
//调用start()方法启动一个producer实例
producer.start();
//发送10条消息到Topic为TopicTest,tag为TagA,消息内容为“Hello RocketMQ”拼接上i的值
for (int i = 0; i < 100; i++) {
try {
Message msg = new Message("TopicTest",// topic
"TagA",// tag
("Hello RocketMQ " + i).getBytes("utf-8")// body
);
//调用producer的send()方法发送消息
//这里调用的是同步的方式,所以会有返回结果
SendResult sendResult = producer.send(msg);
System.out.println(sendResult.getSendStatus()); //发送结果状态
//打印返回结果,可以看到消息发送的状态以及一些相关信息
System.out.println(sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(1000);
}
}
//发送完消息之后,调用shutdown()方法关闭producer
producer.shutdown();
}
}
2.3、结果
先运行两个消费者,再运行生产者,结果如下:
至此,最初的想法已基本验证完毕。
三、中间件的封装
需要思考的问题:
如何保证RocketMQ消息的可靠性?
如何保证服务节点的执行结果?
既然是队列,就离不开解耦、削峰和数据分发的功能;
既然是中间件,就要考虑独立、可靠地完成消息传递的工作。
1、监控
引入rocketmq-console服务,项目地址 https://github.com/SummerUnfair/rocketmq-externals.git
具体使用方法见git文档。
2、架构
- 引入SpringCloud Stream,设计生产者集群、消费者集群、消息的防灾、过期消息清理;
- 设计路由中心,多路复用、负载均衡;
- 数据一致性校验;
- 消息的同步、异步、单向、顺序、批量、过滤的处理;
- 事务消息及事务的管理
………………
这是消息总线最终应该有的样子:
总结
以上就是最近一个阶段的简单记录。
– 为了理想肝脑涂地