Behandeln Sie Race-Bedingungen in NodeJS mit Mutex

Selbst wenn JavaScript Single-Threaded ist, kann es aufgrund von Asynchronität zu Parallelitätsproblemen wie Race-Bedingungen kommen. Dieser Artikel soll Ihnen sagen, wie ich mit gegenseitigem Ausschluss umgegangen bin. Aber zuerst wollen wir uns daran erinnern, was eine Rennbedingung ist, bevor wir uns mit dem Problem befassen und Ihnen sagen, wie ich es gelöst habe.

Race condition

Eine Race Condition tritt auf, wenn mehrere Prozesse versuchen, auf dieselbe gemeinsam genutzte Ressource zuzugreifen, und mindestens einer von ihnen versucht, seinen Wert zu ändern. Stellen Sie sich zum Beispiel vor, wir haben einen gemeinsamen Wert a = 3 und zwei Prozesse A und B. Stellen Sie sich vor, Prozess A möchte 5 zum aktuellen Wert von a addieren, während Prozess B nur dann 2 zu a addieren möchte, wenn a < 5 . Je nachdem, welcher Prozess zuerst ausgeführt wird, ist das Ergebnis nicht das erwartete.Wenn Prozess A zuerst ausgeführt wird, ist der Wert von a 8, während er 10 ist, wenn Prozess B ausgeführt wird first.To vermeiden Sie Rassenbedingungen, wir verwenden gegenseitigen Ausschluss. Ich werde später in diesem Artikel ausführlicher darüber sprechen.

Lassen Sie mich nun das Problem erklären, das ich hatte.

Kontext

Eines meiner Projekte bei Theodo bestand darin, eine NodeJS-Anwendung zu erstellen, die mehrere Veranstaltungen und Wettbewerbe für ihre Kunden vorschlug. Es gab auch eine Premium-Mitgliedschaftsfunktion, mit der Benutzer an kostenlosen Veranstaltungen teilnehmen konnten. Um teilzunehmen, müssen Benutzer nur auf die Schaltfläche Teilnahme auf der Veranstaltungsseite klicken, und sie erhalten ein Ticket. Mehrfache Teilnahmen an derselben Veranstaltung werden durch Deaktivieren der Schaltfläche nach erfolgter Teilnahme ausgeschlossen.

Darüber hinaus gibt es eine Sicherheitsüberprüfung im Backend, um zu verhindern, dass Benutzer mit einer vorhandenen bezahlten Bestellung ein weiteres Ticket erhalten. Sie können den vereinfachten Code unten sehen:

async function participateInFreeEvent(user: User, eventId: number): Promise<void> {const existOrder = await findOrder(eventId, user.id); if (!existOrder) { const order = buildNewOrder(eventId, user.id); createOrder(order.id, eventId, user.id); }}

Zuerst suche ich in der Datenbank nach einer vorhandenen Bestellung, die sich auf einen Benutzer und ein Ereignis bezieht. Wenn kein Auftrag vorhanden ist, wird ein neuer Auftrag erstellt und in der Datenbank gespeichert. Ansonsten wird nichts getan.

Einige Leute haben es jedoch geschafft, mehrere Tickets zu erhalten, indem sie viele Male schnell auf diese Schaltfläche geklickt haben. Dies war ein Race-Condition-Problem.

Was genau war das Problem?

Die vorherige Bedingung war nicht genug. Selbst wenn JavaScript Mono-threaded ist, verhindert es keine Race-Conditions. Wenn Sie mit asynchronen Funktionen arbeiten, blockiert der Thread seine Ausführung nicht, sondern führt entweder die nächste Zeile aus, die nicht vom asynchronen Aufruf abhängt, oder setzt die Ausführung entsprechend einem Antwortereignis fort. Dadurch können sich zwei verschiedene Ausführungen verflechten.

Nehmen wir das folgende Beispiel: Ein Benutzer stellt 2 aufeinanderfolgende Anforderungen an das Back-End und nimmt an, er oder sie hat keine Reihenfolge, die diesem Ereignis entspricht. Da JavaScript Mono-threaded ist, wird der Ausführungsstapel sein:

 Ausführungsstapel

Aber die Ausführungsreihenfolge der verschiedenen Zeilen der Funktion wird sein:

 Codeausführung ohne Sperre

findOrder und createOrder sind asynchrone Aufrufe, da sie in die Datenbank lesen und schreiben. Infolgedessen verflechten sich die beiden Anforderungen. Wie Sie in der obigen Abbildung sehen können, wird die zweite findOrder direkt nach der ersten Anforderung ausgeführt. Daher ist die zweite Auswertung von !existOrder true, da der Anruf vor dem Erstellen einer Bestellung getätigt wurde.

Fazit: Unser User erhält 2 Tickets.

Lösung

Ich musste einen Weg finden, diesen Teil des Codes zu sperren, um die gesamte Funktion auszuführen, bevor eine andere Anforderung denselben Code ausführen konnte, und so Race-Conditions vermeiden. Ich habe dies mit mutex mit der async-mutex Bibliothek gemacht (Sie können es installieren, indem Sie yarn add async-mutex ausführen).

Ein Mutex ist ein gegenseitiges Ausschlussobjekt, das eine Ressource erstellt, die von mehreren Threads eines Programms gemeinsam genutzt werden kann. Die Ressource kann als Sperre angesehen werden, die nur ein Thread erwerben kann. Wenn ein anderer Thread die Sperre erhalten möchte, muss er warten, bis die Sperre freigegeben wird. Aber Vorsicht, dass die Sperre immer irgendwann freigegeben werden sollte, egal was während der Ausführung passiert. Andernfalls führt dies zu Deadlocks und Ihr Programm wird blockiert.

Um den Kauf von Tickets nicht zu verlangsamen, habe ich einen Mutex pro Benutzer verwendet, den ich in einer Karte gespeichert habe. Wenn der Benutzer keinem Mutex zugeordnet ist, wird in der Map eine neue Instanz mit der Benutzer-ID als Schlüssel erstellt, und dann habe ich den Mutex folgendermaßen verwendet :

import { Mutex, MutexInterface } from 'async-mutex';class PaymentService {private locks : Map<string, MutexInterface>;constructor() {this.locks = new Map();}public async participateInFreeEvent(user: User, eventId: number): Promise<void> { if (!this.locks.has(user.id)) { this.locks.set(user.id, new Mutex()); } this.locks .get(user.id) .acquire() .then(async (release) => { try { const existOrder = await findOrder(eventId, user.id); if (!existOrder) { const order = buildNewOrder(eventId, user.id); createOrder(order.id, eventId, user.id); } } catch (error) { } finally { release(); } }, ); }}

Sie können sehen, dass ich mit dem try-catch-finally Block sicherstelle, dass die Sperre immer freigegeben wird. Nun sieht die Ausführung wie in der folgenden Abbildung aus:

 Codeausführung mit Sperre

Auf diese Weise kann ein Benutzer nur einmal teilnehmen.

Natürlich ist dies nicht der einzige Weg, um diese Art von Problem zu lösen. Transaktionen sind auch in diesem Fall sehr hilfreich.

Denken Sie auch daran, dass das Problem auf einer einzelnen Serverinstanz behoben wurde. Wenn Sie mehrere Server haben, sollten Sie verteilten Mutex verwenden, um allen Prozessen mitzuteilen, dass die Sperre erworben wurde.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.