1、为什么用mq

优势

主要有3个:

应用解耦(降低微服务之间的关联)、

异步提速(微服务拿到mq消息后同时工作)、

削峰填谷(可以消息堆积)

劣势

系统可用性降低(MQ一旦宕机整个系统不可用)

复杂度提高(需要解决系统消息一致性、重复消费...)

一致性问题(不同系统拿到mq中的消息后,部分系统处理失败怎么办)

2、rocketmq集群工作流程

由上图可以看出,rocketMQ集群=消息服务器集群+命名服务器集群,其中消息服务器集群=生产者集群+broker集群+消费者集群。

命名服务器集群(nameserver cluster)

● 命名服务器集群是管理生产者、broker、消费者的纽带,哪个生产者/broker/消费者可用都是通过命名服务器得知其信息,所以生产者/broker/消费者都需要定时发送心跳给命名服务器

● 命名服务器与生产者的关系:命名服务器记录有许多broker的ip地址,每个生产者发送消息到broker前都需要先去命名服务器获取某个broker的ip,然后再发送消息到broker

● 命名服务器和消息者的关系:命名服务器记录有许多broker的ip地址,消费者想监听broker中的消息,需要先去命名服务器获取某个broker的ip,然后再监听broker中的消息

生产者集群(producer cluster)

● 每个生产者部署在不同的IP上形成了集群

● 生产者的消息=topic+tag,topic用来区分消息类型,一种topic类型的消息可以分布在多个不同的broker中,同类型的消息就用tag区分,如我们系统里的佣金宝的topic是"topic-yjb",然后佣金宝下面可以划分多个tag

消费者集群(consumer cluster)

● 每个消费者部署在不同的IP上形成了集群

● 消费者获取某个broker中的消息理论上有两种方法:

○ pull拉取模式:消费者开启线程定时访问broker,如有消息存在则拉取,缺点是太消耗消费者的资源了,不管有没有消息都会去访问broker

○ push推送模式:消费者起一个监听器监听broker(与broker建立一个长链接),若broker中有消息,则broker会自动推送消息给消费者,一般用这种。其中push模式的底层也是通过消费者主动拉取的方式来实现的,只不过它的名字叫push而已,意思是Broker尽可能实时的推送消息给消费者,和pull模式相比,push模式都帮我们封装了底层,而pull模式就要自己写代码去手动拉取消息,所以pull模式更像拉取,而封装好的push更像是推送。

3、消息类型

同步/异步/单向消息

同步:发送消息是按顺序发送

异步:发送消息是异步的,生产者发送完消息就干其他事情,消费者稍后会在生产者的回调函数中返回消费结果【new SendCallback(){} 】,及时性差

单项消息:生产者只管发出去,并不接收返回值

批量消息

特点:

● 同一批消息的topic应该相同;

● 消息内容大小=(topic+body+其他key/value属性+日志固定20字节)<4M

● 不是延时消息

tag过滤消息

消费者指定特定的tag,则只接收该tag的消息

sql过滤消息

消费者支持类似于sql查询语法那样的消息过滤

//生产者
String msg="hello,小明同学";
Message message=new Message("topic",msg.getBytes("UTF-8"));
message.putUserProperty("name","xiaoming");
message.putUserProperty("age","27");
SendResult sendResult1 =defaultMQProducer.send(message); //消费者用sql语法过滤出age>26岁的消息,即只接受age>26岁的消息
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
defaultMQPushConsumer.subscribe("topic", MessageSelector.bySql("age>26"));

4、消息的特殊处理

顺序消息

场景:在某些业务系统中,一些业务流程处理的顺序必须是按顺序的,比如客户下单:创建订单 -> 付款 -> 推送消息 -> 订单完成,在并发环境下,不可能只有一个客户下单,当多个客户下单时,他们的这四种消息有可能是混乱的。

解决方案:rocketmq默认每个topic在broker中都会有四个队列存放该类数据,队列是FIFO性质的,我们可以利用队列去按序存放这些消息以达到按序消费的目的。

生产者主要代码:

