Cómo manejar adecuadamente dos hilos actualizando la misma fila en una base de datos

Tengo un hilo llamado T1 para leer un archivo plano y analizarlo. Necesito crear un nuevo hilo llamado T2 para analizar parte de este archivo y luego este hilo T2 deberá actualizar el estado de la entidad original, que también está siendo analizado y actualizado por el hilo original T1 . ¿Cómo puedo manejar esto? ¿situación?

Recibo un archivo plano con los siguientes registros de muestra:

 AAAA BBBB AACC BBCC AADD BBDD 

Primero, este archivo se guarda en la base de datos en estado Received . Ahora todos los registros que comienzan con BB o con AA deben procesarse en un subproceso separado. Una vez que se analiza con éxito, ambos subprocesos intentarán actualizar el estado de este objeto de archivo en una base de datos a Parsed . En algunos casos, obtengo staleObjectException . Edición: Y el trabajo realizado por cualquier hilo antes de la excepción se pierde. Estamos utilizando el locking optimista. ¿Cuál es la mejor manera de evitar este problema?

¿Posibles excepciones de hibernación cuando dos subprocesos actualizan el mismo objeto?

La publicación anterior ayuda a entender parte de ella, pero no ayuda a resolver mi problema.

Parte 1 – Tu problema – la forma en que lo veo.

La razón principal por la que recibe esta excepción es que está utilizando Hibernate con un locking posiblemente optimista . Básicamente, esto le indica que el subproceso T1 o el subproceso T2 ya han actualizado el estado a PARSED y ahora el otro subproceso contiene la versión antigua de la fila con una versión más pequeña que la de la base de datos e intenta actualizar el estado a PARSED también. .

La gran pregunta aquí es ” ¿Están los dos hilos tratando de preservar los mismos datos ?”. Si la respuesta a esta pregunta es sí, incluso si la última actualización se realiza correctamente, no debería haber ningún problema, ya que eventualmente están actualizando la fila al mismo estado. En ese caso, realmente no necesita un locking optimista porque sus datos, en cualquier caso, estarán sincronizados.

El problema principal surge si después de que el estado se establezca en RECIEVED si los dos subprocesos T1 y T2 realmente dependen uno del otro al restablecer el estado siguiente. En ese caso, debe asegurarse de que si T1 se ha ejecutado primero (o viceversa) T2 necesita actualizar los datos de la fila actualizada y volver a aplicar sus cambios en función de los cambios ya introducidos por T1. En este caso la solución es la siguiente. Si encuentra staleObjectException, básicamente necesita actualizar sus datos de la base de datos y reiniciar su operación.

Análisis de la Parte 2 en el enlace publicado ¿ Posibles excepciones de hibernación cuando dos subprocesos actualizan el mismo Objeto? Enfoque 1 , este es más o menos el último en actualizar la situación de victorias . Más o menos evita el locking optimista (el conteo de versiones). En caso de que no tenga una dependencia de T1 a T2 o retroceda para establecer el estado PARSED. Esto debe ser bueno.

**** Aproach 2 ** Bloqueo optimista ** Esto es lo que tiene ahora. La solución es actualizar los datos y reiniciar su operación.

Aproach 3 Lock de DB de nivel de fila La solución aquí es más o menos la misma que para el enfoque 2 con la pequeña corrección que tiene el locking pesimista. La principal diferencia es que, en este caso, puede ser un locking de LECTURA y es posible que ni siquiera pueda leer los datos de la base de datos para actualizarlos si se trata de LEER PESSIMISTIC.

Aproach 4 aplicación de sincronización Hay muchas formas diferentes de hacer la sincronización. Un ejemplo sería organizar realmente todas sus actualizaciones en una cola de JB o JMS (si desea que sea persistente) y enviar todas las actualizaciones desde un solo hilo. Para visualizar un poco, T1 y T2 pondrán elementos en la cola y habrá un solo T3 operaciones de lectura de hilos y empujándolos al servidor de la base de datos.

Si utiliza la sincronización a nivel de la aplicación, debe tener en cuenta que no todas las estructuras se pueden distribuir en una implementación de varios servidores.

Bueno, no puedo pensar en otra cosa por ahora 🙂

No estoy seguro de entender la pregunta, pero parece que constituiría un error lógico para un subproceso T1 que solo está procesando, por ejemplo, los registros que comienzan con AA para marcar todo el archivo como “analizado”. ¿Qué sucede si, por ejemplo, su aplicación se bloquea después de las actualizaciones de T1 pero mientras T2 aún está procesando registros BB? Es probable que algunos registros de BB se pierdan, ¿correcto?

De todos modos, el quid del problema es que tienes una condición de carrera con dos subprocesos que actualizan el mismo objeto. La excepción de objeto obsoleto solo significa que uno de tus hilos perdió la carrera. Una mejor solución evita por completo una carrera.

(Supongo que aquí el procesamiento de registros individuales es idempotente, si ese no es el caso, creo que tiene problemas mayores, ya que algunos modos de falla darán como resultado el reprocesamiento de registros. Si el procesamiento de registros debe realizarse una vez y solo una vez, entonces tiene un problema más difícil para el cual una cola de mensajes probablemente sería una mejor solución.)

Aprovecharía la funcionalidad de java.util.concurrent para enviar registros a trabajadores en subprocesos y hacer que el subproceso interactúe con el bloque de hibernación hasta que se hayan procesado todos los registros, momento en el que ese subproceso puede marcar el archivo como “analizado”.

Por ejemplo,

 // do something like this during initialization, or use a Guava LoadingCache... Map executors = new HashMap<>(); // note I'm assuming RecordType looks like an enum executors.put(RecordType.AA_RECORD, Executors.newSingleThreadExecutor()); 

luego, a medida que procesa el archivo, distribuye cada registro de la siguiente manera, creando una lista de futuros correspondientes al estado de las tareas en cola. Supongamos que el procesamiento exitoso de un registro devuelve un booleano “verdadero”:

 List> tasks = new ArrayList<>(); for (Record record: file.getRecords()) { Executor executorForRecord = executors.get(record.getRecordType()); tasks.add(executor.submit(new RecordProcessor(record))); } 

Ahora espere a que todas las tareas se completen con éxito: hay formas más elegantes de hacerlo, especialmente con Guava. Tenga en cuenta que también debe lidiar con ExecutionException aquí, si su tarea falló con una excepción, la estoy ocultando aquí.

 boolean allSuccess = true; for (Future task: tasks) { allSuccess = allSuccess && task.get(); if (!allSuccess) break; } // if all your tasks completed successfully, update the file record if (allSuccess) { file.setStatus("Parsed"); } 

Suponiendo que cada subproceso T1, T2 analizará diferentes partes del archivo, significa que nadie anulará el otro análisis de subproceso. Lo mejor es desacoplar el proceso de análisis de la confirmación de DB.

T1, T2 realizará el análisis T3 o el subproceso principal realizará la confirmación una vez que ambos T1, T2 hayan finalizado. y creo que en este enfoque es más correcto cambiar el estado del archivo a Parsed solo cuando ambos subprocesos hayan finalizado.

puede pensar en T3 como una clase CommitService que espera hasta T1, T2 finsih y luego se compromete a DB

CountDownLatch es una herramienta útil para hacerlo. y aqui hay un ejemplo

    Intereting Posts