Strategien zum „Auschecken“ von Datensätzen zur Verarbeitung

10

Ich bin mir nicht sicher, ob es dafür ein benanntes Muster gibt oder nicht, weil es eine schreckliche Idee ist. Ich benötige meinen Dienst jedoch, um in einer aktiven / aktiven Umgebung mit Lastenausgleich zu arbeiten. Dies ist nur der Anwendungsserver. Die Datenbank befindet sich auf einem separaten Server. Ich habe einen Dienst, der für jeden Datensatz in einer Tabelle einen Prozess durchlaufen muss. Dieser Vorgang kann ein oder zwei Minuten dauern und wird alle n Minuten wiederholt (konfigurierbar, normalerweise 15 Minuten).

Mit einer Tabelle mit 1000 Datensätzen, die diese Verarbeitung benötigen, und zwei Diensten, die für denselben Datensatz ausgeführt werden, möchte ich, dass jeder Dienst einen zu verarbeitenden Datensatz "auscheckt". Ich muss sicherstellen, dass jeweils nur ein Dienst / Thread jeden Datensatz verarbeitet.

Ich habe Kollegen, die in der Vergangenheit eine "Sperrtabelle" verwendet haben. Wenn ein Datensatz in diese Tabelle geschrieben wird, um den Datensatz in der anderen Tabelle logisch zu sperren (diese andere Tabelle ist übrigens ziemlich statisch und mit einem sehr gelegentlichen neuen Datensatz), wird er gelöscht, um die Sperre aufzuheben.

Ich frage mich, ob es nicht besser wäre, wenn die neue Tabelle eine Spalte hätte, die angibt, wann sie gesperrt war und ob sie derzeit gesperrt ist, anstatt ständig ein Löschen einzufügen.

Hat jemand einen Tipp für so etwas? Gibt es ein etabliertes Muster für logisches Langzeitsperren? Gibt es Tipps, wie Sie sicherstellen können, dass jeweils nur ein Dienst das Schloss greift? (Mein Kollege verwendet TABLOCKX, um die gesamte Tabelle zu sperren.)

Dean
quelle

Antworten:

12

Ich bin weder ein großer Fan des zusätzlichen "Lock" -Tisches noch der Idee, den gesamten Tisch zu sperren, um die nächste Platte zu holen. Ich verstehe, warum dies getan wird, aber das schadet auch der Parallelität für Vorgänge, die aktualisiert werden, um einen gesperrten Datensatz freizugeben (sicherlich können zwei Prozesse nicht darüber streiten, wenn es nicht möglich ist, dass zwei Prozesse denselben Datensatz am gesperrt haben gleiche Zeit).

Ich würde es vorziehen, der Tabelle eine ProcessStatusID-Spalte (normalerweise TINYINT) hinzuzufügen, in der die Daten verarbeitet werden. Und gibt es ein Feld für LastModifiedDate? Wenn nicht, sollte es hinzugefügt werden. Wenn ja, werden diese Datensätze dann außerhalb dieser Verarbeitung aktualisiert? Wenn Datensätze außerhalb dieses bestimmten Prozesses aktualisiert werden können, sollte ein weiteres Feld hinzugefügt werden, um StatusModifiedDate (oder ähnliches) zu verfolgen. Für den Rest dieser Antwort werde ich nur "StatusModifiedDate" verwenden, da dies in seiner Bedeutung klar ist (und tatsächlich als Feldname verwendet werden kann, selbst wenn derzeit kein "LastModifiedDate" -Feld vorhanden ist).

Die Werte für ProcessStatusID (die in eine neue Nachschlagetabelle mit dem Namen "ProcessStatus" und Fremdschlüssel für diese Tabelle eingefügt werden sollten) können sein:

  1. Abgeschlossen (oder in diesem Fall sogar "Ausstehend", da beide "zur Verarbeitung bereit" bedeuten)
  2. In Bearbeitung (oder "Verarbeitung")
  3. Fehler (oder "WTF?")

An diesem Punkt scheint es sicher anzunehmen, dass die Anwendung nur den nächsten zu verarbeitenden Datensatz abrufen möchte und nichts weitergibt, um diese Entscheidung zu treffen. Wir möchten also den ältesten Datensatz (zumindest in Bezug auf StatusModifiedDate) abrufen, der auf "Abgeschlossen" / "Ausstehend" gesetzt ist. Etwas in der Art von:

SELECT TOP 1 pt.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1
ORDER BY pt.StatusModifiedDate ASC;

Wir möchten diesen Datensatz gleichzeitig auf "In Bearbeitung" aktualisieren, um zu verhindern, dass der andere Prozess ihn abruft. Wir könnten die OUTPUTKlausel verwenden, um UPDATE und SELECT in derselben Transaktion ausführen zu lassen:

UPDATE TOP (1) pt
SET    pt.StatusID = 2,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID
FROM   ProcessTable pt
WHERE  pt.StatusID = 1;

Das Hauptproblem hierbei ist, dass wir zwar eine Operation ausführen können, es jedoch keine Möglichkeit gibt, TOP (1)eine UPDATEOperation durchzuführen ORDER BY. Wir können es jedoch in einen CTE einwickeln, um diese beiden Konzepte zu kombinieren:

