¿Puedo confiar en leer los valores de identidad de SQL Server en orden?


24

TL; DR: La siguiente pregunta se reduce a: Al insertar una fila, ¿hay una ventana de oportunidad entre la generación de un nuevo Identityvalor 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 Identitycolumna 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 COMMITTEDy sin él IDENTITY_INSERT). Al mismo tiempo, hay procesos que leen periódicamente filas del índice agrupado, ordenadas por esa CheckpointSequencecolumna (también a nivel de aislamiento READ COMMITTED, con la READ COMMITTED SNAPSHOTopció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 CheckpointSequencecláusula ( y una WHERE CheckpointSequence > -1clá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 Identityno dice mucho acerca de cómo Identityfunciona 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í:

  1. Se inicia una transacción (explícita o implícitamente).
  2. Se genera un valor de identidad (X).
  3. 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).
  4. Se inserta la fila.
  5. 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 Identityimplementació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 Identitysupuesto , 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?


¿Has intentado usar Secuencia en lugar de identidad? Con la identidad, no creo que pueda predecir de manera confiable qué inserto obtendrá un valor de identidad particular, pero esto no debería ser un problema al usar una secuencia. Por supuesto, eso cambia la forma en que haces las cosas ahora.
Antoine Hernández

@SoleDBAGuy ¿No haría una secuencia que la condición de carrera que describí anteriormente sea aún más probable? Produzco un nuevo valor de secuencia X (reemplazando el paso 2 anterior), luego inserto una fila (pasos 3 y 4). Entre 2 y 3, existe la posibilidad de que alguien más produzca el siguiente valor de secuencia X + 1, lo confirme, y un lector lea ese valor X + 1 antes de siquiera insertar mi fila con el valor de secuencia X.
Fabian Schmied

Respuestas:


26

Al insertar una fila, ¿hay una ventana de oportunidad entre la generación de un nuevo valor de identidad y el bloqueo de la clave de fila correspondiente en el índice agrupado, donde un observador externo podría ver un nuevo valor de identidad insertado por una transacción concurrente?

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:

  1. La sesión 1 adquiere un valor de identidad (3)
  2. La sesión 2 adquiere un valor de identidad (4)
  3. La sesión 2 realiza su inserción y confirma (por lo que la fila 4 es totalmente visible)
  4. La sesión 1 realiza su inserción y confirmaciones (fila 3)

Después del paso 3, una consulta que utiliza row_number bajo lectura de bloqueo confirmada devolvió lo siguiente:

Captura de pantalla

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_CURRENTantes 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 :

  • Cada nuevo valor se genera en función de la semilla actual y el incremento.
  • Cada nuevo valor para una transacción particular es diferente de otras transacciones concurrentes en la tabla.

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 .


7

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:

create database IdentityTest
go
use IdentityTest
go
create table dbo.IdentityTest (ID int identity, c1 char(10))
create clustered index CI_dbo_IdentityTest_ID on dbo.IdentityTest(ID)

Realice inserciones y selecciones concurrentes en esta tabla en un programa de consola C #:

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading;

namespace IdentityTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var insertThreads = new List<Thread>();
            var selectThreads = new List<Thread>();

            //start threads for infinite inserts
            for (var i = 0; i < 100; i++)
            {
                insertThreads.Add(new Thread(InfiniteInsert));
                insertThreads[i].Start();
            }

            //start threads for infinite selects
            for (var i = 0; i < 10; i++)
            {
                selectThreads.Add(new Thread(InfiniteSelectAndCheck));
                selectThreads[i].Start();
            }
        }

        private static void InfiniteSelectAndCheck()
        {
            //infinite loop
            while (true)
            {
                //read top 2 IDs
                var cmd = new SqlCommand("select top(2) ID from dbo.IdentityTest order by ID desc")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    var dr = cmd.ExecuteReader();

                    //read first row
                    dr.Read();
                    var row1 = int.Parse(dr["ID"].ToString());

                    //read second row
                    dr.Read();
                    var row2 = int.Parse(dr["ID"].ToString());

                    //write line if row1 and row are not consecutive
                    if (row1 - 1 != row2)
                    {
                        Console.WriteLine("row1=" + row1 + ", row2=" + row2);
                    }
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }

        private static void InfiniteInsert()
        {
            //infinite loop
            while (true)
            {
                var cmd = new SqlCommand("insert into dbo.IdentityTest (c1) values('a')")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    cmd.ExecuteNonQuery();
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }
    }
}

