浅谈面向文件编程(文件读写)的重要性

>>强大,10k+点赞的 SpringBoot 后台管理系统竟然出了详细教程!
在我印象中,似乎很少有关于文件操作的面试题,而大多数面试题都围绕着多线程、网络编程、RPC、数据库,但其实掌握文件操作也同等重要。只是我们很少会碰到需要操作文件的需求,毕竟百分之九十的工作都是依靠操作数据库、网络通信完成,而存储都被各类关系型数据库、分布式数据库、缓存、搜索引擎、甚至云存储替代了。
虽然偶尔我们也需要实现文件上传的接口,但文件上传一般都会选择存储到云端,顶多就临时转存一下,相信很多人会选择直接百度拷贝一份代码完成文件存储了事,甚至于都不关心是如何实现的。而多数的表格导出操作也都依赖一些现成的框架,以致于面向文件编程的重要性被弱化了。
如果我们去研究一些框架的底层源码,我们就能发现掌握文件操作其实也很重要。以RocketMQ为例,RocketMQ的消息存储并没有借用数据库,也没有借用其它第三方框架,仅仅是用文件存储。我很好奇,为什么没有面试题问RocketMQ的消息存储实现原理。
我自己也开发过一些组件/框架/中间件,但由于文件操作这块知识太欠缺,首先想到的都是依赖一些第三方存储中间件/库实现,如RedisMysqlLevelDB,这直接提升了框架的使用成本。所以我也一直知道掌握文件操作的重要性。
有时候我也在想,为什么部署Kafka(旧版本)要部署一个Zookeeper,而部署Zookeeper的作用只是用于管理节点、消费者、实现Leader选举。部署Zookeeper为了保证Zookeeper的可用性又要部署几个节点,这无疑增加了Kafka的使用成本。所以当我看到Alibaba Sentinel实现集群限流功能提供嵌入式模式时就很理解为什么要同时提供嵌入式部署和独立部署两种模式。
我去年开始着手自研一个分布式延迟调度中间件,其实核心功能早就实现了,也以嵌入式部署的方式在项目中支撑业务功能。但为了去掉依赖Redis实现存储功能、第三方框架实现RPC功能、广播机制实现Leader选举功能,我才决定重新写一个。因此我用Raft共识算法+LevelDBKey-Value存储库)替代Redis实现存储、基于Netty自己封装RPC框架、基于Raft算法替代广播实现Leader选举。这直接就降低了这款自研中间件的使用成本。而在实现Raft算法的日记Appender时,我又遇到了同样的槛,但这次我选择跨过去。
阿里开源的众多项目中,除RocketMQ的消息存储使用文件存储外,Sentinel存储资源指标数据统计也是使用文件存储,这两个框架在实现存储上都使用了同一种设计思想,即数据文件+索引文件。我在自研分布式延迟调度中间件中就借鉴了RocketMQSentinel中的文件存储索引设计,数据文件存储日记,而索引文件存储日记ID与日记在数据文件中的物理偏移量。
Sentinel按精确到秒的时间戳存储索引,和时间戳是有序增长的,而且时间戳是long类型占8个字节,根据单个指标文件的最大大小,物理偏移量也正好可以是long类型,因此每个索引占16个字节。资源指标数据可能由于某段时间没有请求或者应用重启导致某些时间戳没有记录,但至少时间戳是单调递增的,因此我们只需要采用简单的折半查找就能快速定位到索引。由于Sentinel资源指标数据收集不需要考虑高并发,这样的设计足以满足需求。
RocketMQ需要提供可以通过key或时间区间来查询消息的功能,因此RocketMQ的索引存储实现相对Sentinel较难。单个索引文件固定的文件大小约为400M,一个索引文件可以保存2000W个索引,索引文件的底层存储设计相当于是在文件系统中实现HashMap结构,每个文件头存储了此文件存储的消息的最小时间戳和最大时间戳,这用于实现按时间区间搜索消息记录。消息keyhash值则作为索引项存放在索引文件中的物理偏移量,当然,还要加上文件头的大小,以及乘以单项索引占用的字节数。
你或许觉得,RocketMQSentinel实现索引难在算法,的确,算法是灵魂。但软件的强大依然需要依赖硬件的支持,你是否考虑到,如何跳转到文件中的某个位置读取指定字节的数据,又如何改写文件中指定位置的数据?如何考虑并发读写问题,如何调优性能?是实时写文件呢,还是参考MysqlBinlogRedisRDB刷盘策略?文件的NIO如何理解、如何使用MappedByteBuffer提升性能以及原理是什么?
解读这几个问题是我学习文件读写的目的,也体现了掌握面向文件编程的重要性。
最后,虽然为了提升工作效率以及降低犯错率我们并不需要重复造轮子,但重复造轮子无疑是提升自身能力最高效的学习方法。写出来的东西并不一定就要用,但为了写出一款好的开源作品,我会不断的尝试。

原文始发于微信公众号(Java艺术):浅谈面向文件编程(文件读写)的重要性