;WITH cte AS
(
   SELECT TOP 1 pt.RecordID
   FROM   ProcessTable pt (READPAST, ROWLOCK, UPDLOCK)
   WHERE  pt.StatusID = 1
   ORDER BY pt.StatusModifiedDate ASC;
)
UPDATE cte
SET    cte.StatusID = 2,
       cte.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
OUTPUT INSERTED.RecordID;

Die offensichtliche Frage ist, ob zwei Prozesse, die gleichzeitig SELECT ausführen, denselben Datensatz abrufen können. Ich bin mir ziemlich sicher, dass die UPDATE with OUTPUT-Klausel, insbesondere in Kombination mit den READPAST- und UPDLOCK-Hinweisen (siehe unten für weitere Details), in Ordnung sein wird. Ich habe dieses genaue Szenario jedoch nicht getestet. Wenn sich die obige Abfrage aus irgendeinem Grund nicht um die Race-Bedingung kümmert, wird Folgendes hinzugefügt: Anwendungssperren.

Die obige CTE-Abfrage kann in sp_getapplock und sp_releaseapplock eingeschlossen werden , um einen "Gate Keeper" für den Prozess zu erstellen. Dabei kann jeweils nur ein Prozess eingegeben werden, um die obige Abfrage auszuführen. Die anderen Prozesse werden blockiert, bis der Prozess mit dem Applock sie freigibt. Und da dieser Schritt des Gesamtprozesses nur darin besteht, die RecordID abzurufen, ist er ziemlich schnell und blockiert die anderen Prozesse nicht sehr lange. Und genau wie bei der CTE-Abfrage blockieren wir nicht die gesamte Tabelle, wodurch andere Aktualisierungen für andere Zeilen zugelassen werden (um deren Status entweder auf "Abgeschlossen" oder "Fehler" zu setzen). Im Wesentlichen:

BEGIN TRANSACTION;
EXEC sp_getapplock @Resource = 'GetNextRecordToProcess', @LockMode = 'Exclusive';

   {CTE UPDATE query shown above}

EXEC sp_releaseapplock @Resource = 'GetNextRecordToProcess';
COMMIT TRANSACTION;

Anwendungssperren sind sehr schön, sollten aber sparsam verwendet werden.

Zuletzt benötigen Sie nur eine gespeicherte Prozedur, um den Status auf "Abgeschlossen" oder "Fehler" zu setzen. Und das kann einfach sein:

CREATE PROCEDURE ProcessTable_SetProcessStatusID
(
   @RecordID INT,
   @ProcessStatusID TINYINT
)
AS
SET NOCOUNT ON;

UPDATE pt
SET    pt.ProcessStatusID = @ProcessStatusID,
       pt.StatusModifiedDate = GETDATE() -- or GETUTCDATE()
FROM   ProcessTable pt
WHERE  pt.RecordID = @RecordID;

Tabellenhinweise (gefunden unter Hinweise (Transact-SQL) - Tabelle ):

  • READPAST (scheint genau in dieses Szenario zu passen)

    Gibt an, dass das Datenbankmodul keine Zeilen liest, die von anderen Transaktionen gesperrt wurden. Wenn READPAST angegeben ist, werden Sperren auf Zeilenebene übersprungen. Das heißt, das Datenbankmodul überspringt die Zeilen, anstatt die aktuelle Transaktion zu blockieren, bis die Sperren aufgehoben werden ... READPAST wird hauptsächlich verwendet, um Sperrkonflikte bei der Implementierung einer Arbeitswarteschlange zu reduzieren, die eine SQL Server-Tabelle verwendet. Ein Warteschlangenleser, der READPAST verwendet, überspringt Warteschlangeneinträge, die von anderen Transaktionen gesperrt wurden, zum nächsten verfügbaren Warteschlangeneintrag, ohne warten zu müssen, bis die anderen Transaktionen ihre Sperren aufheben.

  • ROWLOCK (nur um sicher zu gehen)

    Gibt an, dass Zeilensperren verwendet werden, wenn Seiten- oder Tabellensperren normalerweise verwendet werden.

  • UPDLOCK

    Gibt an, dass Aktualisierungssperren genommen und gehalten werden sollen, bis die Transaktion abgeschlossen ist. UPDLOCK verwendet Aktualisierungssperren für Lesevorgänge nur auf Zeilen- oder Seitenebene.

Solomon Rutzky
quelle
1

Ähnliches (ohne Anwendungen, nur innerhalb der Datenbank) mit Service Broker-Warteschlangen. Leicht, vollständig ACID-konform, kann nahezu unbegrenzt skaliert werden. Eine transparente Zeilensperre (oder eher ein "Verstecken") ist integriert. Verfügbar ab Version 2005.

In Ihrem Fall könnte die Gesamtarchitektur folgendermaßen aussehen: Einige Prozesse senden Nachrichten gemäß ihren Zeitplänen in Service Broker-Dialoge, und Listener holen sie aus der Warteschlange auf der Zielseite ab. Abgesehen davon, dass Sie separate Nachrichtentypen erstellen, können Sie so ziemlich alles in den Nachrichtentext aufnehmen - beispielsweise das Zeitlimit und alle Parameter, die die Aufgabe möglicherweise hat.

Das ist sicher nicht so einfach zu verstehen, aber sobald Sie es bekommen, werden seine Vorteile offensichtlich.

Roger Wolf
quelle