Esta consola imprime una línea para cada caso cuando uno de los hilos de lectura "pierde" una entrada.


1
Código agradable pero solo verifica los identificadores consecutivos ( "// escribe la línea si la fila1 y la fila no son consecutivas" ). Puede haber lagunas producidas que imprimirá su código. Eso no significa que estas brechas se llenen más tarde.
ypercubeᵀᴹ

1
Debido a que el código no desencadena un escenario donde IDENTITYproducirí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!
Fabian Schmied

5

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:

  1. Los registros se eliminan.
  2. Se produjo un error al intentar insertar un nuevo registro (revertido)
  3. Una actualización / inserción con valor explícito (opción identity_insert).
  4. El valor incremental es más de 1.
  5. Una transacción se revierte.

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


Creo que las brechas no son mi problema principal: mi problema principal es la visibilidad ascendente de los valores. (Es decir, por ejemplo, el valor de identidad 7 no debe ser observable para una consulta ordenada por ese valor antes que el valor de identidad 6).
Fabian Schmied

1
He visto valores de identidad como: 1, 2, 5, 3, 4.
stacylaray

Claro, esto es fácilmente reproducible, por ejemplo, usando el escenario de la respuesta de Lennart. La pregunta con la que estoy luchando es si puedo observar ese orden de confirmación cuando uso una consulta con una ORDER BY CheckpointSequenceclá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.
Fabian Schmied

1
¿Cuál es la consulta? Si usa lectura confirmada, entonces, en su ejemplo, ordenar por mostraría 1, 2, 3, 5 porque se han confirmado y 4 no, es decir, lectura sucia. Además, su explicación de NEventStore establece "Por lo tanto, es vital que los eventos nunca se puedan omitir, ya que nunca se volverán a considerar".
stacylaray

La consulta se proporciona arriba ( gist.github.com/fschmied/47f716c32cb64b852f90 ): está paginada, pero se reduce a una simple 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.)
Fabian Schmied

1

Sospecho que ocasionalmente puede ocasionar problemas, problemas que empeoran cuando el servidor está bajo una gran carga. Considere dos transacciones:

  1. T1: insertar en T ... - digamos 5 insertarse
  2. T2: insertar en T ... - digamos 6 insertarse
  3. T2: commit
  4. El lector ve 6 pero no 5
  5. T1: cometer

En el escenario anterior, su LAST_READ_ID será 6, por lo que 5 nunca se leerán.


Mis pruebas parecen indicar que este escenario no es un problema porque Reader (paso 4) se bloqueará (hasta que T1 haya liberado sus bloqueos) cuando intente leer la fila con el valor 5. ¿Me estoy perdiendo algo?
Fabian Schmied

Puede que tengas razón, no conozco el mecanismo de bloqueo en el servidor SQL tan bien (por lo tanto, sospecho en mi respuesta).
Lennart

Depende del nivel de aislamiento del lector. Es mi ver ambos, bloquear o ver solo 6.
Michael Green

0

Ejecutando este script:

BEGIN TRAN;
INSERT INTO dbo.Example DEFAULT VALUES;
COMMIT;

A continuación se muestran los bloqueos que veo adquiridos y liberados como capturados por una sesión de evento extendido:

name            timestamp                   associated_object_id    mode    object_id   resource_type   session_id  resource_description
lock_acquired   2016-03-29 06:37:28.9968693 1585440722              IX      1585440722  OBJECT          51          
lock_acquired   2016-03-29 06:37:28.9969268 7205759890195415040     IX      0           PAGE            51          1:1235
lock_acquired   2016-03-29 06:37:28.9969306 7205759890195415040     RI_NL   0           KEY             51          (ffffffffffff)
lock_acquired   2016-03-29 06:37:28.9969330 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969579 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969598 7205759890195415040     IX      0           PAGE            51          1:1235
lock_released   2016-03-29 06:37:28.9969607 1585440722              IX      1585440722  OBJECT          51      

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 BYcomience la exploración antes de las filas recién insertadas deseadas, esperaría el comportamiento que desea en el READ COMMITTEDnivel de aislamiento predeterminado siempre que la READ_COMMITTED_SNAPSHOTopción de base de datos esté desactivada.


