manipulați condițiile de Cursă în NodeJS folosind Mutex

chiar dacă JavaScript este cu un singur fir, pot exista probleme de concurență, cum ar fi condițiile de cursă datorate asincronismului. Acest articol își propune să vă spun cum am tratat acest lucru folosind excluderea reciprocă. Dar mai întâi, să ne amintim care este o condiție de rasă înainte de a intra în problemă și de a vă spune cum am rezolvat-o.

condiție de cursă

o condiție de cursă apare atunci când mai multe procese încearcă să acceseze aceeași resursă partajată și cel puțin una dintre ele încearcă să-i modifice valoarea. De exemplu, imaginați-vă că avem o valoare partajată a = 3 și două procese a și B. Imaginați-vă că procesul a dorește să adauge 5 la valoarea curentă a lui A, în timp ce procesul B dorește să adauge 2 la a numai dacă a < 5. În funcție de ce proces se execută mai întâi, rezultatul nu va fi cel așteptat.Dacă procesul a execută mai întâi, valoarea lui a va fi 8, în timp ce va fi 10 dacă procesul B execută first.To evitați condițiile de rasă, folosim excluderea reciprocă. Voi vorbi despre asta mai detaliat mai târziu în acest articol.

acum permiteți-mi să explic problema pe care am avut-o.

Context

unul dintre proiectele mele de la Theodo a constat în construirea unei aplicații NodeJS care a propus mai multe evenimente și concursuri pentru clienții săi. A existat, de asemenea, o caracteristică de membru premium care a permis utilizatorilor să participe la evenimente gratuite. Pentru a participa, utilizatorii trebuie doar să facă clic pe butonul de participare de pe pagina evenimentului și primesc un bilet. Participările Multiple la același eveniment sunt interzise prin dezactivarea butonului după succesul participării.

mai mult, există o verificare de securitate în back-end pentru a împiedica utilizatorii cu o Comandă plătită existentă să obțină un alt bilet. Puteți vedea codul simplificat de mai jos:

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

în primul rând, caut în baza de date o comandă existentă legată de un utilizator și un eveniment. Dacă nu există nicio comandă, atunci o nouă comandă este creată și salvată în baza de date. În caz contrar, nu se face nimic.

cu toate acestea, unii oameni au reușit să obțină mai multe bilete făcând clic de multe ori rapid pe acest buton. Aceasta a fost o problemă de rasă.

care a fost problema mai exact ?

condiția anterioară nu era suficientă. Într-adevăr, chiar dacă JavaScript este mono-threaded, nu împiedică condițiile de rasă. Când aveți de-a face cu funcții asincrone, firul nu blochează executarea acestuia, dar fie execută următoarea linie care nu depinde de apelul asincron, fie continuă execuția corespunzătoare unui eveniment de răspuns. Drept urmare, două execuții diferite se pot împleti.

ia exemplul de mai jos: un utilizator face 2 cereri consecutive la back-end, și să presupunem că el sau ea nu are nici o ordine corespunzătoare acestui eveniment. Deoarece JavaScript este mono-threaded, stiva de execuție va fi:

stiva de execuție

dar ordinea de execuție a diferitelor linii ale funcției va fi:

executarea codului fără blocare

findOrder și createOrder sunt apeluri asincrone de când citesc și scriu în baza de date. În consecință, cele două cereri se vor întrepătrunde. După cum puteți vedea în figura de mai sus, al doilea findOrder este executat imediat după cea a primei cereri. Prin urmare, a doua evaluare a !existOrder va fi true deoarece apelul a fost efectuat înainte de crearea unei comenzi.

concluzie: utilizatorul nostru va primi 2 bilete.

soluție

a trebuit să găsesc o modalitate de a bloca această parte a codului pentru a executa întreaga funcție înainte de a permite unei alte cereri să execute același cod și, astfel, să evit condițiile de rasă. Am făcut acest lucru folosind mutex, cu Biblioteca async-mutex (o puteți instala rulând yarn add async-mutex).

un mutex este un obiect de excludere reciprocă care creează o resursă care poate fi partajată între mai multe fire ale unui program. Resursa poate fi văzută ca o blocare pe care un singur fir o poate dobândi. Dacă un alt fir dorește să achiziționeze blocarea, trebuie să aștepte până când blocarea este eliberată. Dar aveți grijă ca blocarea să fie întotdeauna eliberată în cele din urmă, indiferent de ce se întâmplă în timpul execuției. În caz contrar, va duce la blocaje, iar programul dvs. va fi blocat.

pentru a nu încetini achiziționarea de bilete, am folosit un mutex per utilizator, pe care l-am stocat într-o hartă. Dacă utilizatorul nu este mapat la un mutex, o nouă instanță este creată în hartă folosind ID-ul de utilizator ca o cheie, și apoi am folosit mutex ca aceasta :

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

puteți vedea că, cu blocul try-catch-finally, mă asigur că blocarea va fi întotdeauna eliberată. Acum, execuția va arăta ca figura de mai jos:

executarea codului cu blocare

în acest fel, un utilizator va putea participa o singură dată.

desigur, aceasta nu este singura modalitate de a rezolva acest tip de problemă. Tranzacțiile sunt, de asemenea, foarte utile în acest caz.

de asemenea, rețineți că a rezolvat problema pe o singură instanță de server. Dacă aveți mai multe servere, atunci ar trebui să utilizați mutex distribuit pentru a anunța toate procesele că blocarea este achiziționată.

Lasă un răspuns

Adresa ta de email nu va fi publicată.