TL; DR: La question ci-dessous se résume à: lors de l'insertion d'une ligne, existe-t-il une fenêtre d'opportunité entre la génération d'une nouvelle Identity
valeur et le verrouillage de la clé de ligne correspondante dans l'index clusterisé, où un observateur externe pourrait voir un plus récent 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 Identity
colonne appelée CheckpointSequence
, qui est la clé de l'index cluster de la table (qui possède également un certain nombre d'index non cluster supplémentaires). Les lignes sont insérées dans la table par plusieurs processus et threads simultanés (au niveau de l'isolement READ COMMITTED
et sans IDENTITY_INSERT
). En même temps, il existe des processus qui lisent périodiquement des lignes de l'index cluster, ordonnées par cette CheckpointSequence
colonne (également au niveau d'isolement READ COMMITTED
, l' READ COMMITTED SNAPSHOT
option étant 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 pourrais-je faire pour que ce 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 une ORDER BY CheckpointSequence
clause ( et une WHERE CheckpointSequence > -1
clause), 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 Identity
fonctionnement 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ément". 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:
- Une transaction est lancée (explicitement ou implicitement).
- Une valeur d'identité (X) est générée.
- 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).
- La ligne est insérée.
- 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 du 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 du magasin d'événements classés par point de contrôle. afin d'effectuer des calculs de toutes sortes. Une fois qu'un événement avec le 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 le point de contrôle X + 1 et plus. Par conséquent, il est essentiel que les événements ne puissent jamais être ignorés, car ils ne seraient plus jamais pris en compte. J'essaie actuellement de déterminer si l' Identity
implémentation de point de contrôle basée sur cette condition répond à cette exigence. Ce sont les instructions SQL exactes utilisées : schéma , requête du rédacteur ,Requête du lecteur .)
Si j'ai raison et que la situation décrite ci-dessus pourrait survenir, je ne vois que deux options pour y faire face, qui ne sont pas satisfaisantes:
- 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 cela
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?
la source
Réponses:
Oui.
L' allocation de valeurs d'identité est indépendante de la transaction utilisateur contenante . C'est l'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 rendue active (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ù:
Après l'étape 3, une requête utilisant row_number sous un verrouillage de lecture validé a renvoyé ce qui suit:
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 des fonctions T-SQL comme
IDENT_CURRENT
avant que la ligne existe dans la table (même non validée).Fondamentalement, il n'y a pas plus de garanties concernant les valeurs d'identité que celles documentées :
C'est vraiment ça.
Si un traitement FIFO transactionnel strict est requis, vous n'avez probablement pas d'autre choix que de sérialiser manuellement. Si l'application a moins d'exigences, vous avez plus d'options. La question n'est pas claire à 100% à cet égard. Néanmoins, vous pouvez trouver des informations utiles dans l'article de Remus Rusanu sur l' utilisation des tables comme files d'attente .
la source
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:
Effectuez des insertions et des sélections simultanées sur cette table dans un programme de console C #:
Cette console imprime une ligne pour chaque cas lorsqu'un des fils de lecture "manque" une entrée.
la source
IDENTITY
des écarts seraient générés (comme l'annulation d'une transaction), les lignes imprimées affichent en effet des valeurs "ignorées" (ou du moins, elles l'ont fait lorsque je l'ai exécuté et vérifié sur ma machine). Très bel échantillon de repro!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:
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
la source
ORDER BY CheckpointSequence
clause (qui se trouve être l'ordre de l'index clusterisé). Je pense que cela se résume à la question de savoir si la génération d'une valeur d'identité est liée de toute façon aux verrous pris par l'instruction INSERT, ou s'il s'agit simplement de deux actions indépendantes effectuées par SQL Server l'une après l'autre.SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence
. Je ne pense pas que cette requête se lirait au-delà de la ligne verrouillée 4, ou non? (Dans mes expériences, il bloque lorsque la requête essaie d'acquérir le verrou KEY pour la ligne 4.)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:
Dans le scénario ci-dessus, votre LAST_READ_ID sera 6, donc 5 ne sera jamais lu.
la source
Exécution de ce script:
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:
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 vous
SELECT...ORDER BY
commencez l'analyse avant les lignes nouvellement insérées souhaitées, je m'attendrais à ce que vous souhaitiez leREAD COMMITTED
niveau d'isolement par défaut tant que l'READ_COMMITTED_SNAPSHOT
option de base de données est désactivée.la source
RangeI_N
sont compatibles , c'est-à-dire qu'ils ne se bloquent pas (le verrou est principalement là pour bloquer sur un lecteur sérialisable existant).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:
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:
Qui a renvoyé le résultat suivant pour ma base de données:
(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
conformément aux 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
Requête 2
La requête suivante a été exécutée dans une fenêtre de requête avec le SPID 58
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:
Et voici les résultats:
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 eXlusive (IX) prévu sur l'objet, un verrou eXlusive (IX) prévu sur la page à insérer et un eXclusive 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. Étant donné que la deuxième requête ne peut que des
READ COMMITTED
données, elle attend soit le délai d'expiration, soit la validation de la transaction 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 a qu'un seul 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.
la source
Identity
valeur suivante et l'acquisition du verrou KEY sur la ligne (où les lectures / écritures simultanées pourraient tomber). Je ne pense pas que cela soit prouvé impossible par vos observations, car on ne peut pas arrêter l'exécution des requêtes et analyser les verrous pendant cette fenêtre de temps ultra-courte.