it-swarm.com.de

Kann ich mich darauf verlassen, dass die SQL Server-Identitätswerte der Reihe nach gelesen werden?

TL; DR: Die folgende Frage läuft darauf hinaus: Gibt es beim Einfügen einer Zeile ein Zeitfenster zwischen der Generation eines neuen Identity -Werts und des Locking des entsprechenden Zeilenschlüssels im Clustered-Index, wobei ein externer Beobachter einen neueren sehen konnte = Identity Wert, der durch eine gleichzeitige Transaktion eingefügt wird? (In SQL Server.)

Detaillierte Version

Ich habe eine SQL Server-Tabelle mit einer Identity -Spalte namens CheckpointSequence, die der Schlüssel des Clustered-Index der Tabelle ist (der auch eine Reihe zusätzlicher nicht gruppierter Indizes enthält). Zeilen werden eingefügt durch mehrere gleichzeitige Prozesse und Threads in die Tabelle eingefügt (auf Isolationsstufe READ COMMITTED Und ohne IDENTITY_INSERT). Gleichzeitig gibt es periodisch Prozesse Lesen Zeilen aus dem Clustered-Index, geordnet nach dieser CheckpointSequence -Spalte (ebenfalls auf Isolationsstufe READ COMMITTED, Mit dem READ COMMITTED SNAPSHOT Option deaktiviert).

Ich verlasse mich derzeit auf die Tatsache, dass die Lesevorgänge niemals einen Kontrollpunkt "überspringen" können. Meine Frage lautet: Kann ich mich auf diese Eigenschaft verlassen? Und wenn nicht, was könnte ich tun, um sie wahr werden zu lassen?

Beispiel: Wenn Zeilen mit den Identitätswerten 1, 2, 3, 4 und 5 eingefügt werden, muss der Leser darf nicht die Zeile mit dem Wert 5 anzeigen, bevor die Zeile mit dem Wert 4 angezeigt wird. Tests zeigen dass die Abfrage, die eine ORDER BY CheckpointSequence - Klausel (und eine WHERE CheckpointSequence > -1 - Klausel) enthält, zuverlässig blockiert, wenn Zeile 4 gelesen, aber noch nicht festgeschrieben werden soll, selbst wenn Zeile 5 bereits festgeschrieben wurde.

Ich glaube, dass zumindest theoretisch hier eine Rassenbedingung vorliegt, die dazu führen könnte, dass diese Annahme gebrochen wird. Leider sagt die Dokumentation zu Identity nicht viel darüber aus, wie Identity im Kontext mehrerer gleichzeitiger Transaktionen funktioniert, sondern nur "Jeder neue Wert wird basierend auf dem aktuellen Startwert und Inkrement generiert. "" und "Jeder neue Wert für eine bestimmte Transaktion unterscheidet sich von anderen gleichzeitigen Transaktionen in der Tabelle." ( MSDN )

Meine Argumentation ist, es muss irgendwie so funktionieren:

  1. Eine Transaktion wird gestartet (entweder explizit oder implizit).
  2. Ein Identitätswert (X) wird generiert.
  3. Die entsprechende Zeilensperre wird für den Clustered-Index basierend auf dem Identitätswert vorgenommen (es sei denn, die Sperreneskalation setzt ein. In diesem Fall ist die gesamte Tabelle gesperrt).
  4. Die Zeile wird eingefügt.
  5. Die Transaktion wird festgeschrieben (möglicherweise viel später), sodass die Sperre wieder aufgehoben wird.

Ich denke, dass zwischen Schritt 2 und 3 ein sehr kleines Fenster ist, 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 trotzdem - es könnte passieren. Oder könnte es?

(Wenn Sie sich für den Kontext interessieren: Dies ist die Implementierung von 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 nach Checkpoint geordneten Ereignisspeicher, um Berechnungen aller Art durchzuführen. Sobald ein Ereignis mit Checkpoint X verarbeitet wurde, berücksichtigen Clients nur "neuere" Ereignisse, dh Ereignisse mit Checkpoint X + 1 und höher. Daher ist dies von entscheidender Bedeutung Diese Ereignisse können niemals übersprungen werden, da sie nie wieder berücksichtigt werden. Ich versuche derzeit festzustellen, ob die auf Identity basierende Checkpoint-Implementierung diese Anforderung erfüllt. Dies sind die genaue verwendete SQL-Anweisungen : Schema , Writer's Query , Reader's Query .)

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 Identity natürlich Lücken erzeugen 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 sollte ich jedoch annehmen?

Irgendwelche besseren Ideen?

24
Fabian Schmied

