Estoy atrapado en un problema de concurrencia.
Es un problema típico en el que el usuario envía 2 o 3 transacciones para conservar algunos datos que NO DEBEN SER duplicados en la base de datos, en caso de un registro duplicado, debe devolver un error.
Este problema es fácil cuando puede agregar un índice (único) a una columna donde almacena un hash.
Pero en este caso, tengo una tabla enorme (probablemente millones de registros) y no puedo simplemente modificar la tabla.
De hecho, tenemos una columna donde almacenamos un hash de los datos que no deberían duplicarse, pero no se estableció un índice único.
Estoy probando mi código java para verificar si existe justo antes del vaciado, aún obteniendo duplicados.
Mis posibles soluciones para esto son:
- Cree un activador que verifique si el hash que estoy tratando de insertar ya existe en la tabla.
- Cree otra tabla para almacenar índices únicos para esta tabla y agregue una clave foránea a la tabla principal.
- Siéntate en posición fetal y llora
fuente
Respuestas:
Hay un par de escenarios posibles que son fáciles de resolver y uno pernicioso que no lo es.
Para un usuario que ingresa un valor, luego ingresa el mismo valor un tiempo después, un simple SELECCIONAR antes de que INSERT detecte el problema. Esto funciona para el caso en que un usuario envía un valor y algún tiempo después otro usuario envía el mismo valor.
Si el usuario envía una lista de valores con duplicados, digamos {ABC, DEF, ABC}, en una sola invocación del código, la aplicación puede detectar y filtrar los duplicados, quizás arrojando un error. También deberá verificar que la base de datos no contenga ninguno de los valores únicos antes de la inserción.
El escenario complicado es cuando la escritura de un usuario está dentro del DBMS al mismo tiempo que la escritura de otro usuario, y están escribiendo el mismo valor. Entonces tienes una carrera una condición entre ellos. Dado que el DBMS es (muy probablemente, usted no dice cuál está usando) un sistema multitarea preventivo, cualquier tarea puede pausarse en cualquier momento de su ejecución. Eso significa que la tarea del usuario1 puede verificar que no haya una fila existente, luego la tarea del usuario2 puede verificar que no haya una fila existente, luego la tarea del usuario1 puede insertar esa fila, luego la tarea del usuario2 puede insertar esa fila. En cada punto, las tareas son individualmente felices porque están haciendo lo correcto. Sin embargo, a nivel mundial se produce un error.
Normalmente, un DBMS se encargaría de esto poniendo un bloqueo en el valor en cuestión. En este problema, está creando una nueva fila para que todavía no haya nada que bloquear. La respuesta es un bloqueo de rango. Como sugiere, esto bloquea un rango de valores, ya sean actuales o no. Una vez bloqueado, ese rango no puede ser accedido por otra tarea hasta que se libere el bloqueo. Para obtener bloqueos de rango, debe especificar un nivel de aislamiento de SERIALIZABLE . El fenómeno de otra tarea a escondidas después de que su tarea se haya verificado se conoce como registros fantasmas .
Establecer el nivel de aislamiento en Serializable en toda la aplicación tendrá implicaciones. El rendimiento se reducirá. Otras condiciones de carrera que funcionaron suficientemente bien en el pasado pueden comenzar a mostrar errores ahora. Sugeriría configurarlo en la conexión que ejecuta su código inductor duplicado y dejar el resto de la aplicación como está.
Una alternativa basada en código es verificar después de la escritura en lugar de antes. También haga INSERT, luego cuente el número de filas que tienen ese valor hash. Si hay duplicados, deshaga la acción. Esto puede tener algunos resultados perversos. Digamos que la tarea 1 escribe, luego la tarea 2. Luego, la tarea 1 verifica y encuentra un duplicado. Retrocede aunque fue el primero. Del mismo modo, ambas tareas pueden detectar el duplicado y la reversión. Pero al menos tendrá un mensaje con el que trabajar, un mecanismo de reintento y ningún duplicado nuevo. Los retrocesos están mal vistos, al igual que usar excepciones para controlar el flujo del programa. Tenga en cuenta que todosel trabajo en la transacción se revertirá, no solo la escritura que induce duplicados. Y tendrá que tener transacciones explícitas que pueden reducir la concurrencia. La verificación duplicada será terriblemente lenta a menos que tenga un índice en el hash. Si lo haces, ¡también puedes hacerlo único!
Como ha comentado, la solución real es un índice único. Me parece que esto debería encajar en su ventana de mantenimiento (aunque, por supuesto, conoce mejor su sistema). Digamos que el hash es de ocho bytes. Para cien millones de filas, eso es aproximadamente 1 GB. La experiencia sugiere que un poco de hardware razonable procesaría estas filas en un minuto o dos, como máximo. La comprobación y la eliminación duplicadas se sumarán a esto, pero se pueden programar con anticipación. Sin embargo, esto es solo un aparte.
fuente
Verificar las colisiones de hash es un buen primer paso, pero tenga cuidado, no puede garantizar que el mismo programa produzca el mismo hash en los mismos datos si se reinicia . Muchas funciones hash "rápidas" usan un prng incorporado que se siembra al momento del inicio del programa. Use un hash criptográfico si el hash debe ser siempre el mismo, pase lo que pase, como lo hace en esta aplicación. Tenga en cuenta que no necesita un hash criptográfico bueno o seguro.
El segundo paso es verificar la igualdad de datos, ya que incluso las mejores funciones de hash a veces darán lugar a colisiones, ya que (generalmente) está reduciendo la entropía de sus datos.
Entonces:
Paso 1: verifica si tienes una colisión en un hash criptográfico
Paso 2: si los hashes coinciden, verifique que los datos reales sean los mismos
fuente
Haga una nueva tabla con una clave primaria única
En el lado del cliente, comience a generar GUID para cada registro para que pueda detectar reenvíos simples.
Coloque nuevos registros en la nueva tabla para que al menos sea bueno para la entrada de nuevos datos.
Tener una columna en la nueva tabla "CheckedAgainstOldData"
Realice una tarea de backend que haga lo que sea que haga con la comprobación de hash lenta actual para ver si puede encontrar un duplicado en los datos antiguos y establecer el indicador en consecuencia, rechazar los duplicados en este punto, enviando una notificación al cliente.
Mientras tanto, tenga otra tarea de backend que mueva los datos de la tabla anterior a la nueva, verificando duplicados con su comprobación de hash y generando el GUID.
Puede dejar esta tarea ejecutándose durante varios días (si es necesario), transfiriendo los datos sin tiempo de inactividad.
Una vez que se completa la transferencia, puede desactivar el lento proceso "CheckedAgainstOldData". y transfiera todos los datos a una sola tabla.
Francamente, si el problema es tan grave como lo describe y el software es antiguo, entonces tendrá miles de duplicados.
fuente
Suponiendo que los datos que provienen del "usuario" significan alguien sentado frente a un teclado y que los engaños surgen de dos usuarios que ingresan los mismos datos en el mismo momento. Intente agregar una función que cause un retraso aleatorio al comienzo del disparador. Déle un mínimo del tiempo que tarde en escribir un nuevo registro en la tabla y probablemente un máximo de no más de un nanocentro más o menos. De esa forma, cuando reciba solicitudes de engaño, se debe hacer el primero y el desencadenante de existencia debe devolver el resultado correcto. (Aclaración: cada llamada debe tener su propio tiempo de retraso aleatorio único, junto con los mismos principios que el protocolo ALOHA )
fuente