DefaultMQProducer defaultMQProducer=new DefaultMQProducer("group1");
defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");
try {
defaultMQProducer.start();
List<Order> list=new ArrayList<>();
//模拟业务流程乱序提交:单个订单消息有序,多个订单间消息无序
Order order01=new Order(0,"创建订单");
Order order11=new Order(1,"创建订单");
Order order02=new Order(0,"付款"); Order order03=new Order(0,"推送");
Order order21=new Order(2,"创建订单");
Order order12=new Order(1,"付款"); Order order04=new Order(0,"完成");
Order order13=new Order(1,"推送");
Order order22=new Order(2,"付款"); Order order14=new Order(1,"完成");
Order order23=new Order(2,"推送");
Order order24=new Order(2,"完成"); list.addAll(new ArrayList<Order>(Arrays.asList(order01,order11,order02,order03,order21,order12,order13,order22,order23,order04,order14,order24)));
for(Order order:list){
Message message=new Message("topic-order",order.toString().getBytes());
/*
*每个topic默认创建4个队列,defaultMQProducer可以通过MessageQueueSelector的select方法设置
*当前Message发送到"topic-order"的哪个队列:通过订单的唯一属性值,如orderId,对topic中的queue队列数取模,
*这样同一个订单的不同消息就会被按序放进同一个queue中
*/
SendResult sendResult=defaultMQProducer.send(message, new MessageQueueSelector() { @Override
public MessageQueue select(List<MessageQueue> queueList, Message msg, Object o) {
System.out.println("队列数:"+queueList.size());
//获取队列下标
int size=order.getOrderId()%queueList.size();
//计算该message放在"topic-order"哪个队列中
MessageQueue mq=queueList.get(size);
return mq;
}
},null);
System.out.println(sendResult);
} } catch (Exception e) {
e.printStackTrace();
}

消费者主要代码:

//使用MessageListenerConcurrently则多个线程服务一个队列,而MessageListenerOrderly是一个线程服务一个队列(topic默认四个队列就是四个线程)
defaultMQPushConsumer.registerMessageListener(new MessageListenerOrderly() {
@Override
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
System.out.println("线程:"+Thread.currentThread().getName()+",队列:"+consumeOrderlyContext.getMessageQueue().getQueueId()+",该队列消息数量:"+list.size());
for(MessageExt messageExt:list){
System.out.println(new String(messageExt.getBody())); }
return ConsumeOrderlyStatus.SUCCESS;
}
});

执行结果:

可以看出一个线程服务一个队列,将同类业务的消息都推送到同一个队列中,是可以实现消息的顺序发送的.

事务消息

1. 为什么要用事务消息?

还是以用户下单为例,用户在producer中创建订单(但未提交事务到mysql),然后把下单消息发送给broker(即MQ服务器),MQ服务器再把该消息发给所有订阅了该类topic的消费者,可能出现如下情况:

(1)producer成功进行了数据库操作(即提交事务到mysql),且MQ服务器接收消息成功,然后被消费者消费 -->皆大欢喜

(2)producer成功进行了数据库操作(即提交事务到mysql),但发到MQ服务器失败,进而消费者不能消费该类消息 -->不正常

(3)producer进行数据库操作的时候发生了意外导致数据库操作失败(即提交事务到mysql),但发到MQ服务器成功,进而消费者会去消费该类消息 -->不正常

上面第2、3种情况都是不正常的,解决办法就是引入事务消息,事务消息的过程如下:

第一阶段producer先发送一条"half"型消息到MQ服务器,MQ服务器收到后随即返回一个发送成功标识 ->

producer进行数据库操作执行事务,执行成功则发送二次确认(Commit或Rollback)消息给服务器 ->

MQ服务器收到Commit则将第一阶段的"half"型消息标记为可投递,消费者若订阅了该topic则能收到该消息;MQ服务器收到Rollback则删除第一阶段的消息,消费者将接收不到该消息,就当什么事也没发生过 ->

MQ服务器会有一个事务补偿机制:若服务器很久都没有收到producer返回的二次确认commit/rollback,则会主动去调用producer的接口进行回查,然后producer再去数据库中查看事务是否执行成功 ,如成功/失败,则发送commit/rollback给MQ服务器,然后后面的操作同上一步

