hantera rasförhållanden i NodeJS med Mutex

även om JavaScript är enkelgängat kan det finnas samtidighetsproblem som rasförhållanden på grund av asynkronism. Denna artikel syftar till att berätta hur jag hanterade detta med ömsesidig uteslutning. Men först, låt oss komma ihåg vad som är ett tävlingsförhållande innan vi går in i problemet och berättar hur jag löste det.

Race condition

ett race condition uppstår när flera processer försöker komma åt samma delade resurs och åtminstone en av dem försöker ändra dess värde. Föreställ dig till exempel att vi har ett delat värde a = 3 och två processer A och B. Föreställ dig att process A vill lägga till 5 till det aktuella värdet av a medan process B bara vill lägga till 2 till a om a < 5. Beroende på vilken process som körs först blir resultatet inte det som förväntas.Om process A körs först kommer värdet på A att vara 8 medan det blir 10 Om process B körs first.To undvik rasförhållanden, vi använder ömsesidig uteslutning. Jag kommer att prata mer om det senare i den här artikeln.

låt mig nu förklara problemet jag hade.

Context

ett av mina projekt på Theodo bestod i att bygga en NodeJS-applikation som föreslog flera evenemang och tävlingar för sina kunder. Det fanns också en premiummedlemskapsfunktion som gjorde det möjligt för användare att delta i gratisevenemang. För att delta behöver användarna bara klicka på deltagarknappen på evenemangssidan och de får en biljett. Flera deltagare till samma händelse tillåts inte genom att inaktivera knappen efter deltagandet framgång.

dessutom finns det en säkerhetskontroll i back-end för att förhindra användare med en befintlig betald order att få en annan biljett. Du kan se den förenklade koden nedan:

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

först söker jag i databasen efter en befintlig order relaterad till en användare och en händelse. Om det inte finns någon order skapas och sparas en ny order i databasen. Annars görs ingenting.

vissa lyckades dock få flera biljetter genom att klicka många gånger snabbt på den här knappen. Detta var en race-condition fråga.

vad var problemet exakt ?

det tidigare villkoret var inte tillräckligt. Faktum är att även om JavaScript är mono-threaded, förhindrar det inte rasförhållanden. När du hanterar asynkrona funktioner blockerar tråden inte dess körning men den kör antingen nästa rad som inte beror på det asynkrona samtalet eller fortsätter körningen som motsvarar en svarshändelse. Som ett resultat kan två olika avrättningar sammanflätas.

ta exemplet nedan: en användare gör 2 på varandra följande förfrågningar till back-end, och antar att han eller hon har ingen ordning som motsvarar denna händelse. Eftersom JavaScript är mono-threaded kommer exekveringsstacken att vara:

Exekveringsstack

men exekveringsordningen för de olika raderna i funktionen kommer att vara:

kodkörning utan lås

findOrder och createOrder är asynkrona samtal eftersom de läser och skriver in i databasen. Som en konsekvens kommer de två förfrågningarna att sammanflätas. Som du kan se i figuren ovan körs den andra findOrder direkt efter den första begäran. Därför kommer den andra utvärderingen av !existOrder att vara true eftersom samtalet har gjorts innan du skapar en beställning.

slutsats: vår användare kommer att få 2 biljetter.

lösning

jag var tvungen att hitta ett sätt att låsa den här delen av koden för att utföra hela funktionen innan jag tillåter en annan begäran att utföra samma kod och så undvika rasförhållanden. Jag gjorde det med mutex, med async-mutex-biblioteket (Du kan installera det genom att köra yarn add async-mutex).

en mutex är ett ömsesidigt uteslutningsobjekt som skapar en resurs som kan delas mellan flera trådar i ett program. Resursen kan ses som ett lås som endast en tråd kan förvärva. Om en annan tråd vill skaffa låset måste den vänta tills låset släpps. Men se upp för att låset alltid ska släppas så småningom, oavsett vad som händer under utförandet. Annars kommer det att leda till deadlocks, och ditt program kommer att blockeras.

för att inte bromsa köp av biljetter, jag använde en mutex per användare, att jag lagras i en karta. Om användaren inte är mappad till en mutex skapas en ny instans i kartan med användar-id som en nyckel, och sedan använde jag mutex så här :

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

du kan se att med try-catch-finally-blocket ser jag till att låset alltid kommer att släppas. Nu kommer utförandet att se ut som figuren nedan:

kodkörning med lås

på så sätt kan en användare bara delta en gång.

naturligtvis är detta inte det enda sättet att lösa denna typ av problem. Transaktioner är också till stor hjälp i det här fallet.

tänk också på att det löste problemet på en enda serverinstans. Om du har flera servrar bör du använda distribuerad mutex för att låta alla processer veta att låset förvärvas.

Lämna ett svar

Din e-postadress kommer inte publiceras.