web-dev-qa-db-fra.com

Puis-je compter sur la lecture des valeurs d'identité SQL Server dans l'ordre?

TL; DR: La question ci-dessous se résume à: Lors de l'insertion d'une ligne, y a-t-il une fenêtre d'opportunité entre la génération d'une nouvelle valeur Identity et la verrouillage de la clé de ligne correspondante dans l'index clusterisé, où un observateur externe pourrait voir une plus récente = Identity valeur insérée par une transaction simultanée? (Dans SQL Server.)

Version détaillée

J'ai une table SQL Server avec une colonne Identity appelée CheckpointSequence, qui est la clé de l'index cluster de la table (qui possède également un certain nombre d'index non clusterisés supplémentaires). Les lignes sont insérées dans le tableau par plusieurs processus et threads simultanés (au niveau d'isolement READ COMMITTED, Et sans IDENTITY_INSERT). Dans le même temps, il existe des processus périodiques - lecture lignes de l'index cluster, ordonnées par cette colonne CheckpointSequence (également au niveau d'isolement READ COMMITTED, avec le READ COMMITTED SNAPSHOT option désactivée).

Je compte actuellement sur le fait que les processus de lecture ne peuvent jamais "sauter" un point de contrôle. Ma question est: Puis-je compter sur cette propriété? Et sinon, que puis-je faire pour que cela soit vrai?

Exemple: Lorsque des lignes avec les valeurs d'identité 1, 2, 3, 4 et 5 sont insérées, le lecteur ne doit pas voir la ligne avec la valeur 5 avant de voir celle avec la valeur 4. Les tests montrent que la requête, qui contient un ORDER BY CheckpointSequence clause (et un WHERE CheckpointSequence > -1), bloque de manière fiable chaque fois que la ligne 4 doit être lue, mais pas encore validée, même si la ligne 5 a déjà été validée.

Je crois qu'au moins en théorie, il peut y avoir une condition de race ici qui pourrait faire rompre cette hypothèse. Malheureusement, la documentation sur Identity ne dit pas grand-chose sur le fonctionnement de Identity dans le contexte de plusieurs transactions simultanées, elle dit seulement "Chaque nouvelle valeur est générée en fonction de la valeur de départ et de l'incrémentation en cours. " et "Chaque nouvelle valeur pour une transaction particulière est différente des autres transactions simultanées sur la table." ( MSDN )

