消息队列中间件是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题
实现高性能,高可用,可伸缩和最终一致性架构。使用较多的消息队列有ActiveMQ,RabbitMQ,ZeroMQ,Kafka,MetaMQ,RocketMQ,这边着重介绍RocketMQ的搭建和使用。RocketMQ是阿里研发的一个队列模型的消息中间件,后开源给apache基金会成为了apache的顶级开源项目,具有高性能、高可靠、高实时、分布式特点。
消息队列在互联网技术开发中使用非常广泛,主要用于解决应用耦合,消息分发,流量削锋等问题。以下介绍消息队列在实际应用中常用的使用场景(都以用户下单为例)。
场景说明: 一个简单的用户下单后根据支付金额增加用户积分的场景,传统模式下需要订单模块调用积分模块接口,这样的话订单模块与积分模块就形成了系统耦合,一旦积分模块有修改或出现异常就会影响订单模块功能。引入消息队列方案后, 用户下单成功后,将消息写入消息队列就可以了。积分模块只需要订阅下单消息,从消息队列中获取数据进行消费,这样订单模块和积分模块都只要专注实现自己的功能实现,实现解耦。
场景说明:用户下单后日志模块要记录下单日志,库存模块需要减少相应库存,积分模块需要增加用户积分等由下单成功引起的其余模块的业务操作,这个时候可以通过消息队列可以让数据在多个系统更加之间进行流通。数据的产生方不需要关心谁来使用数据,只需要将数据发送到消息队列,数据使用方直接在消息队列中直接获取数据即可。
场景说明:双十一期间系统受到的请求流量猛增,有可能会将系统压垮。传统做法是为了保证系统的稳定性,一般是增加服务器配置、新增服务器做负载均衡这样的话在正常时间段都能满足服务的情况下采用这种做法无疑是对服务器性能的一种浪费,并不划算!另一种做法是如果系统负载超过阈值,就会阻止用户请求,但在流量高峰时这会影响用户体验。通过消息队列就可以完美解决这个问题,引入消息队列方案后可以将大量请求缓存起来,分散到很长一段时间处理,这样可以大大提到系统的稳定性和用户体验。
RocketMQ是一个纯Java、分布式、队列模型的开源消息中间件,搭建RocketMQ需要先配置JAVA环境变量。
进入官网选择需要的版本下载安装包(以下以4.8.0为例)。官网下载地址:官网下载
下载编译好的二进制文件,也可以自己选择源文件。这边主要以二进制包的方式来进行安装。
进入下载也如下图:
1、解压安装包,进入安装目录的bin文件夹,这里包含MQ启动脚本,包括sh和cmd脚本;
2、修改虚拟机内存,RocketMQ 默认的虚拟机内存较大,启动 Broker 或者 NameServer 可能会因为内存不足而导致失败,需要根据实际情况修改修改Broker和NameServer的虚拟机内存。编译runbroker和runserver文件(win环境下修改cmd脚本,linux环境下修改sh脚本即可),以下是参考设置:
# 以linux环境下为例,编辑 runbroker.sh 和 runserver.sh 修改默认 JVM 大小
$ vi bin/runbroker.sh
# 修改-server参数
set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g -Xmn1g"
$ vi bin/runserver.sh
# 修改-server参数
set "JAVA_OPT=%JAVA_OPT% -server -Xms2g -Xmx2g -Xmn1g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
1、启动NameServer
# windows环境下启动
.\mqnamesrv
# linux环境下启动
sh mqnamesrv
# 后台启动
nohup sh mqnamesrv &
2、启动Broker
# windows环境下启动
.\mqbroker -n 127.0.0.1:9876
# linux环境下启动
sh mqbroker -n 127.0.0.1:9876
# 后台启动
nohup sh mqbroker -n localhost:9876 &
#指定配置文件启动
nohup sh mqbroker -n localhost:9876 -c 配置文件目录 &
如果 MQ 处于开启成功后,会有四个端口,其中,一个 namesrv 端口,默认是 9876,还有三个 broker 端口(10909、10911、10912)
如果需要本地开发环境连接到rocketmq的服务器进行消息推送,需要开启修改对应的配置文件实现。配置文件再mq根目录下的conf文件夹中;
3、关闭MQ服务
# windows环境下启动
.\mqshutdown namesrv //关闭NameServer
.\mqshutdown broker //关闭Broker
# linux环境下启动
sh bin/mqshutdown namesrv //关闭NameServer
sh bin/mqshutdown broker //关闭Broker
window下如果无法启动broker,删除c:/user/用户名/store下所有问下然后重新启动即可。
rocketmq-dashboard是RocketMQ的一个拓展开源项目,可以对MQ进行可视化监控。
下载地址:https://github.com/apache/rocketmq-dashboard
1、下载项目并将配置文件中的namesrv地址改成自己的NameServer地址;
2、配置完成后进行编译打包,然后启动打包完成的jar就可以了;
启动成功后就可以通过浏览器访问 http://ip地址:端口 进入控制台页面,可视化查询MQ服务的相关信息,如下图所示:
控制台详细使用可以查看:https://github.com/apache/rocketmq-dashboard/blob/master/docs/1_0_0/UserGuide_CN.md
至此RocketMQ安装完毕。
在项目中引入MQ客户端依赖,依赖版本最好和RocketMQ版本一致;
<!--rocketmq包-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
首先介绍一下消息发送的大致流程,当我们调用消息发送方法时该方法会先对待发送消息进行前置验证,如果消息主题和消息内容均没有问题的话,就会根据消息主题(Topic)去获取路由信息,即消息主题对应的队列,broker,broker的ip和端口信息,然后选择一条队列发送消息,成功的话返回发送成功,失败的话会根据我们设置的重试次数进行重新发送,单向消息发送不会进行失败重试!(具体的可以查看MQ源码,这边就不一一赘述了)!
参数名 | 参数说明 |
---|---|
producerGroup | 消息生产者组名,一般一个应用的消息生产者应将其归为同一个消息生产组 |
nameserAddr | 生产者nameservice地址 |
sendMsgTimeOut | 消息发送超时时间,单位毫秒 |
maxMessageSize | 消息最大大小,默认4M |
retryTimesWhenSendFailed | 消息发送失败重试次数,默认2次 |
executorService | 事务消息处理线程池 |
transactionListener | 事务消息监听器 |
/***************************消息生产者***************************/
@Autowired
private MQTransactionListener mqTransactionListener; //TODO 事务消息监听器
//TODO 消息生产者配置信息
@Value("${rocketmq.producer.namesrvAddr:127.0.0.1:9876}")
private String pNamesrvAddr; //TODO 生产者nameservice地址
@Value("${rocketmq.producer.maxMessageSize:4096}")
private Integer maxMessageSize ; //TODO 消息最大大小,默认4M
@Value("${rocketmq.producer.sendMsgTimeout:30000}")
private Integer sendMsgTimeout; //TODO 消息发送超时时间,默认3秒
@Value("${rocketmq.producer.retryTimesWhenSendFailed:2}")
private Integer retryTimesWhenSendFailed; //TODO 消息发送失败重试次数,默认2次
private static ExecutorService executor = ThreadUtil.newExecutor(32);//TODO 执行任务的线程池
//普通消息生产者
@Bean("default")
public DefaultMQProducer getDefaultMQProducer() {
DefaultMQProducer producer = new DefaultMQProducer(this.groupName);
producer.setNamesrvAddr(this.pNamesrvAddr);
producer.setMaxMessageSize(this.maxMessageSize);
producer.setSendMsgTimeout(this.sendMsgTimeout);
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
try {
producer.start();
} catch (MQClientException e) {
System.out.println(e.getErrorMessage());
}
return producer;
}
//事务消息生产者(rocketmq支持柔性事务)
@Bean("transaction")
public TransactionMQProducer getTransactionMQProducer() {
//初始化事务消息基本与普通消息生产者一致
TransactionMQProducer producer = new TransactionMQProducer("transaction_" + this.groupName);
producer.setNamesrvAddr(this.pNamesrvAddr);
producer.setMaxMessageSize(this.maxMessageSize);
producer.setSendMsgTimeout(this.sendMsgTimeout);
producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
//添加事务消息处理线程池
producer.setExecutorService(executor);
//添加事务消息监听
producer.setTransactionListener(mqTransactionListener);
try {
producer.start();
} catch (MQClientException e) {
System.out.println(e.getErrorMessage());
}
return producer;
}
如果是要使用事务消息的话如要设置事务消息处理线程池和事务消息监听器,监听器和消费者监听类似,后面会在消费者介绍中说明;
1)、消息发送根据消息功能主要分为普通消息、事务消息、顺序消息、延时消息等!特别说明一下事务消息多用于保证多服务模块间的事务一致性!事务消息发送后并不会直接通知消费者消费消息,而是会先生成一个半消息,会先进入事务消息监听器中,确保该消息事务提交成功后才会向broker发送消息,从而被消费者获取并进行消费;
2)、根据发送方式可以分为同步消息,异步消息和单向消息等,其中同步消息常用于比较重要的消息发送,需要等待broker响应告知消息发送状态;异步消息的话常用于对想用时间敏感,需要快速返回的模块,我们会设置一个回调代码块去异步监听Borker的响应;单向消息的话主要用于对发送结果不敏感,不会影响业务的模块,无需监听broker响应,常用于日志发送等模块。示例如下:
/**
* 添加订单(发送消息积分模块同步添加积分)
* zlx
* 12:09 2021/6/4
* @param order 订单信息
* @return org.apache.rocketmq.client.producer.TransactionSendResult
**/
@Override
public Order addOder(Order order) {
order.setOrderId(SonwflakeUtils.get().id());
if (order.getMessageType() == 1) {
//普通消息
this.save(order);
Message message = new Message("points","default", JSON.toJSONString(order).getBytes());
try {
SendResult sendResult = producer.send(message);//同步消息
System.out.println("发送状态:" + sendResult.getSendStatus() +
",消息ID:" + sendResult.getMsgId() +
",队列:" + sendResult.getMessageQueue().getQueueId());
// producer.sendOneway(message);//单向消息
// producer.send(message, new SendCallback() {//异步消息
// @Override
// public void onSuccess(SendResult sendResult) {
//
// }
//
// @Override
// public void onException(Throwable throwable) {
//
// }
// });
} catch (RemotingException | MQBrokerException | InterruptedException | MQClientException e) {
e.printStackTrace();
}
} else {
//事务消息
Message message = new Message("points","transaction", JSON.toJSONString(order).getBytes());
try {
transactionMQProducer.sendMessageInTransaction(message, null);
} catch (MQClientException e) {
e.printStackTrace();
}
}
return order;
}
参数名 | 参数说明 |
---|---|
producerGroup | 消费者组名,一般一个应用的消息生产者应将其归为同一个消息生产组 |
nameserAddr | 消费者nameservice地址 |
consumeThreadMin | 消费者线程最小线程数 |
consumeThreadMax | 消费者线程最大线程数 |
subscribe | 消费者订阅主题信息,如果是订阅该主题下的所有tag,则tag使用*;如果需要指定订阅该主题下的某些tag,则使用 |
consumeMessageBatchMaxSize | 并发消费条数,默认为1 |
@Value("${spring.application.name:application}")
private String groupName;//集群名称,这边以应用名称作为集群名称
/***************************消息生产者***************************/
@Autowired
private Map<String, MQHandler> mqHandlerMap;
//TODO 消息消费者配置信息
@Value("${rocketmq.consumer.namesrvAddr:127.0.0.1:9876}")
private String cNamesrvAddr; //TODO 消费者nameservice地址
@Value("${rocketmq.consumer.consumeThreadMin:20}")
private int consumeThreadMin; //TODO 最小线程数
@Value("${rocketmq.consumer.consumeThreadMax:64}")
private int consumeThreadMax; //TODO 最大线程数
@Value("${rocketmq.consumer.topics:test~*}")
private String topics; //TODO 消费者监听主题,多个主题以分号隔开(topic~tag;topic~tag)
@Value("${rocketmq.consumer.consumeMessageBatchMaxSize:1}")
private int consumeMessageBatchMaxSize; //TODO 一次消费消息的条数,默认为1条
@Bean
public DefaultMQPushConsumer getRocketMQConsumer() throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
consumer.setNamesrvAddr(cNamesrvAddr);
consumer.setConsumeThreadMin(consumeThreadMin);
consumer.setConsumeThreadMax(consumeThreadMax);
consumer.registerMessageListener(getMessageListenerConcurrently());
//TODO 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费,如果非第一次启动,那么按照上次消费的位置继续消费
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
//TODO 设置消费模型,集群还是广播,默认为集群
//consumer.setMessageModel(MessageModel.CLUSTERING);
//TODO 设置一次消费消息的条数,默认为1条
consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
try {
//TODO 设置该消费者订阅的主题和tag,如果是订阅该主题下的所有tag,则tag使用*;如果需要指定订阅该主题下的某些tag,则使用||分割,例如tag1||tag2||tag3
String[] topicTagsArr = topics.split(";");
for (String topicTags : topicTagsArr) {
String[] topicTag = topicTags.split("~");
consumer.subscribe(topicTag[0],topicTag[1]);
}
consumer.start();
}catch (Exception e){
throw new Exception(e);
}
return consumer;
}
//TODO 并发消息侦听器(如果对顺序消费有需求则使用MessageListenerOrderly 有序消息侦听器)
@Bean
public MessageListenerConcurrently getMessageListenerConcurrently() {
return new MQListenerConcurrently(mqHandlerMap);
}
这边对MessageListenerConcurrently有进行一定封装,主要是为了在消息处理时通过注解定位消息Topic和tag而自动选择对应的消息处理类进行业务处理;封装代码如下:
/**
* 并发消息监听器
*/
public class MQListenerConcurrently implements MessageListenerConcurrently {
@Autowired
private Map<String, MQHandler> mqHandlerMap;
public MQListenerConcurrently(Map<String, MQHandler> mqHandlerMap) {
this.mqHandlerMap = mqHandlerMap;
}
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
if(CollectionUtils.isEmpty(list)){
System.out.println("接受到的消息为空,不处理,直接返回成功");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
MessageExt messageExt = list.get(0);
//TODO 判断该消息是否重复消费(RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重)
//TODO 获取该消息重试次数
int reconsume = messageExt.getReconsumeTimes();
if(reconsume ==3){//消息已经重试了3次,需做告警处理,已经相关日志
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
//TODO 处理对应的业务逻辑
String topic = messageExt.getTopic();
String tags = messageExt.getTags();
System.out.println("接受到的消息主题为:" + topic + "; tag为:" + tags);
MQHandler mqMsgHandler = null;
//获取消息处理类中的topic和tag注解,根据topic和tag进行策略分发出来具体业务
for (Map.Entry<String, MQHandler> entry : mqHandlerMap.entrySet()) {
MQHandlerActualizer msgHandlerActualizer = entry.getValue().getClass().getAnnotation(MQHandlerActualizer.class);
if (msgHandlerActualizer == null) {
//非消息处理类
continue;
}
String annotationTopic = msgHandlerActualizer.topic();
if (!StrUtil.equals(topic,annotationTopic)) {
//非该主题处理类
continue;
}
String[] annotationTags = msgHandlerActualizer.tags();
if(StrUtil.equals(annotationTags[0],"*")){
//获取该实例
mqMsgHandler = entry.getValue();
break;
}
boolean isContains = Arrays.asList(annotationTags).contains(tags);
if(isContains){
//注解类中包含tag则获取该实例
mqMsgHandler = entry.getValue();
break;
}
}
if (mqMsgHandler == null) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
ConsumeConcurrentlyStatus status = mqMsgHandler.handle(tags,messageExt);
// 如果没有return success,consumer会重新消费该消息,直到return success
return status;
}
事务消息监听器也是类似以封装,具体可下载示例代码查看;
使用注解@MQHandlerActualizer标明该消息处理类的主题,默认监听所有tag,如果需要对tag监听进行分类,后面加上tag即可!消息监听器在收到消息后会自动调用主题对应的处理类进行业务处理,示例如下:
@MQHandlerActualizer(topic = "points")
public class PointsMQHandler implements MQHandler {
@Autowired
private PointsService pointsService;
@Override
public ConsumeConcurrentlyStatus handle(String tag, MessageExt messageExt) {
//消息监听
String messageStr = new String(messageExt.getBody());
Map orderMap = (Map) JSON.parse(messageStr);
Points points = new Points();
Long orderId = (Long) orderMap.get("orderId");
System.out.println("消息tag为:" + tag);
System.out.println("消息监听:" + "为订单" + orderId + "添加积分");
//查询该订单是否已经生成对应积分(rocketMQ可能会重复发送消息,需实现幂等)
QueryWrapper<Points> pointsQueryWrapper = new QueryWrapper<>();
pointsQueryWrapper.lambda().eq(Points::getOrderId,orderId);
Points tempPoints = pointsService.getOne(pointsQueryWrapper);
if (tempPoints != null) {
//该订单已经生成积分
System.out.println(orderId + "已经生成积分");
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
points.setPointsId(SonwflakeUtils.get().id());
points.setOrderId(orderId);
Integer orderAmout = (Integer) orderMap.get("orderAmout");
points.setPoints(orderAmout * 10);
pointsService.save(points);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
1、RocketMQ确保所有消息至少传递一次。虽然大多数情况下,消息不会重复。但还是需要对重复消息做;
2、尽量减小消息的体积,例如选择轻量的协议,超过一定体积做压缩处理,就消息协议而言, 二进制协议 < 文本协议。而文本协议中 json < xml 等等;
以上便是RocketMQ的基本介绍,可以配合示例demo一起阅读;