¿Cuáles son las formas posibles de evitar duplicados cuando no puede agregar un índice único?

10

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
rafuru
fuente
¿Su verificación del hash falla debido a colisiones de hash o un error en la verificación?
candied_orange
44
No entendí tu pregunta. Entonces, en lugar de indexar una vez para toda su gran tabla con millones de registros, ¿prefiere leer para cada uno de los próximos millones de registros que agregará, los millones existentes para buscar dobles? o duplicar alguna información y agregar uniones para hacer su cheque?
Christophe
El problema es que, para realizar este cambio, se me advirtió que necesitamos mucho espacio y un largo tiempo de inactividad para nuestro servicio, a fin de cumplir algunos requisitos, nuestro servicio no puede estar inactivo durante más de 2 horas mensuales. Sé que la mejor manera es realizar un mantenimiento en esta tabla, pero es algo que no puedo hacer en este momento, por lo que necesitamos una solución alternativa.
rafuru
44
No lo entiendo: ¿por qué agregar un disparador o agregar otra tabla para "emular" un índice toma menos tiempo de inactividad que simplemente agregar un índice a la tabla existente?
Doc Brown
2
@rafuru: ¿quién dijo que necesita crear un índice único? Un índice estándar y no único probablemente sea todo lo que necesita para encontrar rápidamente todas las filas con el mismo valor hash.
Doc Brown

Respuestas:

3

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.

Michael Green
fuente
2

De hecho, tenemos una columna donde almacenamos un hash de los datos que no deberían duplicarse, pero no se estableció un índice único.

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

Turksarama
fuente
No veo cómo esto responde la pregunta. Supongamos por un momento que la columna hash disponible se llena con una función hash determinista (de lo contrario, cualquier intento de utilizarla no tendría sentido). Según tengo entendido, el problema es que no hay índice en esa columna de hash en la base de datos, por lo que incluso el primer paso en su respuesta, verificar si hay una colisión, aún requeriría un escaneo completo de la tabla para cada nuevo registro en una tabla con varios millones de registros, que probablemente serán demasiado lentos.
Doc Brown
Es lo mejor que puede hacer sin crear un índice, que es lo que estaba haciendo la pregunta. Un escaneo hash al menos significa que solo tiene que verificar una columna, lo cual es mucho más rápido que verificar sin importar cuántas columnas tendrían que verificar.
Turksarama
Estoy bastante seguro, incluso cuando no es posible crear un índice (que en este caso probablemente lo es), la sugerencia original de los OP de " crear otra tabla para almacenar índices únicos para esta tabla y agregar una clave foránea a la tabla principal " hace mucho mas sentido.
Doc Brown
El hash determinista y el hash criptográfico son dos conceptos ortogonales, ¿no es así? un hash criptográfico puede no ser determinista y viceversa, un hash determinista podría muy bien no tener fuerza criptográfica.
Newtopian
No son lo mismo, pero tampoco son ortogonales. Los hashes criptográficos son un subconjunto de hashes deterministas, pero nadie se molesta realmente en hacer hashes deterministas no criptográficos a menos que específicamente desee que sea reversible por alguna razón.
Turksarama
2

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.

Ewan
fuente
1

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 )

Gregor y
fuente