TL; DR: La siguiente pregunta se reduce a: Al insertar una fila, ¿hay una ventana de oportunidad entre la generación de un nuevo Identity
valor y el bloqueo de la clave de fila correspondiente en el índice agrupado, donde un observador externo podría ver un nuevo Identity
valor insertado por una transacción concurrente? (En SQL Server).
Versión detallada
Tengo una tabla de SQL Server con una Identity
columna llamada CheckpointSequence
, que es la clave del índice agrupado de la tabla (que también tiene varios índices no agrupados adicionales). Las filas se insertan en la tabla mediante varios procesos y subprocesos concurrentes (a nivel de aislamiento READ COMMITTED
y sin él IDENTITY_INSERT
). Al mismo tiempo, hay procesos que leen periódicamente filas del índice agrupado, ordenadas por esa CheckpointSequence
columna (también a nivel de aislamiento READ COMMITTED
, con la READ COMMITTED SNAPSHOT
opción desactivada).
Actualmente confío en el hecho de que los procesos de lectura nunca pueden "omitir" un punto de control. Mi pregunta es: ¿Puedo confiar en esta propiedad? Y si no, ¿qué podría hacer para que sea verdad?
Ejemplo: cuando se insertan filas con valores de identidad 1, 2, 3, 4 y 5, el lector no debe ver la fila con el valor 5 antes de ver la que tiene el valor 4. Las pruebas muestran que la consulta, que contiene una ORDER BY CheckpointSequence
cláusula ( y una WHERE CheckpointSequence > -1
cláusula), bloquea de manera confiable cada vez que se lee la fila 4, pero aún no se ha confirmado, incluso si la fila 5 ya se ha confirmado.
Creo que, al menos en teoría, puede haber una condición de carrera aquí que podría hacer que esta suposición se rompa. Desafortunadamente, la documentación sobre Identity
no dice mucho acerca de cómo Identity
funciona en el contexto de múltiples transacciones concurrentes, solo dice "Cada nuevo valor se genera en función del valor inicial y el incremento actual". y "Cada nuevo valor para una transacción particular es diferente de otras transacciones concurrentes en la tabla". ( MSDN )
Mi razonamiento es que debe funcionar de alguna manera así:
- Se inicia una transacción (explícita o implícitamente).
- Se genera un valor de identidad (X).
- El bloqueo de fila correspondiente se toma en el índice agrupado en función del valor de identidad (a menos que se active la escalada de bloqueo, en cuyo caso toda la tabla está bloqueada).
- Se inserta la fila.
- La transacción se confirma (posiblemente mucho tiempo después), por lo que el bloqueo se elimina nuevamente.
Creo que entre los pasos 2 y 3, hay una ventana muy pequeña donde
- una sesión concurrente podría generar el siguiente valor de identidad (X + 1) y ejecutar todos los pasos restantes,
- permitiendo así que un lector que llegue exactamente en ese punto de tiempo lea el valor X + 1, sin el valor de X.
Por supuesto, la probabilidad de esto parece extremadamente baja; pero aún así, podría suceder. O podría?
(Si está interesado en el contexto: esta es la implementación del Motor de persistencia SQL de NEventStore. NEventStore implementa un almacén de eventos de solo agregado donde cada evento obtiene un nuevo número de secuencia de punto de control ascendente. Los clientes leen los eventos del almacén de eventos ordenados por punto de control para realizar cálculos de todo tipo. Una vez que se ha procesado un evento con punto de control X, los clientes solo consideran eventos "más nuevos", es decir, eventos con punto de control X + 1 y superior. Por lo tanto, es vital que los eventos nunca se puedan omitir, ya que nunca se volverían a considerar. Actualmente estoy tratando de determinar si la Identity
implementación del punto de control basado en los requisitos cumple con este requisito. Estas son las sentencias SQL exactas utilizadas : esquema , consulta del escritor ,Consulta del lector .)
Si estoy en lo cierto y podría surgir la situación descrita anteriormente, solo puedo ver dos opciones para tratar con ellos, los cuales son insatisfactorios:
- Cuando vea un valor de secuencia de punto de control X + 1 antes de haber visto X, descarte X + 1 e intente nuevamente más tarde. Sin embargo, debido a que, por
Identity
supuesto , puede producir brechas (p. Ej., Cuando se revierte la transacción), es posible que X nunca aparezca. - Entonces, el mismo enfoque, pero acepta la brecha después de n milisegundos. Sin embargo, ¿qué valor de n debo asumir?
¿Alguna idea mejor?
fuente
Respuestas:
Sí.
La asignación de valores de identidad es independiente de la transacción del usuario que la contiene . Esta es una razón por la que los valores de identidad se consumen incluso si la transacción se revierte. La operación de incremento en sí está protegida por un pestillo para evitar la corrupción, pero ese es el alcance de las protecciones.
En las circunstancias específicas de su implementación, la asignación de identidad (una llamada a
CMEDSeqGen::GenerateNewValue
) se realiza antes de que la transacción del usuario para la inserción se active (y antes de que se bloquee).Al ejecutar dos inserciones simultáneamente con un depurador adjunto para permitirme congelar un subproceso justo después de que el valor de identidad se incremente y se asigne, pude reproducir un escenario donde:
Después del paso 3, una consulta que utiliza row_number bajo lectura de bloqueo confirmada devolvió lo siguiente:
En su implementación, esto daría como resultado que la ID de punto de control 3 se omita incorrectamente.
La ventana de la mala oportunidad es relativamente pequeña, pero existe. Para dar un escenario más realista que tener un depurador conectado: un hilo de consulta en ejecución puede generar el programador después del paso 1 anterior. Esto permite que un segundo subproceso asigne un valor de identidad, insertar y confirmar, antes de que el subproceso original se reanude para realizar su inserción.
Para mayor claridad, no hay bloqueos u otros objetos de sincronización que protejan el valor de identidad después de que se asigna y antes de que se use. Por ejemplo, después del paso 1 anterior, una transacción concurrente puede ver el nuevo valor de identidad utilizando funciones T-SQL como
IDENT_CURRENT
antes de que la fila exista en la tabla (incluso sin confirmar).Básicamente, no hay más garantías en torno a los valores de identidad que las documentadas :
Eso es realmente
Si se requiere un estricto procesamiento transaccional FIFO, es probable que no tenga más remedio que serializar manualmente. Si la aplicación tiene requisitos menos únicos, tiene más opciones. La pregunta no es 100% clara en ese sentido. Sin embargo, puede encontrar información útil en el artículo de Remus Rusanu Uso de tablas como colas .
fuente
Como Paul White respondió absolutamente correcto, existe la posibilidad de filas de identidad "omitidas" temporalmente. Aquí hay un pequeño fragmento de código para reproducir este caso por su cuenta.
Cree una base de datos y una tabla de prueba:
Realice inserciones y selecciones concurrentes en esta tabla en un programa de consola C #:
Esta consola imprime una línea para cada caso cuando uno de los hilos de lectura "pierde" una entrada.
fuente
IDENTITY
produciría huecos (como deshacer una transacción), las líneas impresas muestran valores "omitidos" (o al menos lo hicieron cuando ejecuté y lo comprobé en mi máquina). Muy buena muestra de reproducción!Es mejor no esperar que las identidades sean consecutivas porque hay muchos escenarios que pueden dejar huecos. Es mejor considerar la identidad como un número abstracto y no atribuirle ningún significado comercial.
Básicamente, pueden ocurrir brechas si revierte las operaciones INSERT (o elimina explícitamente las filas), y pueden ocurrir duplicados si establece la propiedad de tabla IDENTITY_INSERT en ON.
Pueden ocurrir brechas cuando:
La propiedad de identidad en una columna nunca ha garantizado:
• Singularidad
• Valores consecutivos dentro de una transacción. Si los valores deben ser consecutivos, la transacción debe usar un bloqueo exclusivo en la tabla o el nivel de aislamiento SERIALIZABLE.
• Valores consecutivos después del reinicio del servidor.
• Reutilización de valores.
Si no puede usar valores de identidad debido a esto, cree una tabla separada que contenga un valor actual y administre el acceso a la tabla y la asignación de números con su aplicación. Esto tiene el potencial de afectar el rendimiento.
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx
fuente
ORDER BY CheckpointSequence
cláusula (que resulta ser el orden del índice agrupado). Creo que se reduce a la pregunta de si la generación de un valor de Identidad está de alguna manera vinculada a los bloqueos tomados por la instrucción INSERT, o si estas son simplemente dos acciones no relacionadas realizadas por SQL Server, una tras otra.SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence
. No creo que esta consulta lea más allá de la fila bloqueada 4, ¿o no? (En mis experimentos, se bloquea cuando la consulta intenta adquirir el bloqueo de TECLA para la fila 4.)Sospecho que ocasionalmente puede ocasionar problemas, problemas que empeoran cuando el servidor está bajo una gran carga. Considere dos transacciones:
En el escenario anterior, su LAST_READ_ID será 6, por lo que 5 nunca se leerán.
fuente
Ejecutando este script:
A continuación se muestran los bloqueos que veo adquiridos y liberados como capturados por una sesión de evento extendido:
Observe el bloqueo de TECLA RI_N adquirido inmediatamente antes del bloqueo de tecla X para la nueva fila que se está creando. Este bloqueo de rango de corta duración evitará que una inserción concurrente adquiera otro bloqueo RI_N KEY ya que los bloqueos RI_N son incompatibles. La ventana que mencionó entre los pasos 2 y 3 no es preocupante porque el bloqueo de rango se adquiere antes que el bloqueo de fila en la clave recién generada.
Siempre que
SELECT...ORDER BY
comience la exploración antes de las filas recién insertadas deseadas, esperaría el comportamiento que desea en elREAD COMMITTED
nivel de aislamiento predeterminado siempre que laREAD_COMMITTED_SNAPSHOT
opción de base de datos esté desactivada.fuente
RangeI_N
son compatibles , es decir, no se bloquean entre sí (el bloqueo está principalmente allí para bloquear un lector serializable existente).Según tengo entendido de SQL Server, el comportamiento predeterminado es que la segunda consulta no muestre ningún resultado hasta que se haya confirmado la primera consulta. Si la primera consulta realiza un ROLLBACK en lugar de un COMMIT, entonces tendrá una ID faltante en su columna.
Configuracion basica
Tabla de base de datos
Creé una tabla de base de datos con la siguiente estructura:
Nivel de aislamiento de la base de datos
Verifiqué el nivel de aislamiento de mi base de datos con la siguiente declaración:
Lo que devolvió el siguiente resultado para mi base de datos:
(Esta es la configuración predeterminada para una base de datos en SQL Server 2012)
Scripts de prueba
Los siguientes scripts se ejecutaron utilizando la configuración estándar del cliente SSMS de SQL Server y la configuración estándar de SQL Server.
Configuraciones de conexiones del cliente
El cliente se ha configurado para usar el Nivel de aislamiento de transacción
READ COMMITTED
según las Opciones de consulta en SSMS.Consulta 1
La siguiente consulta se ejecutó en una ventana de consulta con el SPID 57
Consulta 2
La siguiente consulta se ejecutó en una ventana de consulta con el SPID 58
La consulta no se completa y está esperando que se libere el bloqueo eXclusive en una PÁGINA.
Script para determinar el bloqueo
Este script muestra el bloqueo que ocurre en los objetos de la base de datos para las dos transacciones:
Y aquí están los resultados:
Los resultados muestran que la ventana de consulta uno (SPID 57) tiene un bloqueo Compartido (S) en la BASE DE DATOS, un bloqueo Intencionado eXlusive (IX) en el OBJETO, un bloqueo Intencionado eXlusive (IX) en la PÁGINA en la que desea insertar y un eXclusive bloqueo (X) en la TECLA se ha insertado, pero aún no se ha confirmado.
Debido a los datos no confirmados, la segunda consulta (SPID 58) tiene un bloqueo Compartido (S) en el nivel BASE DE DATOS, un bloqueo Compartido Intencionado (IS) en el OBJETO, un bloqueo Compartido Intencionado (IS) en la página un bloqueo Compartido (S ) bloquee la CLAVE con un estado de solicitud WAIT.
Resumen
La consulta en la primera ventana de consulta se ejecuta sin comprometerse. Como la segunda consulta solo puede enviar
READ COMMITTED
datos, espera hasta que se agote el tiempo de espera o hasta que la transacción se haya confirmado en la primera consulta.Esto es, según tengo entendido, el comportamiento predeterminado de Microsoft SQL Server.
Debería observar que la ID está en secuencia para lecturas posteriores de las instrucciones SELECT si la primera instrucción se COMPROMETE.
Si la primera instrucción hace un ROLLBACK, encontrará una ID faltante en la secuencia, pero aún con la ID en orden ascendente (siempre que haya creado el ÍNDICE con la opción predeterminada o ASC en la columna ID).
Actualizar:
(Sin rodeos) Sí, puede confiar en que la columna de identidad funcione correctamente, hasta que encuentre un problema. Solo hay un HOTFIX con respecto a SQL Server 2000 y la columna de identidad en el sitio web de Microsoft.
Si no puede confiar en que la columna de identidad se actualice correctamente, creo que habría más revisiones o parches en el sitio web de Microsoft.
Si tiene un Contrato de soporte técnico de Microsoft, siempre puede abrir un Caso de asesoramiento y solicitar información adicional.
fuente
Identity
valor y la adquisición del bloqueo de TECLA en la fila (donde podrían caer las lecturas / escritores concurrentes). No creo que sus observaciones demuestren que esto es imposible porque uno no puede detener la ejecución de consultas y analizar bloqueos durante ese período de tiempo ultracorto.