Mon raisonnement est, cela doit fonctionner en quelque sorte comme ceci:

  1. Une transaction est lancée (explicitement ou implicitement).
  2. Une valeur d'identité (X) est générée.
  3. Le verrou de ligne correspondant est pris sur l'index clusterisé en fonction de la valeur d'identité (sauf si l'escalade de verrous entre en jeu, auquel cas la table entière est verrouillée).
  4. La ligne est insérée.
  5. La transaction est validée (peut-être beaucoup de temps plus tard), donc le verrou est à nouveau retiré.

Je pense qu'entre les étapes 2 et 3, il y a une toute petite fenêtre où

  • une session simultanée pourrait générer la prochaine valeur d'identité (X + 1) et exécuter toutes les étapes restantes,
  • permettant ainsi à un lecteur venant exactement à ce moment de lire la valeur X + 1, sans la valeur de X.

Bien sûr, la probabilité de cela semble extrêmement faible; mais encore - cela pourrait arriver. Ou est-ce possible?

(Si vous êtes intéressé par le contexte: il s'agit de l'implémentation de moteur de persistance SQL de NEventStore . NEventStore implémente un magasin d'événements à ajouter uniquement où chaque événement obtient un nouveau numéro de séquence de point de contrôle croissant. Les clients lisent les événements à partir du magasin d'événements commandé par point de contrôle afin d'effectuer des calculs de toutes sortes. Une fois qu'un événement avec point de contrôle X a été traité, les clients ne prennent en compte que les événements "plus récents", c'est-à-dire les événements avec point de contrôle X + 1 et plus. Par conséquent, il est vital que les événements ne peuvent jamais être ignorés, car ils ne seront plus jamais pris en compte. J'essaie actuellement de déterminer si l'implémentation de point de contrôle basée sur Identity répond à cette exigence. Ce sont les instructions SQL exactes utilisées : Schema , Writer's query , Reader's Query .)

Si j'ai raison et que la situation décrite ci-dessus pourrait survenir, je ne vois que deux options pour y faire face, toutes deux insatisfaisantes:

  • Lorsque vous voyez une valeur de séquence de point de contrôle X + 1 avant d'avoir vu X, ignorez X + 1 et réessayez plus tard. Cependant, parce que Identity peut bien sûr produire des lacunes (par exemple, lorsque la transaction est annulée), X pourrait ne jamais arriver.
  • Donc, même approche, mais acceptez l'écart après n millisecondes. Cependant, quelle valeur de n dois-je supposer?

De meilleures idées?

24
Fabian Schmied

Lors de l'insertion d'une ligne, existe-t-il une fenêtre d'opportunité entre la génération d'une nouvelle valeur d'identité et le verrouillage de la clé de ligne correspondante dans l'index clusterisé, où un observateur externe pourrait voir une nouvelle valeur d'identité insérée par une transaction simultanée?

Oui.

L'attribution des valeurs d'identité est indépendante de la transaction utilisateur contenante. C'est une des raisons pour lesquelles les valeurs d'identité sont consommées même si la transaction est annulée. L'opération d'incrémentation elle-même est protégée par un verrou pour éviter la corruption, mais c'est l'étendue des protections.

Dans les circonstances spécifiques de votre implémentation, l'attribution d'identité (un appel à CMEDSeqGen::GenerateNewValue) est effectuée avant même que la transaction utilisateur pour l'insertion ne soit activée (et donc avant que les verrous ne soient pris).

En exécutant deux insertions simultanément avec un débogueur attaché pour me permettre de geler un thread juste après que la valeur d'identité est incrémentée et allouée, j'ai pu reproduire un scénario où:

  1. La session 1 acquiert une valeur d'identité (3)
  2. La session 2 acquiert une valeur d'identité (4)
  3. La session 2 effectue son insertion et valide (donc la ligne 4 est entièrement visible)
  4. La session 1 effectue son insertion et valide (ligne 3)

Après l'étape 3, une requête utilisant row_number sous verrouillage en lecture validée a renvoyé ce qui suit:

Screenshot

Dans votre implémentation, cela entraînerait le non-respect de l'ID de point de contrôle 3.

La fenêtre de l'opportunité est relativement petite, mais elle existe. Pour donner un scénario plus réaliste que d'avoir un débogueur attaché: Un thread de requête en cours d'exécution peut générer le planificateur après l'étape 1 ci-dessus. Cela permet à un deuxième thread d'allouer une valeur d'identité, d'insérer et de valider, avant que le thread d'origine ne reprenne pour effectuer son insertion.

Pour plus de clarté, il n'y a pas de verrous ou d'autres objets de synchronisation protégeant la valeur d'identité après son allocation et avant son utilisation. Par exemple, après l'étape 1 ci-dessus, une transaction simultanée peut voir la nouvelle valeur d'identité à l'aide de fonctions T-SQL comme IDENT_CURRENT avant que la ligne n'existe dans la table (même non validée).

Fondamentalement, il n'y a pas plus de garanties autour des valeurs d'identité que documenté :

  • Chaque nouvelle valeur est générée en fonction de la valeur de départ et de l'incrément actuels.
  • Chaque nouvelle valeur pour une transaction particulière est différente des autres transactions simultanées sur la table.

C'est vraiment ça.

Si strict transactionnel FIFO est requis, vous n'avez probablement pas d'autre choix que de sérialiser manuellement. Si l'application a des exigences moins lourdes, vous avez plus d'options. La La question n'est pas claire à 100% à cet égard. Néanmoins, vous pouvez trouver des informations utiles dans l'article de Remus Rusanu tilisation des tables comme files d'attente .

26
Paul White 9

Comme Paul White a répondu tout à fait correctement, il existe une possibilité pour les lignes d'identité temporairement "ignorées". Voici juste un petit morceau de code pour reproduire ce cas pour vous-même.

Créez une base de données et une table de test:

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)

Effectuez des insertions et des sélections simultanées sur cette table dans un programme de console 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();
                }
            }
        }
    }
}

Cette console imprime une ligne pour chaque cas lorsqu'un des fils de lecture "manque" une entrée.

7
Stefan Kainz

Il est préférable de ne pas s'attendre à ce que les identités soient consécutives car il existe de nombreux scénarios qui peuvent laisser des lacunes. Il est préférable de considérer l'identité comme un numéro abstrait et de ne lui attribuer aucune signification commerciale.

Fondamentalement, des lacunes peuvent se produire si vous annulez des opérations INSERT (ou supprimez explicitement des lignes) et des doublons peuvent se produire si vous définissez la propriété de table IDENTITY_INSERT sur ON.

Des lacunes peuvent survenir lorsque:

  1. Les enregistrements sont supprimés.
  2. Une erreur s'est produite lors de la tentative d'insertion d'un nouvel enregistrement (annulé)
  3. Une mise à jour/insert avec une valeur explicite (option identity_insert).
  4. La valeur incrémentielle est supérieure à 1.
  5. Une transaction est annulée.

La propriété d'identité sur une colonne n'a jamais garanti:

• Unicité

• Valeurs consécutives dans une transaction. Si les valeurs doivent être consécutives, la transaction doit utiliser un verrou exclusif sur la table ou utiliser le niveau d'isolement SERIALIZABLE.

• Valeurs consécutives après redémarrage du serveur.

• Réutilisation des valeurs.

Si vous ne pouvez pas utiliser de valeurs d'identité pour cette raison, créez une table distincte contenant une valeur actuelle et gérez l'accès à la table et à l'affectation des numéros avec votre application. Cela peut avoir un impact sur les performances.

https://msdn.Microsoft.com/en-us/library/ms186775 (v = sql.105) .aspx
https://msdn.Microsoft.com/en-us/library/ms186775 (v = sql.110) .aspx