2. 事务消息和正常消息的区别

(1)事务消息有三种状态:commit状态、回滚状态、中间状态(producer发送了half型消息但未发送commit给到服务器,即未对);commit状态的消息等价于正常消息(可以被消费者感知),但后两种状态的消息对于消费者是不可见的

(2)事务消息仅与生产者有关,仅当事务消息处于commit状态时与正常消息一样可以被消费者感知

3. 代码实现事务消息

生产者主要代码:

//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer transactionMQProducer=new TransactionMQProducer("group1");
transactionMQProducer.setNamesrvAddr("127.0.0.1:9876");
try {
//添加事务监听
transactionMQProducer.setTransactionListener(new TransactionListener(){ //事务消息过程中包括正常事务(数据库操作)、事务补偿,正常事务在该方法执行
@Override
@Transactional
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
try{ //模拟数据库操作事务正常提交:insert delete...
Long orderId= Long.valueOf(new String(message.getBody()));
insert(orderId);
System.out.println("本地数据库事务提交成功!");
//本地执行完数据库操作后(正常事务),事务消息有三种状态:COMMIT_MESSAGE/ROLLBACK_MESSAGE/UNKNOW
return LocalTransactionState.COMMIT_MESSAGE; }catch(Exception ex){ //模拟数据库操作事务提交失败:insert delete... System.out.println("本地数据库事务提交失败,事务消息rollback:"+ex);
//本地执行完数据库操作后(正常事务),事务消息有三种状态:COMMIT_MESSAGE/ROLLBACK_MESSAGE/UNKNOW
return LocalTransactionState.ROLLBACK_MESSAGE; } //业务成功后,也可以直接返回UNKNOW,避免极端的情况下数据库并没有保存成功
//return LocalTransactionState.UNKNOW; } //事务补偿在该方法执行:若executeLocalTransaction返回UNKNOW或长时间没有返回消息给服务器
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) { System.out.println("执行事务补偿...");
Long orderId= Long.valueOf(new String(messageExt.getBody()));
if(null!=selectOne(orderId)){
return LocalTransactionState.COMMIT_MESSAGE;
}else{
//若事务不成功可以返回UNKNOW而不是ROLLBACK_MESSAGE,因为服务器默认回查15次后都是UNKNOW,则会自动回滚haLf型消息
return LocalTransactionState.UNKNOW;
} }
}); transactionMQProducer.start();
Order order=new Order(0,"创建订单");
Message message=new Message("topic-transaction",String.valueOf(order.getOrderId()).getBytes());
SendResult sendResult=transactionMQProducer.sendMessageInTransaction(message,null);
System.out.println("sendResult:"+sendResult);

由事务消息的整个流程可知,先执行executeLocalTransaction,再打印发送结果:

如果executeLocalTransaction返回UNKNOW,则服务器不断尝试事务补偿:

消费者主要代码:

DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876");
defaultMQPushConsumer.subscribe("topic-transaction", "*");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
list.forEach(msg->{
System.out.println("收到消息:"+new String(msg.getBody()));
});
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
defaultMQPushConsumer.start();

执行结果如下:

