rockMq实现原理:
RocketMQ由NameServer注册中心集群、Producer生产者集群、Consumer消费者集群和若干Broker(RocketMQ进程)组成。
1、Broker在启动的时候去向所有的NameServer注册,并保持长连接,每30s发送一次心跳。
2、Producer在发送消息的时候从NameServer获取Broker服务器地址,根据负载均衡算法选择一台服务器来发送消息。
3、Conusmer消费消息的时候同样从NameServer获取Broker地址,然后主动拉取消息来消费。
4.RocketMQ主要的存储文件包括commitlog文件、consumequeue文件、indexfile文件
Broker在收到消息之后,会把消息保存到commitlog的文件当中,而同时在分布式的存储当中,每个broker都会保存一部分topic的数据,同时,每个topic对应的messagequeue下都会生成consumequeue文件用于保存commitlog的物理位置偏移量offset,indexfile中会保存key和offset的对应关系。
由于同一个topic的消息并不是连续的存储在commitlog中,消费者如果直接从commitlog获取消息效率非常低,所以通过consumequeue保存commitlog中消息的偏移量的物理地址,这样消费者在消费的时候先从consumequeue中根据偏移量定位到具体的commitlog物理文件,然后根据一定的规则(offset和文件大小取模)在commitlog中快速定位。
5.Master和Slave之间同步数据
消息在master和slave之间的同步是根据raft协议来进行的:
- 在broker收到消息后,会被标记为uncommitted状态
- 然后会把消息发送给所有的slave
- slave在收到消息之后返回ack响应给master
- master在收到超过半数的ack之后,把消息标记为committed
- 发送committed消息给所有slave,slave也修改状态为committed
6、RocketMQ为什么速度快?
是因为使用了顺序存储、Page Cache和异步刷盘。
- 我们在写入commitlog的时候是顺序写入的,这样比随机写入的性能就会提高很多
- 写入commitlog的时候并不是直接写入磁盘,而是先写入操作系统的PageCache
- 最后由操作系统异步将缓存中的数据刷到磁盘
CommitLog 文件是存放消息数据的地方,所有的消息都将存入到 CommitLog 文件中。生产者将消息发送到 RocketMQ 的 Broker 后,Broker 服务器会将「消息顺序写入到 CommitLog 文件中」,这也就是 RocketMQ 高性能的原因,因为我们知道磁盘顺序写特别快,RocketMQ 充分利用了这一点,极大的提高消息写入效率。
Consumequeue 文件可以看作是索引文件,类似于 MySQL 中的二级索引」。在存放了同一主题下的所有消息,消费者消费的时候只需要去对应的 Consumequeue 组中取消息即可。Consumequeue 文件不会存储消息的全量信息,了解 MySQL 索引的话,应该好理解这里,具体存储的字段,我在上图已经标注。这样做可以带来以下两个好处:
- 由于 Consumequeue 文件内容小,可以尽可能的保证 Consumequeue 文件全部读入到内存,提高消费效率。
- Consumequeue 文件也是会持久化的,不存全量信息可以节约磁盘空间。
「IndexFile」 是 RocketMQ 为消息订阅构建的索引文件,用来提高根据主题与消息队列检索消息的速度
消息数据刷盘(默认异步刷盘)
「异步刷盘方式」:消息写入到内存的 PAGECACHE中,就立刻给客户端返回写操作成功,当 PAGECACHE 中的消息积累到一定的量时,触发一次写操作,将 PAGECACHE 中的消息写入到磁盘中。这种方式「吞吐量大,性能高,但是 PAGECACHE 中的数据可能丢失,不能保证数据绝对的安全」。
「同步刷盘方式」:消息写入内存的 PAGECACHE 后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。这种方式「可以保证数据绝对安全,但是吞吐量不大」。
7、什么是事务、半事务消息?怎么实现的?
事务消息就是MQ提供的类似XA的分布式事务能力,通过事务消息可以达到分布式事务的最终一致性。
半事务消息就是MQ收到了生产者的消息,但是没有收到二次确认,不能投递的消息。
实现原理如下:
- 生产者先发送一条半事务消息到MQ
- MQ收到消息后返回ack(Acknowledgement)确认
- 生产者开始执行本地事务
- 如果事务执行成功发送commit到MQ,失败发送rollback
- 如果MQ长时间未收到生产者的二次确认commit或者rollback,MQ对生产者发起消息回查
- 生产者查询事务执行最终状态
- 根据查询事务状态再次提交二次确认
最终,如果MQ收到二次确认commit,就可以把消息投递给消费者,反之如果是rollback,消息会保存下来并且在3天后被删除。
CAP理论:
- 一致性(Consistency)
- 可用性(Availability)
- 分区容错性(Partition tolerance)
延时消息
rocketmq 先将不同延时等级的消息存入内部对应延时队列中,然后不断的从延时队列中拉取消息判断是否到期,然后进行投递到对应的topic中。
通过固定延时等级的方式,同一个队列中的消息都是相同的延时等级,不需要对消息进行排序,只需要按顺序拉取消息判断是否可以投递就行了。但也限制了延时时间。
另外,因为只要延时消息存入延时队列中,就会写入commitlog文件中,然后rocketmq的高可用(同步复制或异步复制)就会将消息复制到slave中,从而保证延时消息的可靠性。
消息重试
生产端重试:如果由于网络抖动等原因,Producer程序向Broker发送消息时没有成功,即发送端没有收到Broker的ACK,导致最终Consumer无法消费消息,此时RocketMQ会自动进行重试。在5s内没有发送成功,则重试3次。
消费端重试:
异常重试:由于Consumer端逻辑出现了异常,导致返回了RECONSUME_LATER状态,那么Broker就会在一段时间后尝试重试。
超时重试:如果Consumer端处理时间过长,或者由于某些原因线程挂起,导致迟迟没有返回消费状态,Broker就会认为Consumer消费超时,此时会发起超时重试。
注:只有在消息模式为MessageModel.CLUSTERING集群模式时,Broker才会自动进行重试,广播消息是不会重试的。
死信
首先判断消息当前重试次数是否大于等于 16,或者消息延迟级别是否小于 0
只要满足上述的任意一个条件,设置新的 topic(死信 topic)为:%DLQ%+consumerGroup
进行前置属性的添加
将死信消息投递到上述步骤 2 建立的死信 topic 对应的死信队列中并落盘,使消息持久化。
消费者和生产者的重试还是有区别的,主要有两点
1、默认重试次数:Product默认是2次,而Consumer默认是16次。
2、重试时间间隔:Product是立刻重试,而Consumer是有一定时间间隔的。它照1S,5S,10S,30S,1M,2M····2H进行重试。Product在异步情况重试失效,而对于Consumer在广播情况下重试失效。
重试队列
RocketMQ 规定,以下三种情况统一按照消费失败处理并会发起重试。
- 业务消费方返回 ConsumeConcurrentlyStatus.RECONSUME_LATER
- 业务消费方返回null
- 业务消费方主动/被动抛出异常
rocketmq会放到重试队列。这个重试TOPIC的名字是%RETRY%+consumergroup的名字.
RocketMQ不解决消息重复问题,RocketMQ不解决消息重复问题,RocketMQ不解决消息重复问题,重要的事情说三遍。
消息持久化
RocketMQ 在持久化的设计上,采取的是「消息顺序写、随机读的策略」,利用磁盘顺序写的速度,让磁盘的写速度不会成为系统的瓶颈。并且采用 MMPP 这种“零拷贝”技术,提高消息存盘和网络发送的速度。极力满足 RocketMQ 的高性能、高可靠要求。
在 RocketMQ 持久化机制中,涉及到了三个角色:
- 「CommitLog」:消息真正的存储文件,所有消息都存储在 CommitLog 文件中。
- 「ConsumeQueue」:消息消费逻辑队列,类似数据库的索引文件。
- 「IndexFile」:消息索引文件,主要存储消息 Key 与 offset 对应关系,提升消息检索速度。
消费消息的顺序性
rocketMq支持局部顺序消费,但不支持全局,换句话说针对Topic中的每个queue是可以按照FIFO进行消费。
要保证一个订单有关的消息顺序消费,有两点需要注意,一是将订单有关的消息发送到相关Topic中同一个queue里,二是消费者按照先进先出的原则进行消费。
消息发送到指定queue中
在消息发送时,需指定对应的MessageQueueSelector,此时我们只需通过订单号与queue进行关联,代码如下。send中的参数arg即为select中的arg,将订单号作为参数传入,同一订单号的相关消息则可以保证在同一queue中。
顺序消费
如果使用MessageListenerConcurrently的话,必须保证是单线程才能顺序消费,但生产环境下,我们一般 都是多线程的形成,这样则需要使用MessageListenerOrderly。
produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息
注意:是把把消息发到同一个队列(queue),不是同一个topic,默认情况下一个topic包括4个queue
MessageListenerConcurrently使用代码如下
MessageListenerOrderly使用代码如下