Java - 如何有效地编写一个偶尔有漏洞的顺序文件

标签 java file-io fileoutputstream randomaccessfile

我需要将记录写入文件,其中根据数字键的值将数据写入文件位置(即搜索位置)。例如,如果 key 是 100,我可能会在位置 400 处写入。

记录由数字键和一段数据组成。记录不会很大(几个字节)。但是,可能有很多记录(百万)。

有两种可能的情况:

  1. 键是单调递增的。在这种情况下,最好的方法是使用 DataOutputStream 包装 BufferedOutputStream,将缓冲区大小设置为某个数字(例如 64k)以最大化 I/O 吞吐量。

  2. key 在增加,但可能存在较大差距。在这种情况下,使用 OutputStream 需要在文件的间隙中写入零。为避免这种情况,RandomAccessFile 会更好,因为它可以搜索间隙,如果可以搜索整个 block 则可以节省空间。缺点是,据我所知,RandomAccessFile 不缓冲,因此对于顺序键,此方法会很慢。

但是,可能的情况是文件是两者兼而有之。存在单调递增的键序列。有些键之间的间隙很小,有些键之间的间隙很大。

我正在寻找的是一种兼具两全其美的解决方案。如果检测到键之间的间隙,可能是我在两种 I/O 模式之间切换。但是,如果有一个标准的 Java 类可以完成这两件事,那就更好了。我看过 FileImageOutputStream,但我不确定它是如何工作的。

请注意,我不是在寻找代码示例(尽管这有助于演示复杂的解决方案),只是一个通用策略。最好了解顺序数据的最佳大小缓冲区大小以及您需要在什么点(间隙大小)从顺序策略切换到随机访问策略。

编辑:

为了让答案被接受,我希望得到一些保证,即所提议的解决方案可以同时处理这两个问题,而不仅仅是它可能会处理。这需要:

  • 确认顺序模式已缓冲。
  • 确认随机访问模式在文件中留下漏洞。

此外,该解决方案需要内存高效,因为可能会同时打开许多此类文件。

编辑 2

文件可能在 NAS 上。这不是设计使然,而是简单地认识到在企业环境中,这种架构被大量使用,解决方案应该可以处理它(也许不是最佳的)而不是阻止它的使用。 AFAIK,这不应该影响基于 write()lseek() 的解决方案,但可能会使一些更深奥的解决方案无效。

最佳答案

编辑/警告:此解决方案存在潜在问题,因为它大量使用 MappedByteBuffer,并且不清楚如何/何时释放相应的资源。参见 this Q&A & JDK-4724038 : (fs) Add unmap method to MappedByteBuffer .

话虽这么说,但也请看这篇文章的结尾


我会做什么 Nim suggested :

wrap this in a class which maps in "blocks" and then moves the block along as you are writing .. The algorithm for this is fairly straightforward.. Just pick a block size that makes sense for the data you are writing..

事实上,我几年前就这么做了,只是挖出了代码,它是这样的(为了演示而剥离到最低限度,只有一种方法来写入数据):

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Path;

public class SlidingFileWriterThingy {

    private static final long WINDOW_SIZE = 8*1024*1024L;
    private final RandomAccessFile file;
    private final FileChannel channel;
    private MappedByteBuffer buffer;
    private long ioOffset;
    private long mapOffset;

    public SlidingFileWriterThingy(Path path) throws IOException {
        file = new RandomAccessFile(path.toFile(), "rw");
        channel = file.getChannel();
        remap(0);
    }

    public void close() throws IOException {
        file.close();
    }

    public void seek(long offset) {
        ioOffset = offset;
    }

    public void writeBytes(byte[] data) throws IOException {
        if (data.length > WINDOW_SIZE) {
            throw new IOException("Data chunk too big, length=" + data.length + ", max=" + WINDOW_SIZE);
        }
        boolean dataChunkWontFit = ioOffset < mapOffset || ioOffset + data.length > mapOffset + WINDOW_SIZE;
        if (dataChunkWontFit) {
            remap(ioOffset);
        }
        int offsetWithinBuffer = (int)(ioOffset - mapOffset);
        buffer.position(offsetWithinBuffer);
        buffer.put(data, 0, data.length);
    }

    private void remap(long offset) throws IOException {
        mapOffset = offset;
        buffer = channel.map(FileChannel.MapMode.READ_WRITE, mapOffset, WINDOW_SIZE);
    }

}

这是一个测试片段:

SlidingFileWriterThingy t = new SlidingFileWriterThingy(Paths.get("/tmp/hey.txt"));
t.writeBytes("Hello world\n".getBytes(StandardCharsets.UTF_8));
t.seek(1000);
t.writeBytes("Are we there yet?\n".getBytes(StandardCharsets.UTF_8));
t.seek(50_000_000);
t.writeBytes("No but seriously?\n".getBytes(StandardCharsets.UTF_8));

