Gestisci le condizioni di gara in NodeJS Usando Mutex

Anche se JavaScript è a thread singolo, possono esserci problemi di concorrenza come le condizioni di gara a causa dell’asincrono. Questo articolo si propone di dirvi come ho gestito questo utilizzando l’esclusione reciproca. Ma prima, ricordiamo qual è una condizione di gara prima di entrare nel problema e dirti come l’ho risolto.

Race condition

Una race condition si verifica quando più processi tentano di accedere a una stessa risorsa condivisa e almeno uno di essi tenta di modificarne il valore. Ad esempio, immagina di avere un valore condiviso a = 3 e due processi A e B. Immagina che il processo A voglia aggiungere 5 al valore corrente di a mentre il processo B vuole aggiungere 2 ad a solo se a < 5. A seconda del processo eseguito per primo, il risultato non sarà quello previsto.Se il processo A viene eseguito per primo, il valore di a sarà 8 mentre sarà 10 se il processo B viene eseguito first.To evitare le condizioni di gara, usiamo l’esclusione reciproca. Ne parlerò più in dettaglio più avanti in questo articolo.

Ora lasciatemi spiegare il problema che ho avuto.

Contesto

Uno dei miei progetti a Theodo consisteva nella costruzione di un’applicazione NodeJS che proponeva più eventi e concorsi per i suoi clienti. C’era anche una funzione di abbonamento premium che permetteva agli utenti di partecipare a eventi gratuiti. Per partecipare, gli utenti devono solo fare clic sul pulsante di partecipazione nella pagina dell’evento e ricevono un biglietto. Le partecipazioni multiple allo stesso evento non sono consentite disabilitando il pulsante dopo il successo della partecipazione.

Inoltre, c’è un controllo di sicurezza nel back-end per impedire agli utenti con un ordine pagato esistente di ottenere un altro biglietto. Puoi vedere il codice semplificato qui sotto:

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

Innanzitutto, cerco nel database un ordine esistente relativo a un utente e un evento. Se non esiste un ordine, viene creato e salvato un nuovo ordine nel database. Altrimenti, non si fa nulla.

Tuttavia, alcune persone sono riuscite a ottenere diversi biglietti facendo clic molte volte rapidamente su questo pulsante. Questo era un problema di condizioni di gara.

Qual era esattamente il problema ?

La condizione precedente non era sufficiente. Infatti, anche se JavaScript è mono-thread, non impedisce le condizioni di gara. Quando si gestiscono funzioni asincrone, il thread non blocca la sua esecuzione ma esegue la riga successiva che non dipende dalla chiamata asincrona o continua l’esecuzione corrispondente a un evento di risposta. Di conseguenza, due diverse esecuzioni possono intrecciarsi.

Prendi l’esempio seguente: un utente effettua 2 richieste consecutive al back-end e supponiamo che non abbia alcun ordine corrispondente a questo evento. Come JavaScript è mono-thread, lo stack di esecuzione sarà:

stack di Esecuzione

Ma l’ordine di esecuzione delle diverse linee della funzione sarà:

esecuzione di Codice in modalità senza serratura

findOrder e createOrder sono chiamate asincrone dato che leggere e scrivere nel database. Di conseguenza, le due richieste si intrecceranno. Come puoi vedere nella figura sopra, il secondo findOrder viene eseguito subito dopo quello della prima richiesta. Quindi, la seconda valutazione di !existOrder sarà true poiché la chiamata è stata effettuata prima di creare un ordine.

Conclusione: il nostro utente riceverà 2 biglietti.

Soluzione

Ho dovuto trovare un modo per bloccare questa parte del codice per eseguire l’intera funzione prima di consentire a un’altra richiesta di eseguire lo stesso codice, e quindi evitare le condizioni di gara. L’ho fatto usando mutex, con la libreria async-mutex (puoi installarla eseguendo yarn add async-mutex).

Un mutex è un oggetto di esclusione reciproca che crea una risorsa che può essere condivisa tra più thread di un programma. La risorsa può essere vista come un blocco che solo un thread può acquisire. Se un altro thread vuole acquisire il blocco, deve attendere fino a quando il blocco non viene rilasciato. Ma attenzione che il blocco dovrebbe sempre essere rilasciato alla fine, indipendentemente da ciò che accade durante l’esecuzione. Altrimenti, porterà a deadlock e il tuo programma verrà bloccato.

Per non rallentare l’acquisto dei biglietti, ho usato un mutex per utente, che ho memorizzato in una mappa. Se l’utente non è mappato su un mutex, viene creata una nuova istanza nella mappa utilizzando l’ID utente come chiave, quindi ho usato il mutex in questo modo :

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

Puoi vedere che con il blocco try-catch-finally, mi assicuro che il blocco verrà sempre rilasciato. Ora, l’esecuzione sarà simile alla figura seguente:

Esecuzione di codice con lock

In questo modo, un utente sarà in grado di partecipare solo una volta.

Naturalmente, questo non è l’unico modo per risolvere questo tipo di problema. Le transazioni sono anche molto utili in questo caso.

Inoltre, tieni presente che ha risolto il problema su una singola istanza del server. Se si dispone di più server, è necessario utilizzare mutex distribuito per far sapere a tutti i processi che il blocco è stato acquisito.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.