Zum Hauptinhalt springen

Transaktionen

Garnet unterstützt zwei Arten von Transaktionen

  1. Benutzerdefinierte serverseitige Transaktionen
  2. Clientgesteuerte Transaktionen (Redis)

Benutzerdefinierte serverseitige Transaktionen

Benutzerdefinierte Transaktionen ermöglichen das Hinzufügen einer neuen Transaktion und deren Registrierung bei Garnet auf der Serverseite. Diese registrierte Transaktion kann dann von jedem Garnet-Client aufgerufen werden, um eine Transaktion auf dem Garnet-Server auszuführen. Lesen Sie mehr über die Entwicklung benutzerdefinierter serverseitiger Transaktionen auf der Transaktionsseite im Abschnitt Erweiterungen.

Clientgesteuerte Transaktionen (Redis)

Hier können Sie mehr lesen: Redis-Transaktionen. In diesem Design kommen Transaktionsoperationen in einem MULTI/EXEC-Bereich. Jede Operation in diesem Bereich ist Teil der Transaktion. Das Modell erlaubt es Ihnen nicht, das Ergebnis von Lesevorgängen innerhalb des MULTI/EXEC-Bereichs zu verwenden, erlaubt Ihnen aber, Schlüssel vor (d.h. WATCH) zu lesen und zu überwachen, und wenn sie zur Ausführungszeit unverändert sind, wird die Transaktion committet.

Beispiel

WATCH mykey
val = GET mykey
val = val + 1 # not Redis command this happens outside
MULTI
SET mykey $val
EXEC

Im obigen Beispiel, wenn mykey sich vor dem Befehl EXEC ändert, wird die Transaktion abgebrochen, da die Berechnung von *val* ungültig wird.

Transaktions-Backend

Transaktionen in Garnet werden mit den folgenden Klassen implementiert

  • TransactionManager
  • WatchVersionMap
  • WatchedKeyContainer
  • RespCommandsInfo

Aufgaben des TransactionManager

Speichern des Zustands der Transaktion:

  • Gestartet: Geht in diesen Zustand nach dem Befehl MULTI. TxnManager wird jeden Befehl in diesem Zustand mit Ausnahme von EXEC in die Warteschlange stellen.
  • Laufend: Geht in diesen Zustand nach EXEC. TxnManager wird die wartenden Befehle in diesem Zustand ausführen.
  • Abgebrochen: Geht in diesen Zustand, wenn etwas schiefgeht.

Befehle in die Warteschlange stellen:

Wenn TxnManager in den Zustand *Gestartet* übergeht, wird er (1) jeden folgenden Befehl in die Warteschlange stellen und (2) jeden Schlüssel speichern, der in diesen Befehlen verwendet wird, um ihn zur Ausführungszeit mithilfe von 2PL zu sperren. Um Befehle in die Warteschlange zu stellen, werden sie **im Netzwerkpuffer belassen**. Verwendung der TrySkip-Funktion in RespServerSession. Um die Schlüssel zur Ausführungszeit zu sperren, speichern wir Zeiger auf den tatsächlichen Speicherort der Schlüssel im Netzwerkpuffer mithilfe eines Arrays von TxnKeyEntry, das eine ArgSlice und den Sperrtyp (Shared oder Exclusive) enthält.

Die Funktion TrySkip verwendet die Klasse RespCommandsInfo, um die richtige Anzahl von Tokens zu überspringen und Syntaxfehler zu erkennen. RespCommandsinfo speichert die Anzahl der Arity oder Argumente jedes Befehls. Z. B. hat der Befehl GET eine Arity von zwei. Der Befehl-Token GET und ein Schlüssel. Wir speichern die Mindestanzahl von Argumenten mit einem negativen Wert für Befehle, die mehrere Argumente haben können. Der Befehl SET hat eine Arity von -3, was bedeutet, dass er mindestens drei Argumente (einschließlich des Befehls-Tokens) benötigt.

Während TrySkip rufen wir TransactionManager.GetKeys auf, das über die Argumente geht und TxnKeyEntry für jeden Schlüssel in den Argumenten speichert.

