时间过的好快,一周就这样悄悄的过去了,又到了老王说书时间。今天我们接着上几期的话题,扯扯Message Queue的那些事儿。
上周我们聊了MQ的Recv Module,他负责接收调用方发来的请求数据,并将数据包解析出来。接下来,我们就看看这些被解析出来,要被转发的数据,是如何做序列化和反序列化的。这一部分的内容,就是MQ最核心的东西。
我们来看看整个提交的流程。
数据从各个地方汇聚到Recv Module,这个模块将数据解析以后,得到一个个的data。然后通过本地函数的调用,将这些data提交给Data Serialize Module的Data Handler。
为了保证数据不丢失,必须考虑常见的异常情况,比如:程序因为bug crash、机器死机等。最简单的方法,就是接下来的所有操作都是同步的,而非异步。如果是异步操作的话,必然有数据放入内存,从而导致数据的丢失。而如果是同步的话,即使这次挂掉,调用方还会尝试重发这次的数据。所以,接下来的工作就是对提交的data按时间排序、分配id、写入磁盘。既然是串行化的操作,就有一个非常严格的要求,就是所有的调用必须非常高效,否则,我们的MQ就没有存在的意义了。那接下来,我们就看看每一个工作单元是如何来工作的。
Data Handler
Data Handler要做的工作其实也非常简单,就是将这些大量的数据进行排序操作,并且按照时间先后顺序将这些数据进行串行化,使得data是按时间有序组织的。
在具体实现上,也比较简单,就是利用加锁的方法,先来先得。没有抢到锁的,就在锁队列里等待唤起。
Id Allocator
这个单元的作用,就是给每个排序好的数据做个编号。那为什么要做编号呢?不编号行不行?
如果单从提交的流程上来看,确实不需要编号。直接将数据写入磁盘就可以了。但是,当我们走到发送流程上的时候,就会出问题了。现在我该把哪个数据发送给第三方呢?他们怎么告诉MQ,已经成功的接收到哪些数据了呢?总不至于要把数据在磁盘上的偏移量暴露给第三方吧。
所以,最好的方式,就是给每个数据打一个ID,这样在发送给第三方的时候,就可以通过这个唯一ID来通讯和确认。
在具体实现上,可以用一个本地文件来记录当前分配的最大ID值。每次分配一个ID以后,将本地文件值加一,然后重新写入。因为这个文件只有几个字节大小,对他的操作,大体都在操作系统的系统缓存里,几乎不耗时。所以,这个单元的操作是非常迅速的。
Data Writer
数据写入单元的作用,即是将带有ID的data序列化到磁盘中。如果不考虑读取效率的问题,我们只需要写入数据本身。但是,当我们要读取数据的时候,就麻烦了。比如,这个时候我要给某一个第三方程序发送ID=96的数据,怎么办呢?MQ只有从磁盘里,一个个的挨着数,一直数到96号data,才能发送。这相当于是一个O(N)的算法,效率极其低下。
所以,考虑到还有读出的操作,我们需要给写入的数据加一个索引。能够根据ID快速的定位到data所在的位置。而且,考虑到有磁盘操作,我们的索引文件最好尽可能的小,这样在读取的时候,能够尽可能利用系统的缓存,而不是每次都去读取磁盘。
之前有朋友问老王,MQ写入能有多快?他有磁盘操作,不是跟其他的后端一样嘛,都是一个慢操作,这样他有什么存在的意义?
这个问题提的非常好。MQ最关键的部分就是这个数据写入的部分。他实现的好不好,直接决定了MQ最终的运行效率。所以,这一部分,老王准备再下一次来详细的分享。今天我们只是把数据序列化这个模块的功能和流程梳理清楚。
Data Reader
数据读取单元,则是根据请求ID从之前序列化好的磁盘数据中,读取对应的data。因为之前已经建立了索引,所以这个读取操作,相对变得高效(具体的索引内容,也是在下一次的分享中详细来描述,大家就姑且认为他很高效吧,哈哈~)。
好了,到现在为止,老王已经把MQ的核心部件的流程拆解的差不多了,大家觉得如何?下周,我们就具体聊聊磁盘存储、读取的算法和数据结构,以及文件系统如何来组织~