MQ分布式消息队列
MQ消息队列:就是一个中间件。
mq的分类:RocketMq 、Kafka 、RabbitMq 、MqTT
MQ优势
提高系统响应速度:任务异步处理。 将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
提高系统稳定性:一是并发被消峰后,系统不容易被高并发打垮,二是系统挂了也没关系,操作内容放到消息队列不丢失,后续重新消费者一样能消费做业务处理。
排序保证 FIFO:遵循队列先进先出的特点,能够保证消息按照添加的数据被消费。
一、MQ的认识
MQ全称为Message Queue,即消息队列 ,是一种提供消息队列服务的中间件,也称为消息中间件,是一套提供了消息生 产、存储、消费全过程的软件系统,遵循FIFO原则。
二、MQ的使用场景
限流削峰:
MQ可以将系统的超量请求暂存其中,以便系统后期可以慢慢进行处理,从而避免了请求的丢失或系统 被压垮。
异步&解耦
上游系统对下游系统的调用若为同步调用,则会大大降低系统的吞吐量与并发度,且系统耦合度太高。 而异步调用则会解决这些问题。所以两层之间若要实现由同步到异步的转化,一般性做法就是,在这两层间添加一个MQ层。 即使消费者挂掉也不影响生产者工作,只要把消息放入队列即可,消费者重启后自己消费即可。
数据收集
分布式系统会产生海量级数据流,如:业务日志、监控数据、用户行为等。针对这些数据流进行实时或 批量采集汇总,然后对这些数据流进行大数据分析,这是当前互联网平台的必备技术。通过MQ完成此 类数据收集是最好的选择。
大数据处理
比如我们的平台向“三方平台”获取数据,一次请求了大量数据回来要进行处理,由于数据较多处理不过来,那么就可以放入MQ,再创建一些消费者进行数据处理即可。
【注意】如下情况不太适合MQ
小项目,体量不大,并发量低的使用MQ会太过笨重 - 你可以考虑使用Redis做一个消息队列。
对数据的一致性有要求(强一致性)的的场景不适合使用MQ,因为MQ是异步的。
RocketMQ介绍
RocketMQ是什么
RocketMQ是一个统一消息引擎、轻量级数据处理平台。
RocketMQ是⼀款阿⾥巴巴开源的消息中间件,双十一承载了万亿级消息的流转,2016年11⽉,阿⾥巴巴向 Apache 软件基⾦会捐赠 RocketMQ,成为 Apache 孵化项⽬,2017 年 9 ⽉ ,Apache 宣布 RocketMQ孵化成为 Apache 顶级项⽬(TLP )成为国内⾸个互联⽹中间件在 Apache 上的顶级项⽬。
RocketMQ特征
- 支持集群模型、负载均衡、水平扩展能力
- 亿级别消息堆积能力
- 采用零拷贝的原理,顺序写盘,随机读
- 底层通信框架采用Netty NIO
- NameServer代替Zookeeper,实现服务寻址和服务协调
- 消息失败重试机制、消息可查询
- 强调集群无单点,可扩展,任意一点高可用,水平可扩展
- 经过多次双十一的考验
RocketMQ的安装
1.下载RocketMQ
下载地址:RocketMQ下载官网
解压即可使用
2.配置ROCKETMQ_HOME环境变量
3.启动MQ
1.启动NameServer
Cmd命令框执行进入至MQ文件夹\bin下,然后执行 start mqnamesrv.cmd,启动NameServer。
成功后会弹出提示框,此框勿关闭。
2.启动Broker
进入至MQ文件夹\bin下,修改bin目录下的 runbroker.cmd 中JVM占用内存大小
CMD执行start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true ,启动Broker。
下载RocketMQ可视化插件
RocketMQ可视化管理插件下载地址:下载地址
修改配置
解压后,修改配置:src/main/resource/application.properties ,这里需要指向Name Server 的地址和端口 如下:
打包插件:
回到安装目录(pom.xml所在目录),执行: mvn clean package -Dmaven.test.skip=true ,然后会在target目录生成打包后的jar文件
启动插件
进入 target 目录,CMD执行 java -jar rocketmq-console-ng-1.0.0.jar , 访问 http://localhost:8080
RocketMQ的原理
RokcetMQ架构
RocketMQ的集群架构如下
1.Producer
消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。
2.Consumer
消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。
3.Broker
Broker主要负责消息的存储、投递和查询以及服务高可用保证。
4.NameServer
NameServer是一个Broker与Topic路由的注册中心支持Broker的动态注册与发现,主要包括两个功能
- Broker管理
NameServer接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活。 - 路由信息管理
每个NameServer将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费
RocketMQ的使用
导入pom
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.8.0</version>
</dependency>
</dependencies>
一、普通消息
1.同步消息
同步消息是发送者发送消息,需要等待结果的返回,才能继续发送第二条消息,这是一种阻塞式模型,虽然消息可靠性高,但是阻塞导致性能低。
API : SendResult result = producer.send(message);
代码如下(示例):
public class Producer {
//演示消息同步发送
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
//生产者
DefaultMQProducer producer = new DefaultMQProducer("syn-producerGroup");
//设置name server地址
producer.setNamesrvAddr("127.0.0.1:9876");
//设置队列数量为2,默认为4,根据情况设置
producer.setDefaultTopicQueueNums(2);
//启动
producer.start();
for (int i = 0 ; i < 16 ; i++){
Message message = new Message();
//消息主题
message.setTopic("syn-topic");
//消息标签
message.setTags("sms");
//添加内容
message.setBody((i+"我是消息").getBytes(CharsetUtil.UTF_8));
//执行发送
SendResult result = producer.send(message);
//打印结果
System.out.println(result);
}
producer.shutdown();
} }
同步发送使用 SendResult result = producer.send(message); 方法即可,马上可以拿到返回值。
- SendStatus : 状态OK
- msgId: 发送者生成的ID
- OffsetMsgId : 由Broker生成的消息ID
- MessageQueue :队列信息
2、异步消息
异步消息是发送者发送消息,无需等待发送结果就可以再发送第二条消息,它是通过回调的方式来获取到消息的发送结果,消息可靠性高,性能也高
API : producer.send(message,SendCallback) 示例代码:
producer.send(
//创建消息对象
new Message("asyn-topic", "sms", "我是消息".getBytes(CharsetUtil.UTF_8)),
//添加发送回调
new SendCallback() {
//发送成功结果处理
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
//发送异常结果处理
@Override
public void onException(Throwable throwable) {
System.out.println("发送异常:"+throwable.getMessage());
}
}
);
SendCallback 是消息发送结果回调。如果:sendResult.getSendStatus() == SendStatus.SEND_OK 表示成功
3.单向消息
这种方式指的是发送者发送消息后无需等待Broker的结果返回,Broker也不会返回结果,该方式性能最高,但是消息可靠性低。API : producer.sendOneway(message) 示例代码:
... 省略...
Message message = new Message("asyn-topic", "sms", "我是消息".getBytes(CharsetUtil.UTF_8));
producer.sendOneway(message);
sendOneway 单向发送是没有返回结果值的。
延迟消息
我们通常使用定时任务比如Quartz来解决超时业务,比如:订单支付超时关单,VIP会员超时提醒。但是使用定时任务来处理这些业务场景在数据量大的时候并不是一个很好的选择,会造成大量的空扫描浪费性能。我们可以考虑使用延迟消息来解决。
1.消息发送者
public class Producer {
//演示消息同步发送
public static void main(String[] args) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
//生产者
DefaultMQProducer producer = new DefaultMQProducer("syn-producerGroup-delay");
//设置name server地址
producer.setNamesrvAddr("127.0.0.1:9876");
//启动
producer.start();
for (long i = 0 ; i < 4 ; i++){
Order order = new Order(i,"订单"+i,"创建");
//添加内容
byte[] bytes = (JSON.toJSONString(order)).getBytes(CharsetUtil.UTF_8);
Message message = new Message("topic-order-delay","product-order-delay",bytes);
//延迟级别 3,代表 10s延迟
message.setDelayTimeLevel(3);
message.setKeys("key-"+i);
//执行发送
SendResult result = producer.send(message);
System.out.println("发送时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(result);
}
producer.shutdown();
}
}
2.消息消费者
public class Consumer {
public static void main(String[] args) throws MQClientException {
//创建消费者
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("syn-consumerGroup-delay");
//设置name server 地址
defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876");
//从开始位置消费
defaultMQPushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
//订阅
defaultMQPushConsumer.subscribe("topic-order-delay","product-order-delay");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(message->{
System.out.println("消费时间:"+new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8));
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
defaultMQPushConsumer.start();
}
}
事务消息
如果业务只涉及到一个数据库的写操作,我们只需要保证这一个事物的提交和回滚,这种事务管理叫传统事物或本地事务,如果业务涉及到多个数据库(多个服务)的写操作,我们需要保证多个数据库同时提交或回滚,这种夸多个数据库的事务操作叫分布式事务。
事务消息原理
事务流程中的最大的难点就是如何保证事务消息发送和本地事务的原子性,即:第一步和第二步要么都成功,要么都失败,不能说消息发送成功了,结果用户保存失败了,那么积分服务可能会增加成功,就导致数据不一致。RocketMQ已经帮我们处理好这个问题。它的工作原理如下[理解]:
- 事务发起方,即用户服务会先向broker发送一个prepare“半事务消息”(一个并不完整的消息)到RMQ_SYS_TRANS_HALF_TOPIC的queue中, 该消息对消费者不可见。
- MQ会返回一个ACK确认消息发送成功或者失败
- 消息发送成功,用户服务执行保存用户操作,提交本地事务,并根据本地事务的执行结果来决定半消息的提交状态为提交或者回滚
- 本地事务提交成功,事务发起方即用户服务会向broker再次发起“结束半事务”消息请求,commit或者rollback指令
- broker端收到请求后,首先从RMQ_SYS_TRANS_HALF_TOPIC的queue中查出该消息,设置为完成状态。如果消息状态为提交,则把半消息从RMQ_SYS_TRANS_HALF_TOPIC队列中复制到这个消息原始topic的queue中去(之后这条消息就能被正常消费了);如果消息状态为回滚,则什么也不做。
- Producer发送的半消息结束请求是oneway的,也就是发送后就不管了,只靠这个是无法保证半消息一定被提交的(比如未执行第4步),rocketMq提供了一个兜底方案,这个方案叫消息反查机制,Broker启动时,会启动一个TransactionalMessageCheckService任务,该任务会定时从半消息队列中读出所有超时未完成的半消息,针对每条未完成的消息,Broker会给对应的Producer发送一个消息反查请求,根据反查结果来决定这个半消息是需要提交还是回滚,或者后面继续来反查
- consumer(本例中指积分系统)消费消息,执行本地数据变更,提交本地事务
事务消息实战
- 编写本地事务检查监听TransactionListener ,一是执行本地事务逻辑,二是返回本地事务执行状态
- 发消息时生产者需要设置producer.setTransactionListener 事务监听
1.事务监听器
public class MyTransactionCheckListener implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
//执行业务,保存本地事务
//保存成功
return LocalTransactionState.COMMIT_MESSAGE ; //ROLLBACK_MESSAGE; //未知
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
//这里查询本地事务状态
return LocalTransactionState.COMMIT_MESSAGE;
}
}
2.消息生产者
public class TransationSender {
public static void main(String[] args) throws MQClientException {
//使用事务消息生产者
TransactionMQProducer producer = new TransactionMQProducer("tran-product-group");
producer.setNamesrvAddr("127.0.0.1:9876");
//线程池底层是使用新开线程去发布消息到MQ
ExecutorService excutorService = Executors.newFixedThreadPool(20);
producer.setExecutorService(excutorService);
//指定事务监听器
producer.setTransactionListener(new MyTransactionCheckListener());
//设置事务消息监听
producer.start();
for(int i = 0 ; i < 10 ; i++){
String orderId = UUID.randomUUID().toString();
String tags = "Tag";
Message message = new Message("topic-tran", "tag", orderId, ("下单:"+i).getBytes(CharsetUtil.UTF_8));
//发送事务消息
TransactionSendResult transactionSendResult = producer.sendMessageInTransaction(message, null);
System.out.println(transactionSendResult);
}
producer.shutdown();
}
}
3.消息消费者
public class TransationConsumer {
public static void main(String[] args) throws MQClientException {
//创建消费者
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("trans-consumer-group");
//设置name server 地址
defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876");
//订阅
defaultMQPushConsumer.subscribe("topic-tran", "tag");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(message->{
System.out.println(message+" ; "+new String(message.getBody(), CharsetUtil.UTF_8));
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
defaultMQPushConsumer.start();
}
}