Gibt es beim Einfügen einer Zeile ein Zeitfenster zwischen der Generierung eines neuen Identitätswerts und dem Sperren des entsprechenden Zeilenschlüssels im Clustered-Index, in dem ein externer Beobachter einen neueren Identitätswert sehen könnte, der durch eine gleichzeitige Transaktion eingefügt wird?

Ja.

Die Zuordnung von Identitätswerten ist unabhängig von der enthaltenen Benutzertransaktion. Dies ist ein Grund dafür, dass Identitätswerte auch dann verbraucht werden, wenn die Transaktion zurückgesetzt wird. Die Inkrementierungsoperation selbst ist durch einen Latch geschützt, um eine Beschädigung zu verhindern. Dies ist jedoch der Umfang der Schutzmaßnahmen.

Unter den besonderen Umständen Ihrer Implementierung erfolgt die Identitätszuweisung (ein Aufruf von CMEDSeqGen::GenerateNewValue) bevor die Benutzertransaktion für die Einfügung überhaupt aktiviert wird (und damit vor Sperren sind vergeben).

Durch gleichzeitiges Ausführen von zwei Einfügungen mit einem angehängten Debugger, damit ich einen Thread einfrieren kann, nachdem der Identitätswert erhöht und zugewiesen wurde, konnte ich ein Szenario reproduzieren, in dem:

  1. Sitzung 1 erhält einen Identitätswert (3)
  2. Sitzung 2 erhält einen Identitätswert (4)
  3. Sitzung 2 führt das Einfügen und Festschreiben durch (sodass Zeile 4 vollständig sichtbar ist).
  4. Sitzung 1 führt das Einfügen und Festschreiben durch (Zeile 3).

Nach Schritt 3 gab eine Abfrage mit row_number unter Locking read commit Folgendes zurück:

(Screenshot

In Ihrer Implementierung würde dies dazu führen, dass Checkpoint ID 3 falsch übersprungen wird.

Das Fenster der Fehlgelegenheit ist relativ klein, aber es existiert. So geben Sie ein realistischeres Szenario an als das Anhängen eines Debuggers: 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.

Aus Gründen der Übersichtlichkeit gibt es keine Sperren oder andere Synchronisationsobjekte, die den Identitätswert nach seiner Zuweisung und vor seiner Verwendung schützen. Beispielsweise kann nach Schritt 1 oben eine gleichzeitige Transaktion den neuen Identitätswert mithilfe von T-SQL-Funktionen wie IDENT_CURRENT Anzeigen, bevor die Zeile in der Tabelle vorhanden ist (auch nicht festgeschrieben).

Grundsätzlich gibt es nicht mehr Garantien für Identitätswerte als dokumentiert :

  • Jeder neue Wert wird basierend auf dem aktuellen Startwert und Inkrement generiert.
  • Jeder neue Wert für eine bestimmte Transaktion unterscheidet sich von anderen gleichzeitigen Transaktionen in der Tabelle.

Das ist es wirklich.

Wenn streng transaktional FIFO Verarbeitung erforderlich ist, haben Sie wahrscheinlich keine andere Wahl, als manuell zu serialisieren. Wenn die Anwendung weniger einmalige Anforderungen hat, haben Sie mehr Optionen Die Frage ist in dieser Hinsicht nicht 100% klar. Dennoch finden Sie möglicherweise einige nützliche Informationen in Remus Rusanus Artikel Verwenden von Tabellen als Warteschlangen .

26
Paul White 9

Da Paul White absolut richtig antwortete, besteht die Möglichkeit, Identitätszeilen vorübergehend zu "überspringen". Hier ist nur ein kleiner Code, um diesen Fall für sich selbst zu reproduzieren.

Erstellen Sie eine Datenbank und eine Testtabelle:

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)

Führen Sie in einem C # -Konsolenprogramm gleichzeitige Einfügungen und Auswahlen für diese Tabelle durch:

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();
                }
            }
        }
    }
}

Diese Konsole druckt eine Zeile für jeden Fall, in dem einer der Lesethreads einen Eintrag "verfehlt".

7
Stefan Kainz

Es ist am besten, nicht zu erwarten, dass die Identitäten aufeinander folgen, da es viele Szenarien gibt, die Lücken hinterlassen können. Es ist besser, die Identität als abstrakte Zahl zu betrachten und ihr keine geschäftliche Bedeutung zuzuweisen.

