txt texto separado por palabra manejo leer guardar escribir datos como comas archivos archivo abrir java file-io fileoutputstream randomaccessfile

texto - Java: cómo escribir de manera eficiente un archivo secuencial con agujeros ocasionales en él



leer y escribir archivos en java (5)

Tengo el requisito de escribir registros en un archivo donde los datos se escriben en una ubicación de archivo (es decir, buscar posición) según el valor de una tecla numérica. Por ejemplo, si la clave es 100, podría escribir en la posición 400.

Los registros consisten en la clave numérica y un dato. El registro no será muy grande (unos pocos bytes). Sin embargo, puede haber muchos registros (millones).

Hay dos escenarios posibles:

  1. Las llaves son monótonamente crecientes. En este caso, el mejor enfoque es escribir usando un DataOutputStream envolviendo un BufferedOutputStream , configurando el tamaño del búfer en algún número (por ejemplo, 64k) para maximizar el rendimiento de E / S.

  2. Las claves van en aumento pero con posibles grandes huecos. En este caso, utilizar un OutputStream requeriría que se escribieran ceros en los huecos del archivo. Para evitar esto, un RandomAccessFile sería mejor, ya que podría buscar sobre las brechas, ahorrando espacio si es posible buscar sobre un bloque completo. El inconveniente es que, por lo que sé, RandomAccessFile no almacena en búfer, por lo que este método será lento para las claves secuenciales.

Sin embargo, la situación probable es que el archivo es un poco de ambos. Hay secuencias de claves monótonamente crecientes. Hay algunas claves con pequeños espacios entre sí y otras con espacios muy grandes.

Lo que busco es una solución que ofrezca lo mejor de ambos mundos. Puede ser que cambie entre los dos modos de E / S si se detecta un espacio entre las teclas. Sin embargo, sería mejor si hay una clase estándar de Java que puede hacer ambas cosas. He visto FileImageOutputStream , pero no estoy seguro de cómo funciona esto.

Tenga en cuenta que no estoy buscando ejemplos de código (aunque eso sería útil para demostrar soluciones complejas), solo una estrategia general. Sería bueno saber los tamaños de búfer de tamaños óptimos para datos secuenciales y en qué punto (tamaño de brecha) necesita cambiar de una estrategia secuencial a una estrategia de acceso aleatorio.

EDITAR:

Para que se acepte una respuesta, me gustaría tener cierta seguridad de que la solución propuesta maneja ambas cosas, no solo que podría hacerlo. Esto requeriría:

  • Confirmación de que el modo secuencial está en búfer.
  • Confirmación de que el modo de acceso aleatorio deja agujeros en el archivo.

Además, la solución debe ser eficiente en memoria, ya que podría haber muchos de estos archivos abiertos simultáneamente.

Editar 2

Los archivos podrían estar en un NAS. Esto no es por diseño, sino simplemente por el reconocimiento de que en un entorno empresarial, esta arquitectura se usa mucho y la solución probablemente debería manejarlo (quizás no de manera óptima) y no impedir su uso. AFAIK, esto no debería afectar a una solución basada en write() y lseek() , pero podría invalidar algunas soluciones más esotéricas.


Dices millones de registros de unos pocos bytes. Asumamos que son 10 millones de 10 bytes, lo que significa que el archivo a escribir tendrá alrededor de 100 mb. En nuestros tiempos, eso no es mucho.

Simplemente crearía un Mapa en el que se almacenaron todos los pares clave-valor. Luego escribiría una función que serializa el contenido del mapa a byte[] . Y luego simplemente Files.write() los bytes al disco. Luego reemplace el archivo antiguo con el nuevo archivo. O, mejor aún, mueva primero el archivo antiguo, luego mueva el nuevo.


Edición / advertencia: existen posibles errores con esta solución, ya que utiliza en gran medida MappedByteBuffer , y no está claro cómo / cuándo se liberan los recursos correspondientes. Consulte esta sección de preguntas y respuestas y JDK-4724038: (fs) Agregue el método Unmap a MappedByteBuffer .

Dicho esto, por favor vea también el final de este post.

Haría exactamente lo que Nim sugirió :

envuelva esto en una clase que se asigne en "bloques" y luego mueva el bloque a lo largo de la escritura. El algoritmo para esto es bastante sencillo. Solo elija un tamaño de bloque que tenga sentido para los datos que está escribiendo.

De hecho, hice exactamente eso hace años y simplemente desenterré el código, así es como se hace (reducido al mínimo para una demostración, con un solo método para escribir datos):

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); } }

Aquí hay un fragmento de prueba:

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));

Y cómo se ve el archivo de salida:

$ 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

Espero no haber arruinado todo eliminando los bits innecesarios y cambiando el nombre ... Al menos el cálculo de desplazamiento parece correcto (0x3e0 + 8 = 1000, y 0x02faf080 = 50000000).

Número de bloques (columna izquierda) ocupados por el archivo y otro archivo no disperso del mismo tamaño:

$ 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

