我需要将记录写入文件,其中根据数字键的值将数据写入文件位置(即搜索位置)。例如,如果 key 是 100,我可能会在位置 400 处写入。
记录由数字键和一段数据组成。记录不会很大(几个字节)。但是,可能有很多记录(百万)。
有两种可能的情况:
键是单调递增的。在这种情况下,最好的方法是使用
DataOutputStream
包装BufferedOutputStream
,将缓冲区大小设置为某个数字(例如 64k)以最大化 I/O 吞吐量。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/