Grundsätzlich können Lücken auftreten, wenn Sie INSERT-Operationen zurücksetzen (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:

  1. Datensätze werden gelöscht.
  2. Beim Versuch, einen neuen Datensatz einzufügen, ist ein Fehler aufgetreten (Rollback).
  3. Ein Update/Insert mit explizitem Wert (Option identity_insert).
  4. Der inkrementelle Wert ist größer als 1.
  5. Eine Transaktion wird zurückgesetzt.

Die Identitätseigenschaft einer Spalte hat niemals garantiert:

• Einzigartigkeit

• Aufeinanderfolgende Werte innerhalb einer Transaktion. Wenn die Werte aufeinanderfolgend sein müssen, sollte die Transaktion eine exklusive Sperre für die Tabelle 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 Tabellen- und Nummernzuweisung 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

5
stacylaray

Ich vermute, dass dies gelegentlich zu Problemen führen kann, die sich verschlimmern, wenn der Server stark ausgelastet ist. Betrachten Sie zwei Transaktionen:

  1. T1: In T einfügen ... - sagen wir, 5 werden eingefügt
  2. T2: In T einfügen ... - sagen wir, 6 werden eingefügt
  3. T2: Festschreiben
  4. Der Leser sieht 6, aber nicht 5
  5. T1: Festschreiben

Im obigen Szenario ist Ihre LAST_READ_ID 6, sodass 5 niemals gelesen wird.

1
Lennart

Ausführen dieses Skripts:

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

Nachfolgend sind die Sperren aufgeführt, die von einer erweiterten Ereignissitzung erfasst und freigegeben wurden:

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      

Beachten Sie die RI_N KEY-Sperre, die unmittelbar vor der X-Tastensperre für die neu erstellte Zeile erworben wurde. Diese kurzlebige Bereichsverriegelung 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 erfasst wird.

Solange dein SELECT...ORDER BY startet den Scan vor den gewünschten neu eingefügten Zeilen, ich würde das gewünschte Verhalten in der Standardeinstellung erwarten READ COMMITTED Isolationsstufe solange die Datenbank READ_COMMITTED_SNAPSHOT Option ist deaktiviert.

0
Dan Guzman

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, fehlt in Ihrer Spalte eine ID.

Grundlegende Einstellung

Datenbanktabelle

Ich habe eine Datenbanktabelle mit folgender Struktur erstellt:

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

Datenbankisolationsstufe

Ich habe die Isolationsstufe meiner Datenbank mit der folgenden Anweisung überprüft:

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

Welches ergab das folgende Ergebnis für meine Datenbank:

snapshot_isolation_state    snapshot_isolation_state_desc   is_read_committed_snapshot_on
0                           OFF                             0

(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

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]
*/

Abfrage 2

Die folgende Abfrage wurde in einem Abfragefenster mit der SPID 58 ausgeführt

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

Die Abfrage wird nicht abgeschlossen und wartet darauf, dass die eXclusive-Sperre auf einer SEITE freigegeben wird.

Skript zur Bestimmung der Sperrung

Dieses Skript zeigt die Sperrung der Datenbankobjekte für die beiden Transaktionen an:

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)

Und hier sind die Ergebnisse:

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

Die Ergebnisse zeigen, dass das Abfragefenster 1 (SPID 57) eine gemeinsame Sperre (S) für die DATENBANK, eine beabsichtigte eXlusive (IX) -Sperre für das OBJECT, eine beabsichtigte eXlusive (IX) -Sperre für die Seite, in die es eingefügt werden soll, und eine eXclusive-Sperre aufweist Sperre (X) für den KEY, den er eingefügt, aber noch nicht festgeschrieben hat.

Aufgrund der nicht festgeschriebenen Daten verfügt die zweite Abfrage (SPID 58) über eine gemeinsame Sperre (S) auf DATABASE-Ebene, eine beabsichtigte gemeinsame Sperre (IS) für das OBJEKT und eine beabsichtigte gemeinsame Sperre (IS) auf der Seite eine gemeinsame Sperre (S) ) Sperren Sie den KEY mit dem Anforderungsstatus WAIT.

Zusammenfassung

Die Abfrage im ersten Abfragefenster wird ohne Festschreiben ausgeführt. Da die zweite Abfrage nur READ COMMITTED Daten kann, wartet sie entweder bis zum Timeout oder bis die Transaktion in der ersten Abfrage festgeschrieben wurde.

Nach meinem Verständnis ist dies das Standardverhalten von Microsoft SQL Server.

Sie sollten beachten, dass die ID tatsächlich für nachfolgende Lesevorgänge durch SELECT-Anweisungen in der Reihenfolge ist, wenn die erste Anweisung COMMITs.

Wenn die erste Anweisung ein ROLLBACK ausführt, finden Sie eine fehlende ID in der Sequenz, jedoch immer noch mit der ID in aufsteigender Reihenfolge (vorausgesetzt, Sie haben den INDEX mit der Standard- oder ASC-Option in der ID-Spalte erstellt).

Update:

(Offen) Ja, Sie können sich darauf verlassen, dass die Identitätsspalte ordnungsgemäß funktioniert, bis Sie auf ein Problem stoßen. Es gibt nur ein 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.

0