Ausführung

Wenn der TxnState *Gestartet* ist und wir auf EXEC stoßen, rufen wir TransactionManager.Run() auf. Was diese Funktion tut:

  1. Erwirbt zuerst den LockableContext für den Hauptspeicher und/oder Objektspeicher basierend auf dem Speichertyp.
  2. Geht die TxnKeyEntrys durch und sperrt alle benötigten Schlüssel.
  3. Ruft WatchedKeyContainer.ValidateWatchVersion() auf.
    • Er geht alle beobachteten Schlüssel durch und prüft, ob ihre Version mit der Zeit des Beobachtens übereinstimmt oder nicht.
    • Wenn dies erfolgreich ist, fahren wir mit der Ausführung fort, andernfalls rufen wir TransactionManager.Reset(true) auf, um den Transaktionsmanager zurückzusetzen. Das true-Argument, das wir an Reset übergeben, besagt, dass die Schlüssel auch entsperrt werden müssen.
  4. Schreibt den Transaktionsstartindikator in die AOF, um im Falle eines Fehlers mitten in der Transaktion atomar wiederhergestellt zu werden.

Danach wird TxnState auf *Laufend* gesetzt und der Netzwerk-readHead auf den ersten Befehl nach MULTI gesetzt, und diesmal beginnen wir, diese Befehle tatsächlich auszuführen. Wenn die Ausführung wieder auf EXEC trifft und wir uns im Zustand *Laufend* befinden, ruft sie TransactionManager.Commit() auf. Was sie tut:

  • Entsperrt alle Schlüssel, die wir in Run gesperrt haben.
  • Setzt TransactionManager und WatchedKeyContainer zurück.
  • Hängt außerdem die Commit-Nachricht an die AOF an.

Optimierung der Wiederherstellung

Garnet führt regelmäßige Checkpoints durch und ändert seine Version zwischen diesen Checkpoints. Um die Konsistenz der Checkpoints zu gewährleisten, müssen Transaktionsoperationen die gleiche Version haben oder mit anderen Worten im selben Checkpoint-Fenster liegen.

Um dies zu erzwingen, tun wir derzeit Folgendes:

  • Wenn sich TsavoriteStateMachine in der Prepare-Phase befindet, lassen wir keine Transaktion zur Ausführung starten, um den Abschluss des Checkpoints zu ermöglichen.
  • Wenn eine Transaktion läuft und TsavoriteStateMachine zu Prepare wechselt, verhindern wir eine Versionsänderung, bis die Transaktion die Ausführung abgeschlossen hat.
  • Dies geschieht mithilfe von session.IsInPreparePhase und zwei While-Schleifen am Anfang der Run-Funktion.

WATCH-Befehl

Wird zur Implementierung von optimistic locking verwendet.

  • Bietet ein Check-and-Set (CAS) -Verhalten für Transaktionen.
  • Schlüssel werden überwacht, um Änderungen an ihnen zu erkennen.
  • Wenn mindestens ein beobachteter Schlüssel vor dem EXEC-Befehl modifiziert wird, wird die gesamte Transaktion abgebrochen.
  • Dies wird durch ein Modified-Bit in TsavoriteKV und eine VersionMap in Garnet implementiert.

Versionskarte

Überwacht Modifikationen an den Schlüsseln. Jedes Mal, wenn ein beobachteter Schlüssel modifiziert wird, wird seine Version in der Versionskarte inkrementiert.

  • Wurde durch einen Hash Index implementiert.
  • Um den Overhead für normale Operationen im kritischen Pfad zu vermeiden, inkrementieren wir die Version nur in bestimmten Fällen.
    • Für In-Memory-Datensätze inkrementieren wir die Version nur für **beobachtete Schlüssel**. Die Schlüssel, die in Garnet beobachtet werden, verwenden das Modified-Bit in Tsavorite, um Modifikationen zu verfolgen (mehr zum Modified-Bit unten).

    • Für Datensätze auf der Festplatte inkrementieren wir die Version für Copy-Update RMWs und Upserts. **Wir akzeptieren diesen Overhead absichtlich, da Copy-Updates seltener vorkommen und der Overhead nicht kritisch ist.**

    • Inkrementiert die Version in MainStoreFunctions und ObjectStoreFunctions.

      • InPlaceUpdater, wenn er beobachtet wird.
      • ConcurrentWriter, wenn er beobachtet wird.
      • ConcurrentDeleter, wenn er beobachtet wird.
      • PostSingleWriter
      • PostInitialUpdater
      • PostCopyUpdater
      • PostSingleDeleter

