Gérer les conditions de concurrence Dans NodeJS En utilisant Mutex

Même si JavaScript est mono-thread, il peut y avoir des problèmes de concurrence tels que les conditions de concurrence dues à l’asynchronisme. Cet article vise à vous expliquer comment j’ai géré cela en utilisant l’exclusion mutuelle. Mais d’abord, rappelons ce qu’est une condition de course avant d’entrer dans le problème et de vous dire comment je l’ai résolu.

Condition de concurrence

Une condition de concurrence se produit lorsque plusieurs processus tentent d’accéder à une même ressource partagée et qu’au moins l’un d’entre eux tente de modifier sa valeur. Par exemple, imaginez que nous ayons une valeur partagée a = 3 et deux processus A et B. Imaginez que le processus A veut ajouter 5 à la valeur actuelle de a alors que le processus B veut ajouter 2 à a seulement si a < 5. Selon le processus qui s’exécute en premier, le résultat ne sera pas celui attendu.Si le processus A s’exécute en premier, la valeur de a sera de 8 alors qu’elle sera de 10 si le processus B s’exécute first.To évitez les conditions de course, nous utilisons l’exclusion mutuelle. J’en parlerai plus en détail plus loin dans cet article.

Maintenant, permettez-moi d’expliquer le problème que j’ai eu.

Contexte

Un de mes projets à Theodo a consisté à construire une application NodeJS proposant plusieurs événements et concours pour ses clients. Il y avait aussi une fonctionnalité d’adhésion premium qui permettait aux utilisateurs de participer à des événements gratuits. Pour participer, il suffit aux utilisateurs de cliquer sur le bouton de participation sur la page de l’événement et ils reçoivent un ticket. Les participations multiples au même événement sont interdites en désactivant le bouton après le succès de la participation.

De plus, il y a un contrôle de sécurité dans le back-end pour empêcher les utilisateurs avec une commande payante existante d’obtenir un autre ticket. Vous pouvez voir le code simplifié ci-dessous:

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

Tout d’abord, je recherche dans la base de données un ordre existant lié à un utilisateur et à un événement. Si aucune commande n’existe, une nouvelle commande est créée et enregistrée dans la base de données. Sinon, rien n’est fait.

Cependant, certaines personnes ont réussi à obtenir plusieurs tickets en cliquant plusieurs fois rapidement sur ce bouton. C’était un problème de condition de race.

Quel était le problème exactement?

La condition précédente n’était pas suffisante. En effet, même si JavaScript est mono-threadé, cela n’empêche pas les conditions de course. Lorsque vous traitez des fonctions asynchrones, le thread ne bloque pas son exécution mais il exécute la ligne suivante qui ne dépend pas de l’appel asynchrone ou poursuit l’exécution correspondant à un événement de réponse. En conséquence, deux exécutions différentes peuvent s’entremêler.

Prenons l’exemple ci-dessous : un utilisateur fait 2 requêtes consécutives au back-end, et suppose qu’il n’a pas d’ordre correspondant à cet événement. Comme JavaScript est mono-thread, la pile d’exécution sera:

 Pile d'exécution

Mais l’ordre d’exécution des différentes lignes de la fonction sera:

 Exécution de code sans verrouillage

findOrder et createOrder sont des appels asynchrones puisqu’ils lisent et écrivent dans la base de données. En conséquence, les deux demandes s’entremêleront. Comme vous pouvez le voir sur la figure ci-dessus, la seconde findOrder est exécutée juste après celle de la première requête. Par conséquent, la deuxième évaluation de !existOrder sera true puisque l’appel a été effectué avant de créer une commande.

Conclusion: notre utilisateur recevra 2 tickets.

Solution

J’ai dû trouver un moyen de verrouiller cette partie du code pour exécuter toute la fonction avant d’autoriser une autre requête à exécuter le même code, et ainsi éviter les conditions de concurrence. Je l’ai fait en utilisant mutex, avec la bibliothèque async-mutex (vous pouvez l’installer en exécutant yarn add async-mutex).

Un mutex est un objet d’exclusion mutuelle qui crée une ressource qui peut être partagée entre plusieurs threads d’un programme. La ressource peut être vue comme un verrou qu’un seul thread peut acquérir. Si un autre thread veut acquérir le verrou, il doit attendre que le verrou soit relâché. Mais attention, le verrou doit toujours être libéré éventuellement, peu importe ce qui se passe pendant l’exécution. Sinon, cela entraînera des blocages et votre programme sera bloqué.

Afin de ne pas ralentir l’achat de tickets, j’ai utilisé un mutex par utilisateur, que j’ai stocké dans une carte. Si l’utilisateur n’est pas mappé à un mutex, une nouvelle instance est créée dans la carte en utilisant l’ID utilisateur comme clé, puis j’ai utilisé le mutex comme ceci :

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

Vous pouvez voir qu’avec le bloc try-catch-finally, je m’assure que le verrou sera toujours relâché. Maintenant, l’exécution ressemblera à la figure ci-dessous:

 Exécution de code avec lock

De cette façon, un utilisateur ne pourra participer qu’une seule fois.

Bien sûr, ce n’est pas le seul moyen de résoudre ce genre de problème. Les transactions sont également très utiles dans ce cas.

Gardez également à l’esprit qu’il a résolu le problème sur une seule instance de serveur. Si vous avez plusieurs serveurs, vous devez utiliser le mutex distribué pour informer tous les processus que le verrou est acquis.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.