La cantidad de bloques (y la "dispersión" real) dependerán del sistema operativo y del sistema de archivos, lo anterior estaba en Debian Buster, ext4: los archivos dispersos no son compatibles con HFS + para macOS, y en Windows requieren que el programa haga algo específico. No sé lo suficiente, pero eso no parece fácil ni siquiera factible desde Java, no estoy seguro.

No tengo números nuevos, pero en ese momento esta "técnica de MappedByteBuffer " era muy rápida y, como puede ver arriba, deja agujeros en el archivo.
Tendrá que adaptar WINDOW_SIZE a algo que tenga sentido para usted, agregue todos los métodos de writeThingy que necesite, tal vez writeBytes , lo que más le convenga. Además, en este estado, el archivo crecerá según sea necesario, pero en WINDOW_SIZE de WINDOW_SIZE , que también es posible que necesite adaptar.

A menos que haya una muy buena razón para no hacerlo, probablemente sea mejor mantenerlo simple con este mecanismo único, en lugar de mantener un sistema complejo de modo dual.

Acerca de la fragilidad y el consumo de memoria, he ejecutado la prueba de estrés a continuación en Linux sin ningún problema durante una hora, en una máquina con 800 GB de RAM y en otra VM muy modesta con 1 G de RAM. El sistema se ve perfectamente en buen estado, el proceso java no usa ninguna cantidad significativa de memoria de pila.

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); }

Así que sí, eso es empírico, tal vez solo funcione correctamente en sistemas Linux recientes, tal vez solo sea suerte con esa carga de trabajo en particular ... pero estoy empezando a pensar que es una solución válida en algunos sistemas y cargas de trabajo, puede ser útil.


He cambiado de opinión sobre esto. Debe utilizar MappedByteBuffer . El sistema operativo lo localiza como parte del subsistema de memoria virtual, que satisface sus requisitos de almacenamiento en búfer; es tan rápido como una escritura en la memoria al escribir; y está sujeto al comportamiento del sistema operativo cuando se escriben archivos con agujeros, lo que satisface ese requisito.


Mi primer esfuerzo en esto sería simplemente usar RandomAccessFile ingenuamente y ver si es lo suficientemente rápido. Realmente me sorprendería si fuera lento, aunque Java no lo amortiguará, la implementación del sistema de archivos sí lo hará.

Si realmente hay problemas de rendimiento, mi próximo esfuerzo sería envolver el RandomAccessFile en una fachada de almacenamiento en búfer, con la lógica de escritura siguiendo las líneas de (java-ish pseudocode):

void write(record, location) { if(location != lastLocation + recordLength) { flushBufferToRandomAccessFile(); ) addToBuffer(record); flushBufferToRandomAccessFileIfFull(); lastLocation = location; }

El búfer sería un byte[] . La ganancia potencial aquí es que está haciendo menos randomAccessFile.write(buffer, 0, longLength) lugar de randomAccessFile.write(record, 0, shortLength) .

Puede ordenar esto un poco encapsulando toda la información necesaria acerca de un bloque en Buffer en una clase de Buffer : bytes, ubicación de inicio, ubicación de finalización. También deberá vaciar el búfer para archivar en un método close() ).

Es decir, está recolectando bloques de registros en la memoria del montón, vaciando a RandomAccessFile :

  • cuando alcances el tamaño de tu búfer,
  • cuando una ubicación de registro no es contigua con el bloque actual en búfer
  • despues del ultimo disco

Aprecio que no desee perder memoria, pero independientemente de si está en el montón o en otro lugar, la memoria es memoria y no puede tener un búfer sin ella. Con esta solución, puede ajustar el tamaño de su búfer, e incluso si es suficiente para dos registros, podría reducir a la mitad el número de escrituras.

Si quieres ser fanático sobre el uso de la memoria, estás usando el lenguaje incorrecto.

Si eso no fuera lo suficientemente rápido, consideraría mover las escrituras a otro hilo. Así que escriba sus registros en una cola y deje que el hilo de escritura de archivos se consuma de la cola. Esto no hará que la escritura del archivo sea más rápida en sí misma, pero significa que el consumidor puede ponerse al día con un trabajo pendiente mientras el productor está haciendo un trabajo diferente, por lo que su utilidad depende de si el productor tiene otro trabajo que hacer.


Supongo que cuando sus claves se incrementen secuencialmente durante un tiempo, luego se creará un espacio vacío, no habrá otra clave que se agregue a la secuencia "finalizada". Si esto es correcto entonces sometería la siguiente solución

Mientras sus claves sigan aumentando de manera secuencial, continúe trabajando con su primer enfoque:

escriba usando un DataOutputStream envolviendo un BufferedOutputStream , configurando el tamaño del búfer en algún número (por ejemplo, 64k) para maximizar el rendimiento de E / S.

escribe tus datos en un archivo temporal. Una vez que se produce la brecha, comience a escribir en el siguiente archivo temporal y guarde el registro de sus archivos temporales. De esta manera obtendrá un archivo por secuencia de registros sin espacios. Una vez que haya terminado de procesar los datos de su archivo principal, entonces tenga un método separado que concatine inteligentemente sus archivos temporales en un archivo final. Esta sería una tarea fácil, ya que sabe que cada archivo temporal no tiene espacios vacíos.