以及输出文件的样子:

$ hexdump -C /tmp/hey.txt
00000000  48 65 6c 6c 6f 20 77 6f  72 6c 64 0a 00 00 00 00  |Hello world.....|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
000003e0  00 00 00 00 00 00 00 00  41 72 65 20 77 65 20 74  |........Are we t|
000003f0  68 65 72 65 20 79 65 74  3f 0a 00 00 00 00 00 00  |here yet?.......|
00000400  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
02faf080  4e 6f 20 62 75 74 20 73  65 72 69 6f 75 73 6c 79  |No but seriously|
02faf090  3f 0a 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |?...............|
02faf0a0  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
*
037af080

我希望我没有通过删除不必要的位和重命名来破坏一切...至少偏移计算看起来是正确的(0x3e0 + 8 = 1000,和 0x02faf080 = 50000000)。

文件占用的 block 数(左列),和另一个相同大小的非稀疏文件:

$ head -c 58388608 /dev/zero > /tmp/not_sparse.txt
$ ls -ls /tmp/*.txt
    8 -rw-r--r-- 1 nug nug 58388608 Jul 19 00:50 /tmp/hey.txt
57024 -rw-r--r-- 1 nug nug 58388608 Jul 19 00:58 /tmp/not_sparse.txt

block 数(和实际的“稀疏性”)将取决于操作系统和文件系统,上面是在 Debian Buster 和 ext4 上——HFS+ for macOS 不支持稀疏文件,在 Windows 上它们需要程序做一些事情具体我不太了解,但是这似乎并不容易,甚至不能从 Java 中实现,不确定。

我没有新的数字,但当时这种“滑动-MappedByteBuffer 技术”非常快,正如您在上面看到的那样,它确实在文件中留下了漏洞。
您需要将 WINDOW_SIZE 调整为对您有意义的内容,添加您需要的所有 writeThingy 方法,也许通过包装 writeBytes,无论适合你。此外,在此状态下,它会根据需要增大文件,但会增大 WINDOW_SIZE block ,您可能还需要对其进行调整。

除非有充分的理由不这样做,否则最好使用这种单一机制保持简单,而不是维护复杂的双模式系统。


关于脆弱性和内存消耗,我在一台 800GB RAM 的机器和另一台 1G RAM 的非常适中的 VM 上,在 Linux 上运行了一个小时没有任何问题的压力测试。系统看起来非常健康,java 进程不使用任何大量的堆内存。

    String path = "/tmp/data.txt";
    SlidingFileWriterThingy w = new SlidingFileWriterThingy(Paths.get(path));
    final long MAX = 5_000_000_000L;
    while (true) {
        long offset = 0;
        while (offset < MAX) {
            offset += Math.pow(Math.random(), 4) * 100_000_000;
            if (offset > MAX/5 && offset < 2*MAX/5 || offset > 3*MAX/5 && offset < 4*MAX/5) {
                // Keep 2 big "empty" bands in the sparse file
                continue;
            }
            w.seek(offset);
            w.writeBytes(("---" + new Date() + "---").getBytes(StandardCharsets.UTF_8));
        }
        w.seek(0);
        System.out.println("---");
        Scanner output = new Scanner(new ProcessBuilder("sh", "-c", "ls -ls " + path + "; free")
                .redirectErrorStream(true).start().getInputStream());
        while (output.hasNextLine()) {
            System.out.println(output.nextLine());
        }
        Runtime r = Runtime.getRuntime();
        long memoryUsage = (100 * (r.totalMemory() - r.freeMemory())) / r.totalMemory();
        System.out.println("Mem usage: " + memoryUsage + "%");
        Thread.sleep(1000);
    }

所以是的,这是凭经验得出的,也许它只在最近的 Linux 系统上才能正常工作,也许这只是特定工作负载的运气......但我开始认为它在一些系统上是一个有效的解决方案和工作负载,它可能很有用。

关于Java - 如何有效地编写一个偶尔有漏洞的顺序文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44647589/

相关文章:

android - 如何使用 Intent.ACTION_CREATE_DOCUMENT 编写文件

java - IllegalStateException:系统服务在 onCreate() 之前对 Activity 不可用

java - @OneToMany 映射列表大小限制

java - 从加速度传感器写入文件 - android

android - 为什么保存位图需要这么长时间?

java - 在Java中删除文件

java - 想要在 primefaces 中以水平方式显示错误消息

使用 htmlUnit 的基于 Javascript 的动态内容

c# - 如何减少写入磁盘的旧应用程序和实时读取它的应用程序 (.NET) 上的文件 IO

c - 在读取未知行长度的文件时确定 EOF