TL; DR: Die folgende Frage lautet: Gibt es beim Einfügen einer Zeile ein Zeitfenster zwischen der Generierung eines neuen Identity
Werts und dem Sperren des entsprechenden Zeilenschlüssels im Clustered-Index, in dem ein externer Beobachter einen neueren sehen könnte Identity
Wert, der von einer gleichzeitigen Transaktion eingefügt wurde? (In SQL Server.)
Ausführliche Version
Ich habe eine SQL Server-Tabelle mit einer Identity
Spalte namens CheckpointSequence
, die den Schlüssel des Clustered-Index der Tabelle darstellt (der auch eine Reihe zusätzlicher Nonclustered-Indizes enthält). Zeilen werden von mehreren gleichzeitig ablaufenden Prozessen und Threads (auf Isolationsstufe und ohne ) in die Tabelle eingefügt . Gleichzeitig werden in regelmäßigen Abständen Zeilen aus dem Clustered-Index gelesen , die nach dieser Spalte geordnet sind (ebenfalls auf Isolationsstufe , wobei die Option deaktiviert ist).READ COMMITTED
IDENTITY_INSERT
CheckpointSequence
READ COMMITTED
READ COMMITTED SNAPSHOT
Ich verlasse mich derzeit auf die Tatsache, dass der Lesevorgang niemals einen Checkpoint "überspringen" kann. Meine Frage ist: Kann ich mich auf diese Immobilie verlassen? Und wenn nicht, was könnte ich tun, um es wahr zu machen?
Beispiel: Wenn Zeilen mit den Identitätswerten 1, 2, 3, 4 und 5 eingefügt werden, darf der Leser die Zeile mit dem Wert 5 nicht sehen, bevor die Zeile mit dem Wert 4 angezeigt wird. Tests zeigen, dass die Abfrage, die eine ORDER BY CheckpointSequence
Klausel enthält ( und eine WHERE CheckpointSequence > -1
Klausel) blockiert zuverlässig, wann immer Zeile 4 gelesen, aber noch nicht festgeschrieben werden soll, selbst wenn Zeile 5 bereits festgeschrieben wurde.
Ich glaube, dass zumindest theoretisch eine Race Condition vorliegt, die dazu führen könnte, dass diese Annahme gebrochen wird. Leider Identity
sagt die Dokumentation zu nicht viel darüber aus, wie Identity
im Kontext mehrerer gleichzeitiger Transaktionen gearbeitet wird. Sie sagt nur "Jeder neue Wert wird basierend auf dem aktuellen Startwert und dem aktuellen Inkrement generiert." und "Jeder neue Wert für eine bestimmte Transaktion unterscheidet sich von anderen gleichzeitigen Transaktionen in der Tabelle." ( MSDN )
Meine Überlegung ist, es muss irgendwie so funktionieren:
- Eine Transaktion wird gestartet (entweder explizit oder implizit).
- Ein Identitätswert (X) wird generiert.
- Die entsprechende Zeilensperre wird für den Clustered-Index basierend auf dem Identitätswert verwendet (es sei denn, die Sperreneskalation wird aktiviert. In diesem Fall ist die gesamte Tabelle gesperrt).
- Die Zeile wird eingefügt.
- Die Transaktion wird festgeschrieben (möglicherweise viel Zeit später), sodass die Sperre wieder aufgehoben wird.
Ich denke, zwischen Schritt 2 und 3 gibt es ein sehr kleines Fenster, in dem
- Eine gleichzeitige Sitzung könnte den nächsten Identitätswert (X + 1) generieren und alle verbleibenden Schritte ausführen.
- Auf diese Weise kann ein Leser, der genau zu diesem Zeitpunkt kommt, den Wert X + 1 lesen, wobei der Wert von X fehlt.
Natürlich scheint die Wahrscheinlichkeit dafür äußerst gering zu sein; aber dennoch - es könnte passieren. Oder könnte es?
(Wenn Sie sich für den Kontext interessieren: Dies ist die Implementierung der SQL Persistence Engine von NEventStore . NEventStore implementiert einen Nur-Anhängen-Ereignisspeicher, in dem jedes Ereignis eine neue aufsteigende Prüfpunktsequenznummer erhält. Clients lesen Ereignisse aus dem Ereignisspeicher, sortiert nach Prüfpunkt Wenn ein Ereignis mit Checkpoint X verarbeitet wurde, berücksichtigen Clients nur "neuere" Ereignisse, dh Ereignisse mit Checkpoint X + 1 und höher. Daher ist es wichtig, dass Ereignisse niemals übersprungen werden. Ich versuche derzeit festzustellen, ob die auf Identity
-basierende Checkpoint-Implementierung diese Anforderung erfüllt. Dies sind die genauen verwendeten SQL-Anweisungen : Schema , Writer-Abfrage ,Leseranfrage .)
Wenn ich recht habe und die oben beschriebene Situation eintreten könnte, sehe ich nur zwei Möglichkeiten, mit ihnen umzugehen, die beide unbefriedigend sind:
- Wenn Sie einen Prüfpunktsequenzwert X + 1 sehen, bevor Sie X gesehen haben, schließen Sie X + 1 und versuchen Sie es später erneut. Da es
Identity
jedoch natürlich zu Lücken kommen kann (z. B. wenn die Transaktion zurückgesetzt wird), kommt X möglicherweise nie. - Also der gleiche Ansatz, aber akzeptiere die Lücke nach n Millisekunden. Welchen Wert von n soll ich jedoch annehmen?
Irgendwelche besseren Ideen?
quelle
Antworten:
Ja.
Die Vergabe von Identitätswerten ist unabhängig von der enthaltenen Benutzertransaktion . Dies ist ein Grund dafür, dass Identitätswerte auch dann verwendet werden, wenn die Transaktion zurückgesetzt wird. Die Inkrementierungsoperation selbst ist durch einen Zwischenspeicher geschützt, um eine Beschädigung zu verhindern. Dies ist jedoch der Umfang der Schutzfunktionen.
Unter bestimmten Umständen Ihrer Implementierung wird die Identitätszuweisung (ein Aufruf von
CMEDSeqGen::GenerateNewValue
) vorgenommen, bevor die Benutzertransaktion für die Einfügung überhaupt aktiviert wird (und bevor Sperren aufgehoben werden).Indem ich zwei Einfügungen gleichzeitig mit einem angehängten Debugger ausführte, um einen Thread einzufrieren, nachdem der Identitätswert erhöht und zugewiesen wurde, konnte ich ein Szenario reproduzieren, in dem:
Nach Schritt 3 ergab eine Abfrage unter Verwendung von row_number unter Festschreiben des Lesezugriffs Folgendes:
In Ihrer Implementierung würde dies dazu führen, dass Checkpoint ID 3 falsch übersprungen wird.
Das Fenster der Missmöglichkeit ist relativ klein, aber es existiert. So geben Sie ein realistischeres Szenario an, als wenn ein Debugger angehängt wäre: Ein ausführender Abfragethread kann den Scheduler nach Schritt 1 oben liefern. Auf diese Weise kann ein zweiter Thread einen Identitätswert zuweisen, einfügen und festschreiben, bevor der ursprüngliche Thread seine Einfügung fortsetzt.
Der Übersichtlichkeit halber gibt es keine Sperren oder anderen Synchronisationsobjekte, die den Identitätswert schützen, nachdem er zugewiesen wurde und bevor er verwendet wird. Beispielsweise kann nach Schritt 1 oben eine gleichzeitige Transaktion den neuen Identitätswert mithilfe von T-SQL-Funktionen anzeigen, wie
IDENT_CURRENT
vor dem Vorhandensein der Zeile in der Tabelle (auch ohne Commit).Grundsätzlich gibt es nicht mehr Garantien für Identitätswerte als dokumentiert :
Das ist es wirklich.
Wenn eine strikte Transaktions-FIFO-Verarbeitung erforderlich ist, müssen Sie wahrscheinlich manuell serialisieren. Wenn für die Anwendung weniger Anforderungen gelten, stehen Ihnen mehr Optionen zur Verfügung. Die Frage ist in dieser Hinsicht nicht zu 100% klar. Trotzdem finden Sie einige nützliche Informationen in Remus Rusanus Artikel Verwenden von Tabellen als Warteschlangen .
quelle
Da Paul White absolut richtig geantwortet hat, besteht die Möglichkeit, vorübergehend Identitätszeilen zu "überspringen". Hier ist nur ein kleiner Code, mit dem Sie diesen Fall für sich selbst reproduzieren können.
Erstellen Sie eine Datenbank und eine Testtabelle:
Führen Sie gleichzeitige Einfügungen und Auswahlen für diese Tabelle in einem C # -Konsolenprogramm durch:
Diese Konsole gibt für jeden Fall eine Zeile aus, wenn einer der Lesethreads einen Eintrag "verfehlt".
quelle
IDENTITY
, in dem Lücken entstehen (z. B. das Zurücksetzen einer Transaktion), werden in den gedruckten Zeilen tatsächlich "übersprungene" Werte angezeigt (oder zumindest, als ich sie auf meinem Computer ausgeführt und überprüft habe). Sehr schönes Repro-Muster!Es ist am besten, nicht zu erwarten, dass die Identitäten aufeinander folgen, da es viele Szenarien gibt, die Lücken lassen können. Es ist besser, die Identität als abstrakte Zahl zu betrachten und ihr keine geschäftliche Bedeutung beizumessen.
Grundsätzlich können Lücken auftreten, wenn Sie INSERT-Vorgänge rückgängig machen (oder Zeilen explizit löschen), und Duplikate können auftreten, wenn Sie die Tabelleneigenschaft IDENTITY_INSERT auf ON setzen.
Lücken können auftreten, wenn:
Die Identitätseigenschaft für eine Spalte hat nie garantiert:
• Einzigartigkeit
• Aufeinanderfolgende Werte innerhalb einer Transaktion. Wenn die Werte aufeinander folgen müssen, sollte die Transaktion eine exklusive Sperre für die Tabelle verwenden oder die Isolationsstufe SERIALIZABLE verwenden.
• Aufeinanderfolgende Werte nach dem Neustart des Servers.
• Wiederverwendung von Werten.
Wenn Sie aus diesem Grund keine Identitätswerte verwenden können, erstellen Sie eine separate Tabelle mit einem aktuellen Wert und verwalten Sie den Zugriff auf die Tabelle und die Nummernvergabe mit Ihrer Anwendung. Dies kann die Leistung beeinträchtigen.
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx
quelle
ORDER BY CheckpointSequence
Klausel (die die Reihenfolge des gruppierten Index sein passiert). Ich denke, es läuft auf die Frage hinaus, ob die Generierung eines Identitätswerts in irgendeiner Weise mit den von der INSERT-Anweisung ausgeführten Sperren zusammenhängt oder ob es sich einfach um zwei voneinander unabhängige Aktionen handelt, die von SQL Server nacheinander ausgeführt werden.SELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequence
. Ich glaube nicht, dass diese Abfrage die gesperrte Zeile 4 überschreitet, oder? (In meinen Experimenten wird es blockiert, wenn die Abfrage versucht, die KEY-Sperre für Zeile 4 abzurufen.)Ich vermute, dass dies gelegentlich zu Problemen führen kann, die sich verschlimmern, wenn der Server stark ausgelastet ist. Betrachten Sie zwei Transaktionen:
Im obigen Szenario ist Ihre LAST_READ_ID 6, daher werden 5 niemals gelesen.
quelle
Ausführen dieses Skripts:
Nachfolgend sind die Sperren aufgeführt, die von einer erweiterten Ereignissitzung erfasst und freigegeben wurden:
Beachten Sie die RI_N KEY-Sperre, die unmittelbar vor der X-Tastensperre für die neu erstellte Zeile erworben wurde. Diese kurzlebige Bereichssperre verhindert, dass eine gleichzeitige Einfügung eine andere RI_N KEY-Sperre erhält, da RI_N-Sperren nicht kompatibel sind. Das Fenster, das Sie zwischen den Schritten 2 und 3 erwähnt haben, ist kein Problem, da die Bereichssperre vor der Zeilensperre für den neu generierten Schlüssel aktiviert wird.
Solange
SELECT...ORDER BY
der Scan vor den gewünschten neu eingefügten Zeilen beginnt, würde ich das gewünschte Verhalten in der Standardisolationsstufe erwarten,READ COMMITTED
solange die DatenbankoptionREAD_COMMITTED_SNAPSHOT
deaktiviert ist.quelle
RangeI_N
sind kompatibel , dh blockieren sie nicht (die Sperre meist gibt es auf einem vorhandenen serializable Leser zum Blockieren).Nach meinem Verständnis von SQL Server zeigt die zweite Abfrage standardmäßig keine Ergebnisse an, bis die erste Abfrage festgeschrieben wurde. Wenn die erste Abfrage ein ROLLBACK anstelle eines COMMIT ausführt, wird in Ihrer Spalte eine fehlende ID angezeigt.
Basiseinstellung
Datenbanktabelle
Ich habe eine Datenbanktabelle mit folgender Struktur angelegt:
Isolationsstufe der Datenbank
Ich habe die Isolationsstufe meiner Datenbank mit der folgenden Anweisung überprüft:
Welches das folgende Ergebnis für meine Datenbank zurückbrachte:
(Dies ist die Standardeinstellung für eine Datenbank in SQL Server 2012)
Testskripte
Die folgenden Skripts wurden mit den Standardeinstellungen für SQL Server-SSMS-Clients und den Standardeinstellungen für SQL Server ausgeführt.
Einstellungen für Clientverbindungen
Der Client wurde so eingestellt, dass er die Transaktionsisolationsstufe
READ COMMITTED
gemäß den Abfrageoptionen in SSMS verwendet.Abfrage 1
Die folgende Abfrage wurde in einem Abfragefenster mit der SPID 57 ausgeführt
Abfrage 2
Die folgende Abfrage wurde in einem Abfragefenster mit der SPID 58 ausgeführt
Die Abfrage wird nicht abgeschlossen und wartet auf die Freigabe der eXclusive-Sperre auf einer SEITE.
Skript zum Ermitteln der Sperrung
Dieses Skript zeigt die Sperrung der Datenbankobjekte für die beiden Transaktionen an:
Und hier sind die Ergebnisse:
Die Ergebnisse zeigen, dass das Abfragefenster eins (SPID 57) eine gemeinsame Sperre (S) für die DATABASE, eine beabsichtigte eXlusive-Sperre (IX) für das OBJECT, eine beabsichtigte eXlusive-Sperre (IX) für die PAGE, in die es eingefügt werden soll, und eine exklusive Sperre hat sperren Sie (X) für den KEY, den es eingefügt hat, aber noch nicht festgeschrieben hat.
Aufgrund der nicht festgeschriebenen Daten verfügt die zweite Abfrage (SPID 58) über eine gemeinsame Sperre (S) auf der Ebene DATABASE, eine beabsichtigte gemeinsame Sperre (IS) für das OBJECT und eine beabsichtigte gemeinsame Sperre (IS) auf der Seite a Shared (S) ) den SCHLÜSSEL mit dem Anforderungsstatus WARTEN abschließen.
Zusammenfassung
Die Abfrage im ersten Abfragefenster wird ohne Commit ausgeführt. Da die zweite Abfrage nur
READ COMMITTED
Daten enthalten kann, wartet sie entweder, bis das Timeout auftritt oder bis die Transaktion in der ersten Abfrage festgeschrieben wurde.Dies ist meines Erachtens das Standardverhalten von Microsoft SQL Server.
Sie sollten beachten, dass die ID für nachfolgende Lesevorgänge durch SELECT-Anweisungen in der Tat in der richtigen Reihenfolge ist, wenn die erste Anweisung COMMITs ausführt.
Wenn die erste Anweisung einen ROLLBACK ausführt, finden Sie in der Sequenz eine fehlende ID, die jedoch immer noch aufsteigend sortiert ist (vorausgesetzt, Sie haben den INDEX mit der Standard- oder ASC-Option in der ID-Spalte erstellt).
Aktualisieren:
(Offen gesagt) Ja, Sie können sich darauf verlassen, dass die Identitätsspalte ordnungsgemäß funktioniert, bis Sie auf ein Problem stoßen. Es gibt nur einen HOTFIX in Bezug auf SQL Server 2000 und die Identitätsspalte auf der Microsoft-Website.
Wenn Sie sich nicht darauf verlassen können, dass die Identitätsspalte korrekt aktualisiert wird, gibt es meiner Meinung nach mehr Hotfixes oder Patches auf der Microsoft-Website.
Wenn Sie einen Microsoft-Supportvertrag haben, können Sie jederzeit einen Beratungsfall eröffnen und zusätzliche Informationen anfordern.
quelle
Identity
Werts und dem Erwerb der KEY-Sperre für die Zeile gibt (in die gleichzeitige Lese- / Schreibvorgänge fallen könnten). Ich denke nicht, dass dies durch Ihre Beobachtungen als unmöglich erwiesen ist, da man die Ausführung von Abfragen und die Analyse von Sperren in diesem extrem kurzen Zeitfenster nicht stoppen kann.