5
stacylaray

Je soupçonne que cela peut occasionnellement entraîner des problèmes, des problèmes qui s'aggravent lorsque le serveur est sous forte charge. Considérons deux transactions:

  1. T1: insérez dans T ... - disons 5 insérez
  2. T2: insérez dans T ... - disons 6 insérez
  3. T2: validation
  4. Le lecteur voit 6 mais pas 5
  5. T1: commit

Dans le scénario ci-dessus, votre LAST_READ_ID sera 6, donc 5 ne sera jamais lu.

1
Lennart

Exécution de ce script:

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

Vous trouverez ci-dessous les verrous acquis et libérés tels qu'ils ont été capturés par une session d'événement étendu:

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      

Notez le verrou RI_N KEY acquis immédiatement avant le verrou X pour la nouvelle ligne en cours de création. Ce verrou de plage de courte durée empêchera un insert simultané d'acquérir un autre verrou RI_N KEY car les verrous RI_N sont incompatibles. La fenêtre que vous avez mentionnée entre les étapes 2 et 3 n'est pas un problème car le verrouillage de plage est acquis avant le verrouillage de ligne sur la clé nouvellement générée.

Tant que votre SELECT...ORDER BY commence l'analyse avant les lignes nouvellement insérées souhaitées, je m'attendrais à ce que vous souhaitiez dans la valeur par défaut READ COMMITTED niveau d'isolement tant que la base de données READ_COMMITTED_SNAPSHOT l'option est désactivée.

0
Dan Guzman

D'après ma compréhension de SQL Server, le comportement par défaut est que la deuxième requête n'affiche aucun résultat tant que la première requête n'a pas été validée. Si la première requête effectue un ROLLBACK au lieu d'un COMMIT, alors vous aurez un ID manquant dans votre colonne.

Configuration de base

Table de base de données

J'ai créé une table de base de données avec la structure suivante:

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

Niveau d'isolement de la base de données

J'ai vérifié le niveau d'isolement de ma base de données avec la déclaration suivante:

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

Qui a renvoyé le résultat suivant pour ma base de données:

snapshot_isolation_state    snapshot_isolation_state_desc   is_read_committed_snapshot_on
0                           OFF                             0

(Il s'agit du paramètre par défaut pour une base de données dans SQL Server 2012)

Scripts de test

Les scripts suivants ont été exécutés à l'aide des paramètres client SQL Server SSMS standard et des paramètres SQL Server standard.

Paramètres de connexion client

Le client a été configuré pour utiliser le niveau d'isolation de transaction READ COMMITTED selon les options de requête dans SSMS.

Requête 1

La requête suivante a été exécutée dans une fenêtre de requête avec le 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]
*/

Requête 2

La requête suivante a été exécutée dans une fenêtre de requête avec le 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 requête n'est pas terminée et attend la libération du verrou eXclusive sur une PAGE.

Script pour déterminer le verrouillage

Ce script affiche le verrouillage se produisant sur les objets de base de données pour les deux transactions:

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)

Et voici les résultats:

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

Les résultats montrent que la fenêtre de requête un (SPID 57) a un verrou partagé (S) sur la BASE DE DONNÉES un verrou eXlusif (IX) prévu sur l'OBJET, un verrou eXlusif (IX) prévu sur la PAGE qu'il souhaite insérer et un eXclusif verrou (X) sur la CLÉ insérée, mais pas encore validé.

En raison des données non validées, la deuxième requête (SPID 58) a un verrou partagé (S) au niveau de la BASE DE DONNÉES, un verrou partagé (IS) prévu sur l'objet, un verrou partagé (IS) prévu sur la page un partagé (S ) verrouiller la clé avec un statut de demande WAIT.

Sommaire

La requête dans la première fenêtre de requête s'exécute sans validation. Parce que la deuxième requête ne peut que READ COMMITTED données qu'il attend soit jusqu'à l'expiration du délai, soit jusqu'à ce que la transaction soit validée dans la première requête.

Cela vient de ma compréhension du comportement par défaut de Microsoft SQL Server.

Vous devez observer que l'ID est en effet en séquence pour les lectures suivantes par les instructions SELECT si la première instruction COMMITs.

Si la première instruction fait un ROLLBACK, vous trouverez alors un ID manquant dans la séquence, mais toujours avec l'ID dans l'ordre croissant (à condition que vous ayez créé l'INDEX avec l'option par défaut ou ASC dans la colonne ID).

Mise à jour:

(Franchement) Oui, vous pouvez compter sur le bon fonctionnement de la colonne d'identité jusqu'à ce que vous rencontriez un problème. Il n'y en a qu'un HOTFIX concernant SQL Server 2000 et la colonne d'identité sur le site Web de Microsoft.

Si vous ne pouviez pas vous fier à la mise à jour correcte de la colonne d'identité, je pense qu'il y aurait plus de correctifs ou de correctifs sur le site Web de Microsoft.

Si vous avez un contrat de support Microsoft, vous pouvez toujours ouvrir un dossier de conseil et demander des informations supplémentaires.

0
John aka hot2use