Zum Hauptinhalt springen

Verteilte Locks erklärt

· 4 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Ein praktischer Leitfaden für .NET-Entwickler

Einleitung​

Wenn Anwendungen über mehrere Rechner skalieren, wird die Koordination zu einem der schwierigsten Aspekte des Systementwurfs.

In einer Anwendung auf einem einzelnen Rechner ist gegenseitiger Ausschluss unkompliziert. Entwickler können sich auf Sprachprimitive wie lock in C#, Mutex oder Semaphore verlassen, um sicherzustellen, dass jeweils nur ein Thread auf eine gemeinsame Ressource zugreift.

Diese Primitive funktionieren jedoch nur innerhalb eines einzelnen Prozesses oder Rechners.

Sobald Anwendungen über mehrere Dienste oder Nodes laufen, greifen klassische Locking-Mechanismen nicht mehr. Zwei Dienste auf verschiedenen Rechnern können sich nicht auf eine lokale lock-Anweisung stützen, um den Zugriff auf gemeinsamen Zustand zu koordinieren.

Hier werden verteilte Locks unverzichtbar.

Verteilte Locks erlauben es Diensten in einem Cluster, den Zugriff auf gemeinsame Ressourcen zu koordinieren und dabei sicherzustellen, dass zu einem gegebenen Zeitpunkt nur ein Teilnehmer den Lock hält.


Lokale Locks vs. verteilte Locks​

Ein typischer lokaler Lock in C# sieht so aus:

private static readonly object _lock = new object();

public void ProcessOrder()
{
lock(_lock)
{
Console.WriteLine("Processing order safely.");
}
}

Das funktioniert einwandfrei innerhalb eines einzelnen Prozesses.

Läuft Ihre Anwendung jedoch auf mehreren Dienstinstanzen, besitzt jede Instanz ihr eigenes _lock-Objekt. Der Lock koordiniert die Arbeit nicht mehr über das gesamte System.

Worker A → lock acquired Worker B → lock acquired Worker C → lock acquired

Alle Worker fahren gleichzeitig fort, womit der Zweck des Lockings verfehlt ist.

Verteilte Systeme benötigen einen gemeinsamen Koordinationsmechanismus.


Verteiltes Locking


Die Idee hinter verteilten Locks​

Ein verteilter Lock nutzt ein gemeinsames System, um den Besitz des Locks abzubilden.

Statt Speicher zu sperren, hält ein Node den Lock-Besitz in einem verteilten Koordinationssystem fest, zum Beispiel in:

  • einem verteilten Key/Value-Store
  • einem Koordinationsdienst
  • einem verteilten Cache

Beispielkonzept:

Lock Key: order:123 Owner: worker‑1

Versucht ein anderer Worker, denselben Lock zu erwerben, schlägt die Operation fehl, bis der Lock freigegeben wird oder abläuft.


Praxisszenario: Verteilte Rechnungsverarbeitung​

Stellen Sie sich einen Cluster von Workern vor, die Rechnungen verarbeiten.

Worker A Worker B Worker C

Alle Worker suchen nach offenen Rechnungen.

Ohne Locking:

Worker A → processes invoice #101 Worker B → processes invoice #101 Worker C → processes invoice #101

Dieselbe Rechnung könnte mehrfach verarbeitet werden.

Ein verteilter Lock stellt sicher, dass nur ein Worker die Rechnung verarbeitet.


Verteilte Locks mit Clustron DKV​

Clustron DKV bietet integrierte Primitive für die verteilte Koordination.

Locks werden über die Locks API verwaltet, und Operationen auf gesperrten Keys müssen das korrekte Lock-Handle enthalten.


Einen Lock erwerben​

Ein Client erwirbt zunächst einen Lock für einen bestimmten Key.

var locks = ((IDkv)client).Locks;

var key = "order:123";

var lockHandle = await locks.AcquireAsync(key, TimeSpan.FromSeconds(10));

if (lockHandle == null)
{
Console.WriteLine("Failed to acquire lock.");
return;
}

Console.WriteLine("Lock acquired.");

Das zurückgegebene Lock-Handle repräsentiert den Besitz des Locks.


Operationen unter dem Lock ausführen​

Sobald ein Lock erworben wurde, müssen Operationen das Lock-Handle enthalten.

var result = await client.PutAsync(
key,
"processed",
Put.WithLock(lockHandle.Handle));

if (result.IsSuccess)
{
Console.WriteLine("Value updated safely.");
}

Nur der Lock-Besitzer kann den Wert ändern.


Lesen mit dem Lock​

Auch Lesezugriffe können den Lock-Besitz berücksichtigen.

var value = await client.GetAsync<string>(
key,
Get.WithLock(lockHandle.Handle));

if (value.IsSuccess)
{
Console.WriteLine($"Value: {value.Value}");
}

Konkurrierende Operationen ohne den Lock​

Versucht ein anderer Worker, den Key ohne den Lock zu ändern, schlägt die Operation fehl.

var attempt = await client.PutAsync(key, "new-value");

if (!attempt.IsSuccess)
{
Console.WriteLine($"Operation rejected: {attempt.Status}");
}

Ergebnis:

KvStatus.Locked

Dies verhindert die gleichzeitige Änderung derselben Ressource.


Den Lock freigeben​

Ist die Arbeit abgeschlossen, kann der Lock freigegeben werden.

await lockHandle.ReleaseAsync();

Console.WriteLine("Lock released.");

Nach der Freigabe kann ein anderer Worker den Lock erwerben.


Beispielhafter Cluster-Ablauf​

Worker A → Acquire Lock → Success Worker B → Acquire Lock → Failed Worker C → Acquire Lock → Failed

Worker A → Update Key Worker A → Release Lock

Nur ein Worker führt die Operation aus, was die Korrektheit sicherstellt.


Umgang mit Ausfällen​

Locks in Clustron sind zeitlich begrenzt.

Beim Erwerb eines Locks muss eine Dauer angegeben werden:

await locks.AcquireAsync(key, TimeSpan.FromSeconds(10));

Stürzt der Client, der den Lock hält, ab oder bricht die Verbindung ab, läuft der Lock schließlich ab, sodass ein anderer Worker ihn erwerben kann.

Dadurch bleiben Locks nicht dauerhaft gehalten.


Best Practices​

Bei der Implementierung verteilter Locks:

  • Halten Sie kritische Abschnitte kurz
  • Vermeiden Sie nach Möglichkeit globale Locks
  • Partitionieren Sie Locks nach Ressource
  • Planen Sie stets für Fehlerszenarien

Schlussgedanken​

Verteilte Locks gehören zu den grundlegendsten Koordinationsprimitiven in verteilten Systemen.

Sie erlauben es Diensten auf mehreren Rechnern, den Zugriff auf gemeinsame Ressourcen sicher zu koordinieren und dabei die Korrektheit des Systems zu wahren.

Plattformen wie Clustron DKV vereinfachen dies durch integrierte Locking-Primitive, atomare Operationen und starke Konsistenzgarantien.

Im nächsten Artikel betrachten wir ein weiteres essenzielles Primitiv verteilter Systeme:

Leader-Election.