zvládněte podmínky závodu v NodeJS pomocí mutexu

i když je JavaScript jednovláknový, mohou se vyskytnout problémy se souběžností, jako jsou závodní podmínky kvůli asynchronismu. Tento článek si klade za cíl říct, jak jsem to zvládl pomocí vzájemného vyloučení. Ale nejprve si vzpomeňme, co je to stav závodu, než se dostaneme do problému a řekneme vám, jak jsem to vyřešil.

podmínka závodu

podmínka závodu nastane, když se více procesů pokusí získat přístup ke stejnému sdílenému prostředku a alespoň jeden z nich se pokusí změnit jeho hodnotu. Představte si například, že máme sdílenou hodnotu a = 3 a dva procesy A A B. Představte si, že proces a chce přidat 5 k aktuální hodnotě a, zatímco proces B chce přidat 2 K a, pouze pokud a < 5. V závislosti na tom, který proces se provede jako první, výsledek nebude očekávaný.Pokud proces a provede první, hodnota a bude 8, zatímco to bude 10, pokud proces B provede first.To vyhněte se závodním podmínkám, používáme vzájemné vyloučení. Budu o tom mluvit podrobněji později v tomto článku.

nyní mi dovolte vysvětlit problém, který jsem měl.

kontext

jeden z mých projektů v Theodo spočíval ve vytvoření aplikace NodeJS, která navrhla Více akcí a soutěží pro své zákazníky. K dispozici byla také funkce prémiového členství, která uživatelům umožňovala účastnit se bezplatných akcí. Chcete-li se zúčastnit, stačí kliknout na tlačítko účasti na stránce události a obdrží lístek. Vícenásobné účasti na stejné akci jsou zakázány vypnutím tlačítka po úspěchu účasti.

kromě toho je v back-endu bezpečnostní kontrola, která zabraňuje uživatelům s existujícím placeným příkazem získat další lístek. Zjednodušený kód můžete vidět níže:

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

nejprve Hledám v databázi existující objednávku související s uživatelem a událostí. Pokud žádný příkaz neexistuje, vytvoří se nový účet a uloží se do databáze. Jinak se nic neděje.

nicméně, někteří lidé se podařilo získat několik vstupenek kliknutím mnohokrát rychle na toto tlačítko. Jednalo se o problém závodních podmínek.

jaký byl přesně problém ?

předchozí podmínka nestačila. Opravdu, i když je JavaScript mono-threaded, nebrání závodním podmínkám. Když se zabýváte asynchronními funkcemi, vlákno neblokuje jeho spuštění, ale buď provede další řádek, který nezávisí na asynchronním volání, nebo pokračuje v provádění odpovídající události odpovědi. V důsledku toho se mohou prolínat dvě různé popravy.

Vezměte si příklad níže: uživatel provede 2 po sobě jdoucí požadavky na back-end a předpokládejme, že nemá žádnou objednávku odpovídající této události. Protože JavaScript je mono-threaded, bude execution stack:

Execution stack

ale pořadí provádění různých řádků funkce bude:

provedení kódu bez zámku

findOrder a createOrder jsou asynchronní volání, protože čtou a zapisují do databáze. V důsledku toho se obě žádosti prolínají. Jak vidíte na obrázku výše, druhý findOrder se provede hned po prvním požadavku. Proto bude druhé vyhodnocení !existOrder true, protože volání bylo provedeno před vytvořením objednávky.

závěr: náš uživatel obdrží 2 vstupenky.

řešení

musel jsem najít způsob, jak uzamknout tuto část kódu, abych vykonal celou funkci, než povolím další požadavek na provedení stejného kódu, a tak se vyhnu závodním podmínkám. Udělal jsem to pomocí mutexu s knihovnou async-mutex (můžete ji nainstalovat spuštěním yarn add async-mutex).

mutex je objekt vzájemného vyloučení, který vytváří prostředek, který lze sdílet mezi více vlákny programu. Zdroj lze považovat za zámek, který může získat pouze jedno vlákno. Pokud chce zámek získat další vlákno, musí počkat, až se zámek uvolní. Ale pozor, že zámek by měl být vždy uvolněn nakonec, bez ohledu na to, co se stane během popravy. V opačném případě to povede k zablokování a váš program bude zablokován.

abych nezpomalil nákup vstupenek, použil jsem jeden mutex na uživatele, který jsem uložil do mapy. Pokud uživatel není namapován na mutex, vytvoří se v mapě nová instance pomocí id uživatele jako klíče a potom jsem použil mutex takto :

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

můžete vidět, že s blokem try-catch-finally zajistím, že zámek bude vždy uvolněn. Nyní bude provedení vypadat jako na obrázku níže:

spuštění kódu se zámkem

tímto způsobem se uživatel bude moci zúčastnit pouze jednou.

samozřejmě to není jediný způsob řešení tohoto druhu problému. Transakce jsou v tomto případě také velmi užitečné.

mějte také na paměti, že problém vyřešil na jedné instanci serveru. Pokud máte více serverů, měli byste pomocí distribuovaného mutexu informovat všechny procesy o získání zámku.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna.