Verteilte Locks erklärt
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.

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.