rocketmq消息及流程的更多相关文章

  1. RocketMQ之九:RocketMQ消息发送流程解读

    在讨论这个问题之前,我们先看一下Client的整体架构. Producer与Consumer类体系 从下图可以看出以下几点:(1)Producer与Consumer的共同逻辑,封装在MQClientI ...

  2. RocketMQ消息发送流程和高可用设计

    (源码阅读先看主线 再看支线 先点到为止 后面再详细分解) 高可用的设计就是:当producer发送消息到broker上,broker却宕机,那下一次发送如何避免发送到这个broker上,就是采用La ...

  3. RocketMQ源码 — 八、 RocketMQ消息重试

    RocketMQ的消息重试包含了producer发送消息的重试和consumer消息消费的重试. producer发送消息重试 producer在发送消息的时候如果发送失败了,RocketMQ会自动重 ...

  4. 源码分析RocketMQ消息轨迹

    目录 1.发送消息轨迹流程 1.1 DefaultMQProducer构造函数 1.2 SendMessageTraceHookImpl钩子函数 1.3 TraceDispatcher实现原理 2. ...

  5. RocketMQ之十:RocketMQ消息接收源码

    1. 简介 1.1.接收消息 RebalanceService:均衡消息队列服务,负责通过MQClientInstance分配当前 Consumer 可消费的消息队列( MessageQueue ). ...

  6. RocketMQ(消息重发、重复消费、事务、消息模式)

    分布式开放消息系统(RocketMQ)的原理与实践 RocketMQ基础:https://github.com/apache/rocketmq/tree/rocketmq-all-4.5.1/docs ...

  7. 源码分析 Kafka 消息发送流程(文末附流程图)

    温馨提示:本文基于 Kafka 2.2.1 版本.本文主要是以源码的手段一步一步探究消息发送流程,如果对源码不感兴趣,可以直接跳到文末查看消息发送流程图与消息发送本地缓存存储结构. 从上文 初识 Ka ...

  8. 源码分析 Kafka 消息发送流程

    Futuresend(ProducerRecord<K, V> record) Futuresend(ProducerRecord<K, V> record, Callback ...

  9. RocketMQ消息丢失解决方案:同步刷盘+手动提交

    前言 之前我们一起了解了使用RocketMQ事务消息解决生产者发送消息时消息丢失的问题,但使用了事务消息后消息就一定不会丢失了吗,肯定是不能保证的. 因为虽然我们解决了生产者发送消息时候的消息丢失问题 ...

随机推荐

  1. python方面

    (113条消息) re.sub()用法的详细介绍_jackandsnow的博客-CSDN博客_re sub Python slice() 函数 | 菜鸟教程 (runoob.com) (111条消息) ...

  2. 在Spring框架中如何更有效地使用JDBC?

    使用SpringJDBC 框架,资源管理和错误处理的代价都会被减轻.所以开发者只需写statements 和 queries从数据存取数据,JDBC也可以在Spring框架提供的模板类的帮助下更有效地 ...

  3. Oracle入门基础(六)一一子查询

    SQL> --查询工资比SCOTT高的员工信息 SQL> --1. SCOTT的工资 SQL> select sal from emp where ename='SCOTT'; SA ...

  4. 你将如何使用 thread dump?你将如何分析 Thread dump?

    新建状态(New) 用 new 语句创建的线程处于新建状态,此时它和其他 Java 对象一样,仅仅在堆区 中被分配了内存. 就绪状态(Runnable) 当一个线程对象创建后,其他线程调用它的 sta ...

  5. Spring源码分析笔记--AOP

    核心类&方法 BeanDefinition Bean的定义信息,封装bean的基本信息,从中可以获取类名.是否是单例.是否被注入到其他bean中.是否懒加载.bean依赖的bean的名称等. ...

  6. Java动态代理和CGLib代理

    本文参考 在上一篇"Netty + Spring + ZooKeeper搭建轻量级RPC框架"文章中涉及到了Java动态代理和CGLib代理,在这篇文章中对这两种代理方式做详解 下 ...

  7. ctfhub web 前置技能(请求方式、302跳转、Cookie)

    第一题:请求方式 打开环境分析题目发现当前请求方式为GET 查看源码发现需要将请求方式改为CTFHUB就可以 使用bp抓包 发送到repeater模块修改请求方式 即可得到flag 第二题:302跳转 ...

  8. C/C++头文件以及避免头文件包含造成的重定义方法

    C 头文件 头文件是扩展名为 .h 的文件,包含了 C 函数声明和宏定义,被多个源文件中引用共享.有两种类型的头文件:程序员编写的头文件和编译器自带的头文件. 在程序中要使用头文件,需要使用 C 预处 ...

  9. element el-tree、el-table组件加载数据前闪现 暂无数据 清除

    相信很多人在使用element  el-tree.el-table组件加载数据前会显示一个" 暂无数据 ",体验很不友好,有没有办法处理不显示呢?答案是:有的.废话不多说直接上代码 ...

  10. canvas绘图API详解

    canvas绘图API详解 1.context的状态 矩阵变换属性 当前剪辑区域 context的其他状态属性: strokeStyle, fillStyle, globalAlpha, lineWi ...