obsługuje Warunki wyścigu w NodeJS używając Mutex

nawet jeśli JavaScript jest jednowątkowy, mogą wystąpić problemy z współbieżnością, takie jak warunki wyścigu ze względu na asynchronizm. Ten artykuł ma na celu opowiedzieć, jak poradziłem sobie z tym za pomocą wzajemnego wykluczenia. Ale najpierw przypomnijmy sobie, jaki jest stan rasy, zanim przejdziemy do problemu i powiemy Ci, jak go rozwiązałem.

warunek wyścigu

warunek wyścigu występuje, gdy wiele procesów próbuje uzyskać dostęp do tego samego współdzielonego zasobu i co najmniej jeden z nich próbuje zmodyfikować jego wartość. Na przykład, wyobraźmy sobie, że mamy wspólną wartość a = 3 i dwa procesy A I B. Wyobraźmy sobie, że proces a chce dodać 5 do bieżącej wartości a, podczas gdy proces B chce dodać 2 do a tylko wtedy, gdy a < 5. W zależności od tego, który proces zostanie wykonany jako pierwszy, wynik nie będzie oczekiwany.Jeśli proces a zostanie uruchomiony jako pierwszy, wartość a będzie wynosić 8, podczas gdy wartość a będzie wynosić 10, jeśli proces b zostanie uruchomiony jako pierwszy first.To unikaj warunków rasowych, stosujemy wzajemne wykluczenie. Opowiem o tym bardziej szczegółowo w dalszej części tego artykułu.

teraz pozwól mi wyjaśnić problem, który miałem .

kontekst

jeden z moich projektów w Theodo polegał na zbudowaniu aplikacji NodeJS, która proponowała wiele wydarzeń i konkursów dla swoich klientów. Istniała również funkcja członkostwa premium, która pozwalała użytkownikom uczestniczyć w darmowych wydarzeniach. Aby wziąć udział, wystarczy kliknąć przycisk uczestnictwa na stronie wydarzenia i otrzymać bilet. Wielokrotne uczestnictwo w tym samym wydarzeniu jest wykluczone poprzez wyłączenie przycisku po pomyślnym uczestnictwie.

co więcej, istnieje kontrola bezpieczeństwa w zapleczu, aby uniemożliwić użytkownikom z istniejącym płatnym zamówieniem uzyskanie kolejnego biletu. Możesz zobaczyć uproszczony kod poniżej:

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); }}

najpierw poszukuję w bazie danych istniejącego zlecenia związanego z użytkownikiem i zdarzeniem. Jeżeli zamówienie nie istnieje, wtedy zostanie utworzone nowe zamówienie i zapisane w bazie danych. W przeciwnym razie nic się nie stanie.

jednak niektórym osobom udało się zdobyć kilka biletów, klikając wiele razy szybko ten przycisk. To była kwestia warunków rasowych.

w czym dokładnie tkwił problem ?

poprzedni warunek nie wystarczył. Rzeczywiście, nawet jeśli JavaScript jest jednowątkowy, nie zapobiega to Warunkom wyścigu. Kiedy masz do czynienia z funkcjami asynchronicznymi, wątek nie blokuje jego wykonania, ale albo wykonuje następną linię, która nie zależy od asynchronicznego wywołania, albo kontynuuje wykonywanie odpowiadające zdarzeniu odpowiedzi. W rezultacie dwie różne egzekucje mogą się ze sobą przeplatać.

weźmy przykład poniżej: użytkownik wykonuje 2 kolejne żądania do zaplecza i załóżmy, że nie ma zamówienia odpowiadającego temu zdarzeniu. Ponieważ JavaScript jest jednowątkowy, stos wykonania będzie:

stos wykonania

ale kolejność wykonania poszczególnych linii funkcji będzie:

wykonanie kodu bez blokady

findOrder i createOrder są wywołaniami asynchronicznymi, ponieważ odczytują i zapisują do bazy danych. W konsekwencji oba wnioski będą się ze sobą splatać. Jak widać na powyższym rysunku, drugi findOrder jest wykonywany zaraz po pierwszym żądaniu. W związku z tym druga ocena !existOrder będzie wynosić true, ponieważ połączenie zostało wykonane przed utworzeniem zamówienia.

wniosek: nasz użytkownik otrzyma 2 bilety.

rozwiązanie

musiałem znaleźć sposób na zablokowanie tej części kodu, aby wykonać całą funkcję, zanim zezwoliłem na kolejne żądanie wykonania tego samego kodu, a więc uniknąłem warunków wyścigu. Zrobiłem to za pomocą mutex, z biblioteką async-mutex (możesz ją zainstalować, uruchamiając yarn add async-mutex).

mutex jest obiektem wzajemnego wykluczenia, który tworzy zasób, który może być współdzielony między wieloma wątkami programu. Zasób może być postrzegany jako blokada, którą może zdobyć tylko jeden wątek. Jeśli inny wątek chce zdobyć blokadę, musi poczekać, aż blokada zostanie zwolniona. Ale uważaj, że zamek powinien być zawsze zwolniony w końcu, bez względu na to, co dzieje się podczas wykonywania. W przeciwnym razie doprowadzi to do impasów, a twój program zostanie zablokowany.

aby nie spowolnić zakupu biletów, użyłem jednego mutexu na użytkownika, który zapisałem na mapie. Jeśli użytkownik nie jest mapowany na mutex, to nowa instancja jest tworzona na mapie przy użyciu identyfikatora użytkownika jako klucza, a następnie użyłem mutex w ten sposób :

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(); } }, ); }}

widać, że z blokadą try-catch-finally gwarantuję, że blokada zawsze zostanie zwolniona. Teraz wykonanie będzie wyglądać jak na poniższym rysunku:

wykonanie kodu z blokadą

w ten sposób użytkownik będzie mógł wziąć udział tylko raz.

oczywiście nie jest to jedyny sposób na rozwiązanie tego rodzaju problemu. Transakcje są również bardzo pomocne w tym przypadku.

Pamiętaj również, że rozwiązało to problem na pojedynczej instancji serwera. Jeśli masz wiele serwerów, powinieneś użyć rozproszonego mutex, aby poinformować wszystkie procesy o przejęciu blokady.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.