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:
Je pense qu'entre les étapes 2 et 3, il y a une toute petite fenêtre où
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:
Identity
peut bien sûr produire des lacunes (par exemple, lorsque la transaction est annulée), X pourrait ne jamais arriver.De meilleures idées?
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ù:
Après l'étape 3, une requête utilisant row_number sous verrouillage en lecture validée 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 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é :
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 .
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.
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
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.
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.
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.
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)
)
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)
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.
Le client a été configuré pour utiliser le niveau d'isolation de transaction READ COMMITTED
selon les options de requête dans SSMS.
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]
*/
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.
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.
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.