目录
六:SpringBoot中整合Rocket的事务消息
七:Spring Cloud Stream整合RocketMQ
八:RocketMQ的核心概念
1.消息模型
2.消息生产者
3.消息消费者
4.主题(Topic)
5.代理服务器(Broker Server)
6.名字服务(NameServer)
7.拉取式消费
8.推动式消费
9.生产者组
10.消费者组(Consumer Group)
11.集群消费(Clustering)
12.广播消费(Broadcasting)
13.普通顺序消息(Normal Ordered Message)【局部顺序消息】
14.严格顺序消息(Strictly Ordered Message)【全局顺序消息】
15.消息(Message)
16.标签(Tag)
九:消息存储机制
1.消息存储整体架构
CommitLog
ConsumeQueue
IndexFile
2.页缓存与内存映射
3.消息刷盘
同步刷盘
异步刷盘
十:集群核心概念
1.消息的主从复制
同步
异步
同步发送Message和异步发送的区别:
2.负载均衡
Producer的负载均衡
Consumer端的负载均衡
3.消息重试
消息重试机制(如图)
关于重试次数
4.死信队列
5.幂等消息
幂等性问题情况:
十一:RocketMQ最佳实践
1.保证消息顺序消费
2.快速处理积压消息
3.保证消息可靠性投递
4.使用基于缓存中间件的MQ降级方案
生产者的编写:
1.
2.事务监听实现类
@RocketMQTransactionListener(rocketMQTemplateBeanName="rocketMQTemplate") public class MyTrancationListener implements RocketMQLocalTransactionListener { @Override public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) { //1.参数 String destination = (String) arg ; //2.转换为RocketMQ下的Message 这样就可以获取Tag了 org.apache.rocketmq.common.message.Message message = RocketMQUtil.convertToRocketMessage (new StringMessageConverter(), "utf-8", destination, msg); //3.获取到message上的tag内容 String tags = message.getTags() ; //4.进行判断 if (StringUtils.contains(tags,"TagA")) { return RocketMQLocalTransactionState.COMMIT ; } else if (StringUtils.contains(tags,"TagB")) { return RocketMQLocalTransactionState.ROLLBACK ; } else { return RocketMQLocalTransactionState.UNKNOWN ; } } @Override public RocketMQLocalTransactionState checkLocalTransaction(Message message) { org.apache.rocketmq.common.message.Message msg = RocketMQUtil.convertToRocketMessage( new StringMessageConverter(), "utf-8", null, message ); String tags = msg.getTags(); if (StringUtils.contains(tags,"TagC")) { return RocketMQLocalTransactionState.COMMIT ; } else if (StringUtils.contains(tags,"TagD")) { return RocketMQLocalTransactionState.ROLLBACK ; } else { return RocketMQLocalTransactionState.UNKNOWN ; } } }
3.生产者测试发送消息
@Test void testSendMessageInTransaction() throws InterruptedException { String topic = "my-boot-topic" ; String message = "hello-" ; producer.sendMessageInTransaction(topic,message) ; }
消费者的编写同五即可
Spring Cloud Stream 是一个框架,用于构建与共享消息系统连接的高度可扩展的事件驱动微服务
分析:可以在不改变整体逻辑代码的基础上进行更改对消息处理的中间件 !实现代码与使用的中间件之间的解耦操作
该框架提供了一个灵活的编程模型,该模型基于已经建立和熟悉的Spring习惯用法和最佳实践,包括对持久pub/sub语义,消费者和有状
态分区的支持
如图:
Spring Cloud Stream的核心构建块是:
Destination Binders [目标绑定器]:负责提供与外部消息传递系统集成的组件
Destination Bindings :外部消息系统和最终用户提供的应用程序代码(生产者/消费者)之间的桥梁
Message:生产者和消费者用来与目标绑定器进行通信的规范数据结构
好处:
如下图:
分析:
(1) RocketMQ主要是由Producer,Broker,Consumer三部分组成。NameServer起到了一个类似于Zookeeper的注册中心的作用,存放着Broker集群的地址,给生产者,消费者查看使用。
(2) 其中Producer负责生产消息,Consumer负责消费消息,Broker负责存储消息。但Broker在实际存储的时候是存储到Broker中开辟的MessageQueue消息队列中的。
存储的形式为Broker进行监控队列进行轮询存储 。
eg:broker进行监控着,队列序号为0的MessageQueue存储了之后才能存储到序号为1处。这就是轮询,其实就是Producer与Broker集群中的一个Broker之间的负载均衡策略。
(3) Broker集群中的每一个Broker可以存储多个Topic主题的消息,每一个Topic的消息也可以分片存储于不同的Broker。MessageQueue用于存储消息的物理地址,每一个Topic中的消息地址存储于多个Message Queue中。ConsumerGroup由多个Consumer实例构成。
负责生产消息,一般由业务系统负责生产消息。一个消息生产者会把业务应用系统里产生的消息发送到broker服务器。RocketMQ提供多种发送方式:同步发送,异步发送,顺序发送,单向发送。
同步发送:生产者发送一条消息给Broker之后会等待阻塞,只有当Broker给生产者给出一个反馈后 才能停止阻塞 进而继续执行下一项业
务逻辑。存在缺点:吞吐量小,高可用性差 但是安全性较高,适用于短信的发送。
异步发送:生产者发送一条消息给Broker之后不会等待阻塞,会继续执行生产者下面的业务逻辑。但是生产者对应每一个发送的消息都会
生成一个回调函数,当Broker反馈之后,回调函数都会被进行调用。回调函数会进行判断是否发送消息成功。
吞吐量大,高可用性强 但是安全性没有保障
单向发送:生产者发送给Broker一条消息,无需阻塞等待,无需反馈。通常不使用。
顺序发送分为全局顺序发送和局部顺序发送:
全局顺序:Broker中只有一个MessageQueue队列,并且从前往后进行存储消息。
局部顺序:Broker中有多个MessageQueue队列,局部局限对于一个队列来说,是从前往后开始存储Message数据对象的。
总结:
(1) 局部顺序消费是针对于一个队列来说是从前往后消费的。
(2) 局部顺序消费相对于全局顺序消费更加适用于开发
(3) 通常采用局部顺序消费消息数据,增加高可用性
负责消费消息,一般是后台系统负责异步消费。一个消息消费者会从Broker服务器拉取消息,并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费 和 推动式消费
表示一类消息的集合,每一个主题包含若干条消息,每一条消息只能属于一个主题,主题Topic是RocketMQ进行消息订阅的基本单位
消息中转角色,负责存储消息,转发消息。代理服务器在RocketMQ系统中负责接收从生产者发送来的消息并存储,同时为消费者的拉取
请求作准备。代理服务器也存储消息相关的元数据,包括消费者组,消费进度偏移和主题和队列信息等
名称服务充当路由消息的提供者。类似于zookeeper。Broker集群会把自己broker与topic的映射关系保存给到NameServer中。因此生
产者或消费者能够通过NameServer集群服务器查找各个主题Topic相应的Broker IP列表。多个NameServer实例组成集群,但是相互独
立,没有信息交换【即集群中的各个NameServer实例是无状态的】。
Consumer消费的一种类型,应用通常主动调用Consumer的拉消息方法从Broker服务器拉取消息,主动权由应用控制。一旦获取了批量消息,应用就会启动消费过程。
Consumer消费的⼀种类型,该模式下Broker收到数据后会主动推送给消费端,该消费模式⼀般实时性较⾼
同一类Producer的集合,这类Producer发送同一类消息并且发送逻辑一致。
如果发送的是事务消息并且原来的生产者在发送之后崩溃,则Broker服务器会联系同一生产者组的其他生产者实例以提交或回溯消费。
生产者组的作用:
同⼀类Consumer的集合,这类Consumer通常消费同⼀类消息且消费逻辑⼀致。消费者组使得在消息消费⽅⾯,实现负载均衡和容
错的⽬标变得⾮常容易。
要注意的是,消费者组的消费者实例必须订阅完全相同的Topic。
RocketMQ ⽀持两种消息模式:集群消费(Clustering)和⼴播消费(Broadcasting)。
集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
分析:消费者组中消费者订阅完全相同的Topic。消息会以Consumer Group为单位,一个Group分配相同的一份消息,这一份消息被一
个消费者组中消费者所平均分摊消费 !记住一点:消息被消费之后不会消失,它仍然会保存在broker中的Message Queue中。
⼴播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。
分析:意思即是每一个消费者组中每一个消费者都会被分配到一份相同等量的消息 !
普通顺序消费模式下,消费者通过同一个消费队列收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。
严格(全局) 顺序消息模式下,消费者收到的所有消息均是有顺序的。
消息系统所传输信息的物理载体,⽣产和消费数据的最⼩单位,每条消息必须属于⼀个Topic主题。RocketMQ中每个消息拥有唯⼀的
Message ID,且可以携带具有业务标识的Key【该Key即是业务标识 如:商品id】。系统提供了通过Message ID和Key查询消息的功能。
注释:Message ID是RocketMQ给每一个消息分配的唯一ID号
为消息设置的标志,用于同一主题下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务目的在同一主题Topic下设置不
同标签。标签能够有效地保持代码的清晰度和连贯性,并且优化RocketMQ提供的查询系统。消费者可以根据Tag标签实现对不同子Topic
主题的不同消费逻辑,实现更好的扩展性。
消息主体以及元数据的存储主体,存储Producer端写⼊的消息主体内容,消息内容不 是定⻓的。单个⽂件⼤⼩默认1G ,⽂件名⻓度为20
位,左边补零,剩余为起始偏 移量,⽐如00000000000000000000代表了第⼀个⽂件,起始偏移量为0,⽂件⼤⼩ 为1G=1073741824;
当第⼀个⽂件写满了,第⼆个⽂件为00000000001073741824, 起始偏移量为1073741824,以此类推。消息主要是顺序写⼊⽇志⽂
件,当⽂件满 了,写⼊下⼀个⽂件;
消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题Topic的订阅模式,消息消费是针对主题进行的,如
果要进行遍历CommitLog文件中根据Topic进行检索消息是非常低效的。Consumer即可根据ConsumerQueue来查找待消费的消息,其
中,ConsumerQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消
息⼤⼩size和消息Tag的HashCode值。consumequeue⽂件可以看成是基于topic的commitlog索引⽂件,故ConsumerQueue的文件夹
的组织方式如下:topic/queue/file 三层组织结构,具体 存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。
同样consumequeue⽂件采取定⻓设计,每⼀个条⽬共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息⻓度、8字节tag
hashcode,单个⽂件由30W个 条⽬组成,可以像数组⼀样随机访问每⼀个条⽬,每个ConsumeQueue⽂件⼤⼩约5.72M;
IndexFile(索引⽂件)提供了⼀种可以通过key或时间区间来查询消息的⽅法。
Index⽂件的存储位置是:$HOME \store\index${fileName},⽂件名fileName是以创 建时的时间戳命名的,固定的单个IndexFile⽂件
⼤⼩约为400M,⼀个IndexFile可以 保存 2000W个索引,IndexFile的底层存储设计为在⽂件系统中实现HashMap结构, 故rocketmq的
索引⽂件其底层实现为hash索引。
在上⾯的RocketMQ的消息存储整体架构图中可以看出,RocketMQ采⽤的是混合型 的存储结构,即为Broker单个实例下所有的队列共⽤
⼀个⽇志数据⽂件(即为CommitLog)来存储。RocketMQ的混合型存储结构(多个Topic的消息实体内容都存 储于⼀个CommitLog中)针
对Producer和Consumer分别采⽤了数据和索引部分相分 离的存储结构,Producer发送消息⾄Broker端,然后Broker端使⽤同步或者异
步的 ⽅式对消息刷盘持久化,保存⾄CommitLog中。只要消息被刷盘持久化⾄磁盘⽂件CommitLog中,那么Producer发送的消息就不
会丢失。正因为如此,Consumer也就 肯定有机会去消费这条消息。当⽆法拉取到消息后,可以等下⼀次消息拉取,同时 服务端也⽀持
⻓轮询模式,如果⼀个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。
这⾥,RocketMQ的具体做法是,使⽤Broker端的后台服务线程—ReputMessageService不停 地分发请求并异步构建ConsumeQueue
(逻辑消费队列)和IndexFile(索引⽂件) 数据。
⻚缓存(PageCache)是OS对⽂件的缓存,⽤于加速对⽂件的读写。⼀般来说,程序 对⽂件进⾏顺序读写的速度⼏乎接近于内存的读写速
度,主要原因就是由于OS使⽤PageCache机制对读写访问操作进⾏了性能优化,将⼀部分的内存⽤作PageCache。 对于数据的写⼊,OS
会先写⼊⾄Cache内,随后通过异步的⽅式由pdflush内核线程 将Cache内的数据刷盘⾄物理磁盘上。对于数据的读取,如果⼀次读取⽂
件时出现 未命中PageCache的情况,OS从物理磁盘上访问读取⽂件的同时,会顺序对其他相 邻块的数据⽂件进⾏预读取。
在RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取, 在page cache机制的预读取作⽤下,
ConsumeQueue⽂件的读性能⼏乎接近读内 存,即使在有消息堆积情况下也不会影响性能。⽽对于CommitLog消息存储的⽇志 数据⽂
件来说,读取消息内容时候会产⽣较多的随机访问读取,严重影响性能。如 果选择合适的系统IO调度算法,⽐如设置调度算法
为“Deadline”(此时块存储采⽤SSD的话),随机读的性能也会有所提升。另外,RocketMQ主要通过MappedByteBuffer对⽂件进⾏读
写操作。其中,利⽤了NIO中的FileChannel模型将磁盘上的物理⽂件直接映射到⽤户态的内存地址中(这 种Mmap的⽅式减少了传统IO
将磁盘⽂件数据在操作系统内核地址空间的缓冲区和 ⽤户应⽤程序地址空间的缓冲区之间来回进⾏拷⻉的性能开销),将对⽂件的操作
转化为直接对内存地址进⾏操作,从⽽极⼤地提⾼了⽂件的读写效率(正因为需要 使⽤内存映射机制,故RocketMQ的⽂件存储都使⽤定
⻓结构来存储,⽅便⼀次将整 个⽂件映射⾄内存)。
如上图所示,只有在消息真正持久化⾄磁盘后RocketMQ的Broker端才会真正返回给Producer端⼀个成功的ACK响应。同步刷盘对MQ消
息可靠性来说是⼀种不错的保障,但是性能上会有较⼤影响,⼀般适⽤于⾦融业务应⽤该模式较多。
能够充分利⽤OS的PageCache的优势,只要消息写⼊PageCache即可将成功的ACK返 回给Producer端。消息刷盘采⽤后台异步线程提交
的⽅式进⾏,降低了读写延迟, 提⾼了MQ的性能和吞吐量。
生产者无论是同步发送还是异步发送,本质上都会在接收到Message后进行返回一个ack表示确认。但是返回ack的时机不同导致同步和异
步的性能效率差距产生。
Producer同步发送一个Message给到一个broker上的Topic中的某一个MessageQueue对象中进行存储,当主机把Message从主机复制
到从机之后,此时从机才会返回一个ack给生产者Producer
Producer同步发送一个Message给到一个broker上的Topic中的某一个MessageQueue对象中进行存储,当主机接收到生产者Producer
发送的Message后,立刻就会返回一个ack给Producer生产者 表示确认。接着会异步同时执行主机复制一份Message给从机的过程。
(1) 同步发送Message安全性强,能够确保消息完成主从复制,确保消息不会丢失,即使Message丢失,那么不返回ack。Producer生产
者会重新进行发送该Message消息对象。
但是异步发送不可以,发送异步消息时,主机接收到后会直接返回一个ack给Producer,从而异步同时的进行复制Message给到从机,但
是网络是动荡的,所以可能会造成消息丢失,并且Producer接收到ack后,以为Message已经被主从机成功接收,不会再重新发送。
(2) 但是异步发送Message时,吞吐量大,执行效率高,速度快。
RocketMQ中的负载均衡都在客户端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息时
的负载均衡
分析图片流程:
(1) 易知 在一个Broker中会有多个Topic,每一个Topic都会有许多个MessageQueue,每一个MessageQueue中可以存放许多个
Message对象
(2) 会先进行一个负载均衡策略 合理分配Message消息对象给消费者。
(3) Consumer消费者选取一个MessageQueue中的一个Message对象进行消费,但是在还未成功消费时,MessageQueue中的Message
依旧还是存在的,只有当Consumer消费者消费Message成功 进行提交一个offset之后,MessageQueue中的Message对象才会被视为真
正的被消费了 !
(4) 前面三个步骤都是建立在消息成功消费,提交offset的情况下。但是当消息没有成功消费,提交的不是offset而是其他的信息时,此时
MessageQueue中的Message对象会被重试消费,这就是消息重试,确保消息被消费 !
(5) rebalance是什么?当消费者集群中一条消费者服务器宕机或出故障后 ,Consumer集群会进行一个rebalance操作,即是为了进行重
新分配消息Message的消费范围。
幂等性:多次操作造成的结果是一致的。对于非幂等的操作,幂等性如何保证?
(1) 在请求方式中的幂等性的体现
get: 多次get结果是一致的
post:添加,非幂等 分析:多次点击添加表单进行提交,多次添加数据到数据库中,这是非幂等性的
put:修改:幂等,根据id修改
delete:根据id删除,幂等
对于非幂等的请求,我们要在业务中做幂等性保证
(2) 在消息队列中的幂等性体现
消息队列中,很可能一条消息被冗余部署的多个消费者收到,对于非幂等的操作,比如用户的注册,就需要做幂等性保证,否则消息将会
被重复消费。可以将情况概况为以下几种:
生产者重复发送:由于网络抖动,导致生产者没有收到broker的ack而重发消息,实际上broker收到了多条重复的消息,造成消息重复
消费者重复消费:由于网络抖动,消费者没有返回ack给broker,导致消费者重试消费
rebalance时重新分配消费者对消息的消费范围,由于网络抖动,在rebalance重分配时也可能会出现消费者重复消费某条消息
(3) 如何保证幂等性消费
1.mysql插入业务id作为主键,主键是唯一的,所以一次只能插入一条[不太常用,其实就是利用主键的唯一性来进行插入消息数据的]
2.使用redis或zk的分布式锁(主流的方案)
分许:所谓分布式锁其实是形式意义上的锁,实质上是一个模拟的锁的状态,c1消费者进行访问后,定义的lock变量就会变为true 就可以
进行成功消费,但是当c2消费者过来时 lock就会变为false 消费不成功,这就是分布式锁。
(1) 生产者重复发送造成幂等性问题:由于网络抖动,导致生产者没有收到broker的ack而重发消息,实际上broker收到了多条重复的消
息,造成消息重复
(2) 消费者重复消费造成幂等性问题:由于网络抖动,消费者没有返回ack给broker,导致消费者重试消费
(3) rebalance时重新分配造成幂等性问题:
Consumer1已经成功消费消息m1,然后提交offset。但是此时由于网络抖动导致Consumer1死掉,此时触发rebalance重分配的机制,
由于Consumer2没有提交offset,所以会进行消息重试,此时消息m1会被重复消费 保存到数据库db中。
全局有序:消费的所有消息都严格按照发送消息的顺序进行消费
分析:按照每一个Topic中的每一个MessageQueue中的每一条Message消息数据从前往后依次进行消费。先消费完MessageQueue0中
的所有Message数据后才能消费下一个MessageQueue。
局部有序:消费的部分消息都按照发送的顺序进行消费
分析:只要每一个MessageQueue中的Message数据是从前往后进行消费的就行 !不用在意是先消费哪一个MessageQueue。
流程:
(1) 当我们确定完Producer broker1 消费组1以及DB数据库后开始进行工作,但是此时Producer发送的消息数据是海量级别的,这种级别
的Message消息数据,此时的消费组1结构与broker1是解决不了的。
(2) 第一时间想到的解决方案为:在消费者组与DB数据库之间的流程进行优化:1.优化慢查询SQL 2.使用多线程进行处理Message 3.尽量
使用缓存数据库(使用索引) 4.使用分布式
(3) 但是以上的解决方案可能确实有用 但是优化的效率远不及我们需求的效率 !
所以我们需要多进行开辟许多个消费者组,这些消费者组只是做转发作用,以为对于一开始Producer broker1 消费组1以及DB数据库的
整体架构已经固定好了,无法再进行改变,所以只能让新的消费者组转发broker1打过来的 来不及消费的Message,然后转发给新的
broker
当中间件MQ整个服务不可用时,即是整个broker服务器是崩溃时,为了防止服务雪崩,消息Message可以暂时存储于缓存中间件(比如:
Redis)中,等待MQ恢复后,将redis中的数据重新刷进MQ中。