一、概述
由前文可知,RocketMQ有几个非常重要的概念:
- broker 服务端,负责存储、收发消息
- producer 客户端1,负责产生消息
- consumer 客服端2,负责消费消息
既然是消息队列,那消息的存储的重要程度不言而喻,本节我们聚焦broker服务端,看下消息在broker端是如何存储的,它的落盘策略是怎样的,又是如何保证高效
另:后文的RocketMQ都是基于版本4.9.3
二、写入流程
RocketMQ的普通单消息写入流程如下
简单可以分为三大块:
- 写入前准备
- 加锁后消息写入
- 消息落盘及集群同步
2.1 准备
其实消息的写入准备工作也比较好理解,主要是消息状态的检查以及各类存储状态的检查,可以参看上图中的流程
根据上图,在准备阶段前,RocketMQ会判断操作系统的Page Cache是否繁忙,他是怎么做到的呢?其实Java本身没有提供接口或函数来查看Page Cache的状态,但如果磁盘带宽已经打满,在Page Cache要将数据刷disk时,很有可能便陷入了阻塞,导致Page Cache资源紧张。而当我们的程序又有新的消息要写入Page Cache时,反向阻塞写入请求,我们说这时Page Cache就产生了回压,也就是Page Cache相当繁忙,请求已经不能及时处理了。RocketMQ判断Page Cache是否繁忙的条件也很简单,就是监控某个请求加锁后,写入是否超过1秒,如果超时的话,新的请求会快速失败
2.2 消息协议
RocketMQ有一套相对复杂的消息协议编码,大部分协议中的内容都是在加锁前拼接生成
大部分消息协议项都是定长字段,变长字段如下:
- 1、born inet 产生消息的producer的IP信息 ipv4占用4byte,ipv6占16byte
- 2、broker inet 接收消息的broker的IP信息 ipv4占用4byte,ipv6占16byte
- 3、msg content 消息内容 变长字段(1-21亿)byte
- 4、topic content 消息内容 变长字段(1-127)byte
- 5、properties content 属性内容 变长字段(0-32767)byte
2.3 加锁
此处rmq提供了2种加锁方式
- 1、基于AQS的ReentrantLock (默认方式)
- 2、基于CAS的自旋锁,加锁不成功的话,会无限重试
无论采用哪种策略,都是独占锁,即同一时刻只允许一个线程加锁成功。具体采用哪种方式,可通过配置修改。
两种加锁适用不同的场景,方式1在高并发场景下,能保持平稳的系统性能,但在低并发下表现一般;而方式二正好相反,在高并发场景下,因为采用自旋,会浪费大量的cpu,但在低并发时,却可以获得很高的性能。
所以官方文档中,为了提高性能,建议用户在同步刷盘的时候采用独占锁,异步刷盘的时候采用自旋锁。这个是根据加锁时间长短决定的
2.4 锁内操作
上文提到,写入消息的锁是独占锁,也就意味着同一时刻,只能有一个线程进入,我们看一下锁内都做了哪些操作
- 1、拿到或创建文件操作对象MappedFile此处涉及点较多,我们在文件写入大节详细展开
- 2、二次整理要落盘的消息格式
- 之前已经整理过消息协议了,为什么此处还要进行二次整理?因为之前一些消息协议在没有加锁的时候,还无法确定。主要是以下三项内容:
- a、queueOffset 队列偏移量,此值需要最终返回,且需要保证严格递增,所以需要在锁内进行
- b、physicalOffset 物理偏移量,也就是全局文件的位置,注:此位置是全局文件的偏移量,不是当前文件的偏移量,所以其值可能会大于1G
- c、storeTimestamp 存储时间戳,此处在锁内进行,主要是为了保证消息投递的时间严格保序
- 之前已经整理过消息协议了,为什么此处还要进行二次整理?因为之前一些消息协议在没有加锁的时候,还无法确定。主要是以下三项内容:
- 3、记录写入信息
- 记录当前文件写入情况:比如已写入字节数、存储时间等
三、文件开辟及写入
3.1 文件开辟
文件的开辟是异步进行,有独立的线程专门负责开辟文件。我们可以先看下文