背景当有很多数据需要存储这些数据只是想要简单的按行存储和查询不需要进行其他条件搜索此时就可以考虑不需把这些数据存储在数据库而是直接写入文件然后从文件中查询但是正常情况下如果仅仅只是按行写入文件,读取的时候如果不是从第一行开始读java api是不支持直接定位到某行开始查询比如如果想查出第1000行之后的数据此时需要先读取前面999行的数据之后才能读取第1000行的数据可想而知性能好不到哪去。有些人可能会说用RandomAccessFile可以直接跳过一定偏移量定位到999行可是正常情况下,我们并不能知道999行对应的偏移量也就无法知道要跳过多少偏移量直接定位到999行。本文主要是提供了解决上面问题的工具。实现功能1.可以按行写入数据并对写入的每一行创建索引2.基于索引快速定位到行所在位置不需要一行行遍历3.支持对写入数据进行分片。比如写入1W条数据1000条数据一个文件4.支持分页查询5.支持指定行号查询6.支持写部分数据后继续往后添加新数据7.不支持从中间插入数据(后续不会支持因为目前的实现机制从中间插入插入行后面的所有行都得重新构造索引)代码地址g5zhu5896/file-storage · GitHub介绍核心类FileDataWriter用于往文件中写入数据并创建索引使用:String path /file/张三/; Integer totalSize 10000; Integer fileMaxSize1000; String charsetCharsetUtil.UTF_8; Long indexFixedWidthFactor 3l; //如果文件已存在写入会报错 if (!new File(path).exists()) { WriteMode writeMode WriteMode.NOT_ALLOW_REPEATED; try (FileDataWriter fileDataWriter FileDataWriter.builder().withFileMaxSize(fileMaxSize) .withIndexFixedWidthFactor(indexFixedWidthFactor) .withWriteMode(writeMode) .withCharset(charset).build(filePath)) { for (Long i 1L; i totalSize; i) { fileDataWriter.write(i ); } } catch (IOException e) { e.printStackTrace(); } }相关配置fileMaxSize配置一个文件最大存储的行数path文件要存储的路径。可以自定义规则indexFixedWidthFactor: 索引固定宽度因子,用于计算索引文件中索引的固定宽度。配的越大越浪费空间配小了更容易触发调整固定宽度charset配置写文件的编码 writeMode: 写入模式NOT_ALLOW_REPEATED: 不允许重复写入,当写入文件夹已存在数据会直接抛异常OVERRIDE覆盖模式当写入文件夹已存在数据会删除文件重新写入APPEND追加模式当写入文件夹已存在数据会往最新的数据文件的最后一行继续写(APEEND 模式在存在数据文件时下 charset、fileMaxSize、indexFixedWidthFactor会无效)FileDataPageReader使用:String path /file/张三/; //如果文件不存在则无法读取 if (!new File(path).exists()) { FileDataPageReader fileDataPageReader FileDataPageReader.builder().build(path); //分页查询 //从第二页开始查询 Integer page 2; //一页查20条 Integer pageSize20 ListString strings fileDataPageReader.readPage(page, pageSize); //将行内容转成Interger,此处也可以把json转成对象 ListInteger strings fileDataPageReader.readPage(page, pageSize,item-{ return new Integer(item); }); //指定行数查询 //从第5行开始查 Integer startIndex 5; //共查100条 Integer size 100; ListString strings fileDataPageReader.read(startIndex, size); //将行内容转成Interger,此处也可以把json转成对象 ListInteger strings fileDataPageReader.read(startIndex, size,item-{ return new Integer(item); }); //获取总行数 Integer totalSizefileDataPageReader.getTotalSize(); }文件介绍(目前文件后缀只能写死可以通过改Constants类调整生成后缀).dat文件数据文件按行写入。会有多个文件如下图所示.idx行索引文件会有多个存储每一行对应的文件偏移量通过该文件快速定位数据文件的行偏移量如下图所示cfg配置文件记录当前文件的一些配置信息如下图所示相关配置fileMaxSize配置每一个文件最大存储的行数totalSize总行数charset配置写文件的编码columnIndexWidthMap:行索引动态固定宽度map,主要用于减少索引文件的大小。有序{1:4,1852:8}表示1-1851行的间距是41852及之后的行的间距是8下面配置主要是WriteMode.APPEND用的newCurrentRow: 最新数据文件最后写入行行数1后的行数newIndexFixedWidth最新文件最后一行写入后的索引固定宽度newDataFilePointer最新数据文件的最后写入行写入后末尾的偏移量newIndexFilePointer最新索引文件的最写入行写入后末尾的偏移量相关依赖dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId version1.18.22/version scopeprovided/scope /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.7.19/version /dependency dependency groupIdcom.alibaba.fastjson2/groupId artifactIdfastjson2/artifactId version2.0.26/version /dependency dependency groupIdcom.google.guava/groupId artifactIdguava/artifactId version19.0/version /dependency注意本文代码仅基于Test.main中的demo进行测试验证过所以依然可能存在bug,但个人感觉测试的demo已经挺多的了.核心逻辑定位数据行主要通过RandomAccessFile.seek快速定位到行偏移量然后通过索引文件获取具体的行偏移量。这没什么好说的从索引文件中获取数据行偏移量索引文件需要存储数据行中的偏移量但是索引文件中包含很多行我们想知道数据行的偏移量在索引文件中的哪个位置也无法直接定位。所以为了快速从索引文件中定位到数据行的偏移量采用了索引固定宽度的做法。具体如下假设indexFixedWidthFactor3step1:写入第1行数据,假设占用行偏移量9,宽度为1,则当前索引固定宽度为curFixedWidth1indexFixedWidthFactor4写入索引文件第1行的偏移量为0000(记录的是写入前的偏移量那才是行开始位置)代码在FileDataWriter.writestep2:写入第2行数据,假设占用10个行偏移量,加上第1行的偏移量后为19,写入第2行偏移量0009以此类推第3行为0019step3:直到写入103行假设写入后的行偏移量的行偏移量为10009,由于10009宽度为5大于curFixedWidth,于是重新计算当前索引固定宽度为curFixedWidth5indexFixedWidthFactor8,所以第104行的行偏移量是00010009。以此类推step4假设150行宽度依然小于8然后写到151行当前文件写满需要写下一个文件,则会从头如step1一样计算索引固定宽度curFixedWidth1indexFixedWidthFactor4。step5假设只写到179行,最后的curFixedWidth4此时会将配置信息写入 cfg 文件其中包括columnIndexWidthMap{1:4, 104:8, 151:4}(代码在FileDataWriter.close)step6由于是每一行的行偏移量是固定宽度读取的时候就可以基于columnIndexWidthMap和要读取的行号计算出行数据文件的行偏移量在索引文件中的偏移量直接读取出数据文件的行偏移量(代码在FileDataPageReader.computeSeek)(如假设要从120行开始读取则行偏移量为4*104-1)8*(120-104),从此处往后读取8位及120行的数据文件行偏移量可以优化的点现在写索引文件是按字符写入的比如0001,写入就是四个字符其实可以根据数字大写根据实际长度写入short,int,long 类型数据这样可以减少索引文件的体积。