Modified-Bit

Das Modified-Bit verfolgt Modifikationen in Datensätzen in Tsavorite. Das Modified-Bit für jeden Datensatz wird auf "1" gesetzt, wenn sie modifiziert werden, und **bleibt** "1", bis jemand es mithilfe der ResetModified-API auf Null zurücksetzt.

Watch

  • Wir fügen eine ClientSesssion.ResetModified(ref Key key) API hinzu.
    • CAS das RecordInfo-Wort in dasselbe Wort, aber mit dem **zurückgesetzten Modified-Bit**.
  • Wenn jemand einen Schlüssel in Garnet beobachtet, rufen wir die ResetModified API auf und speichern diesen Schlüssel in WatchedKeyContainer.
  • Zum Zeitpunkt des Beobachtens lesen wir die Version dieses Datensatzes aus der Versionskarte und speichern sie zusammen mit dem Schlüssel in WatchedKeyContainer.
  • Zur Zeit der Transaktionsausführung durchlaufen wir alle Schlüssel in WatchedKeyContainer und wenn ihre Version noch dieselbe ist, fahren wir mit den Transaktionen fort.

Unwatch

  • Wenn ein Datensatz in Tsavorite modifiziert wird, wird das Modified-Bit automatisch gesetzt.
  • Wenn ein Benutzer die Unwatch API in Garnet aufruft, setzen wir einfach den WatchedKeyContainer zurück.
  • Nach jedem DISCARD-, EXEC-, UNWATCH-Befehl machen wir alles rückgängig.

Testen

Wir haben ein Mikro-Benchmark namens TxnPerfBench geschrieben, um Client-Transaktionen zu testen. Das Benchmark enthält vier verschiedene Workloads:

  • READ_TXN
  • WRITE_TXN
  • READ_WRITE_TXN
  • WATCH_TXN

Es ähnelt dem Online-Benchmark und kann unterschiedliche Prozentsätze verschiedener Workloads aufweisen.

dotnet run -c Release -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload WATCH_TXN --op-percent 100
dotnet run -c Release -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload READ_TXN,WRITE_TXN --op-percent 50,50
dotnet run -c Release -t 2 -b 1 --dbsize 1024 -x --client SERedis --op-workload READ_WRITE_TXN --op-percent 100

Vor der Ausführung des Benchmarks laden wir Daten mit opts.DbSize Anzahl von Datensätzen. Es akzeptiert auch die Anzahl der Lese- und Schreibvorgänge pro Transaktion.

TxnPerfBench(..., int readPerTxn = 4, int writePerTxn = 4)

  • Wir unterstützen nur Batch-Größe eins.
  • Wir unterstützen derzeit nur den SE.Redis-Client.

READ_TXN

Führt eine Transaktion mit readPerTxn Anzahl von GET-Anfragen aus.

WRITE_TXN

Führt eine Transaktion mit writePerTxn Anzahl von SET-Anfragen aus.

READ_WRITE_TXN

Führt eine Mischung aus SET- und GET-Anfragen aus (readPerTxn, writePerTxn).

WATCH_TXN

Diese Workload beobachtet readPerTxn Anzahl von Schlüsseln. Startet dann eine Transaktion, liest die beobachteten Schlüssel und schreibt auf writePerTxn Anzahl von Schlüsseln.

readPerTxn = 2
writePerTxn = 2

WATCH x1
WATCH x2
MULTI
GET x1
GET x2
SET x3 v3
SET x4 v4
EXEC