1
De acuerdo con technet.microsoft.com/en-us/library/… , dos bloqueos RangeI_Nson compatibles , es decir, no se bloquean entre sí (el bloqueo está principalmente allí para bloquear un lector serializable existente).
Fabian Schmied

@FabianSchmied, interesante. Ese tema entra en conflicto con la matriz de compatibilidad de bloqueo en technet.microsoft.com/en-us/library/ms186396(v=sql.105).aspx , que muestra que los bloqueos no son compatibles. El ejemplo de inserción en el enlace que mencionó indica el mismo comportamiento que se muestra en el seguimiento en mi respuesta (bloqueo de rango de inserción de corta duración para probar el rango antes del bloqueo de tecla exclusivo).
Dan Guzman

1
En realidad, la matriz dice "N" para "sin conflicto" (no para "no compatible") :)
Fabian Schmied

0

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:

CREATE TABLE identity_rc_test (
    ID4VALUE INT IDENTITY (1,1), 
    TEXTVALUE NVARCHAR(20),
    CONSTRAINT PK_ID4_VALUE_CLUSTERED 
        PRIMARY KEY CLUSTERED (ID4VALUE, TEXTVALUE)
)

Nivel de aislamiento de la base de datos

Verifiqué el nivel de aislamiento de mi base de datos con la siguiente declaración:

SELECT snapshot_isolation_state, 
       snapshot_isolation_state_desc, 
       is_read_committed_snapshot_on
FROM sys.databases WHERE NAME = 'mydatabase'

Lo que devolvió el siguiente resultado para mi base de datos:

snapshot_isolation_state    snapshot_isolation_state_desc   is_read_committed_snapshot_on
0                           OFF                             0

(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 COMMITTEDsegún las Opciones de consulta en SSMS.

Consulta 1

La siguiente consulta se ejecutó en una ventana de consulta con el SPID 57

SELECT * FROM dbo.identity_rc_test
BEGIN TRANSACTION [FIRST_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Nine')
/* Commit is commented out to prevent the INSERT from being commited
--COMMIT TRANSACTION [FIRST_QUERY]
--ROLLBACK TRANSACTION [FIRST_QUERY]
*/

Consulta 2

La siguiente consulta se ejecutó en una ventana de consulta con el SPID 58

BEGIN TRANSACTION [SECOND_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Ten')
COMMIT TRANSACTION [SECOND_QUERY]
SELECT * FROM dbo.identity_rc_test

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:

SELECT request_session_id, resource_type,
       resource_description, 
       resource_associated_entity_id,
       request_mode, request_status
FROM sys.dm_tran_locks
WHERE request_session_id IN (57, 58)

Y aquí están los resultados:

58  DATABASE                    0                   S   GRANT
57  DATABASE                    0                   S   GRANT
58  PAGE            1:79        72057594040549300   IS  GRANT
57  PAGE            1:79        72057594040549300   IX  GRANT
57  KEY         (a0aba7857f1b)  72057594040549300   X   GRANT
58  KEY         (a0aba7857f1b)  72057594040549300   S   WAIT
58  OBJECT                      245575913           IS  GRANT
57  OBJECT                      245575913           IX  GRANT

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 COMMITTEDdatos, 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.


1
Gracias por el análisis, pero mi pregunta es si hay una ventana de tiempo entre la generación del siguiente Identityvalor 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.
Fabian Schmied

No, no puede detener las declaraciones, pero mi observación (lenta) es lo que sucede de manera rápida / normal. Tan pronto como un SPID adquiera un bloqueo para insertar datos, el otro no podrá adquirir el mismo bloqueo. La declaración más rápida tendrá la ventaja de haber adquirido el bloqueo y la ID en secuencia. La siguiente declaración recibirá la siguiente ID después de que se haya liberado el bloqueo.
John aka hot2use

1
De manera normal, sus observaciones coinciden con las mías (y también con mis expectativas), es bueno saberlo. Sin embargo, me pregunto si hay situaciones excepcionales en las que no se mantengan.
Fabian Schmied
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.