java文件按行写入数据后并创建行索引及查询

背景

        当有很多数据需要存储,这些数据只是想要简单的按行存储和查询,不需要进行其他条件搜索,此时就可以考虑不需把这些数据存储在数据库,而是直接写入文件,然后从文件中查询

        但是正常情况下,如果仅仅只是按行写入文件,读取的时候如果不是从第一行开始读,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 fileMaxSize=1000;
String charset=CharsetUtil.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 pageSize=20
    List<String> strings = fileDataPageReader.readPage(page, pageSize);
    //将行内容转成Interger,此处也可以把json转成对象
    List<Integer> strings = fileDataPageReader.readPage(page, pageSize,item->{
        return new Integer(item);
    });

    //指定行数查询
    //从第5行开始查
    Integer startIndex = 5;
    //共查100条
    Integer size = 100;
    List<String> strings = fileDataPageReader.read(startIndex, size);
    //将行内容转成Interger,此处也可以把json转成对象
    List<Integer> strings = fileDataPageReader.read(startIndex, size,item->{
        return new Integer(item);
    });

    //获取总行数
    Integer totalSize=fileDataPageReader.getTotalSize();
    
}

文件介绍

(目前文件后缀只能写死,可以通过改 Constants 类调整生成后缀)

.dat文件

数据文件,按行写入。会有多个,文件如下图所示

.idx

行索引文件,会有多个,存储每一行对应的文件偏移量,通过该文件快速定位数据文件的行偏移量,如下图所示

cfg

配置文件,记录当前文件的一些配置信息,如下图所示

相关配置

fileMaxSize:配置每一个文件最大存储的行数
totalSize:总行数
charset:配置写文件的编码
columnIndexWidthMap:行索引动态固定宽度map,主要用于减少索引文件的大小。有序,{1:4,1852:8}表示1-1851行的间距是4,1852及之后的行的间距是8

下面配置主要是 WriteMode.APPEND 用的
newCurrentRow: 最新数据文件最后写入行行数+1后的行数
newIndexFixedWidth:最新文件最后一行写入后的索引固定宽度
newDataFilePointer:最新数据文件的最后写入行写入后末尾的偏移量
newIndexFilePointer:最新索引文件的最写入行写入后末尾的偏移量

相关依赖

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.22</version>
	<scope>provided</scope>
</dependency>
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.7.19</version>
</dependency>
<dependency>
	<groupId>com.alibaba.fastjson2</groupId>
	<artifactId>fastjson2</artifactId>
	<version>2.0.26</version>
</dependency>
<dependency>
	<groupId>com.google.guava</groupId>
	<artifactId>guava</artifactId>
	<version>19.0</version>
</dependency>

注意

        本文代码仅基于 Test.main 中的 demo 进行测试验证过,所以依然可能存在 bug ,但个人感觉测试的 demo 已经挺多的了.

核心逻辑 

定位数据行

主要通过 RandomAccessFile.seek 快速定位到行偏移量,然后通过索引文件获取具体的行偏移量。这没什么好说的,

从索引文件中获取数据行偏移量

        索引文件需要存储数据行中的偏移量,但是索引文件中包含很多行,我们想知道数据行的偏移量在索引文件中的哪个位置,也无法直接定位。

        所以为了快速从索引文件中定位到数据行的偏移量,采用了索引固定宽度的做法。具体如下

        假设 indexFixedWidthFactor=3

        step1: 写入第1行数据,假设占用行偏移量9,宽度为1,则当前索引固定宽度为curFixedWidth=1+indexFixedWidthFactor=4,写入索引文件第1行的偏移量为0000(记录的是写入前的偏移量,那才是行开始位置)代码在 FileDataWriter.write)

        step2: 写入第2行数据,假设占用10个行偏移量,加上第1行的偏移量后为19,写入第2行偏移量0009,以此类推,第3行为0019

        step3: 直到写入103行,假设写入后的行偏移量的行偏移量为10009,由于10009宽度为5,大于 curFixedWidth ,于是重新计算当前索引固定宽度为 curFixedWidth=5+indexFixedWidthFactor=8 ,所以第104行的行偏移量是00010009。以此类推

        step4:假设150行宽度依然小于8,然后写到151行,当前文件写满,需要写下一个文件,则会从头如 step1 一样计算索引固定宽度curFixedWidth=1+indexFixedWidthFactor=4。

        step5:假设只写到179行,最后的 curFixedWidth=4 ,此时会将配置信息写入 cfg 文件,其中包括 columnIndexWidthMap={1:4, 104:8, 151:4} (代码在 FileDataWriter.close)

        step6:,由于是每一行的行偏移量是固定宽度,读取的时候就可以基于 columnIndexWidthMap 和要读取的行号,计算出行数据文件的行偏移量在索引文件中的偏移量,直接读取出数据文件的行偏移量(代码在FileDataPageReader.computeSeek)(如假设要从120行开始读取,则行偏移量为4*(104-1)+8*(120-104),从此处往后读取8位及120行的数据文件行偏移量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值