lidar com condições de corrida em NodeJS usando Mutex

mesmo que o JavaScript seja de thread único, pode haver problemas de simultaneidade como condições de corrida devido ao assíncrono. Este artigo tem como objetivo contar como lidei com isso usando a exclusão mútua. Mas primeiro, vamos lembrar o que é uma condição de corrida antes de entrar no problema e dizer como eu resolvi isso.

condição de Corrida

Uma condição de corrida ocorre quando vários processos tentam acessar um mesmo recurso compartilhado e pelo menos um deles tenta modificar o seu valor. Por exemplo, imagine que temos um valor compartilhado a = 3 e dois processos a e B. Imagine que o processo a deseja adicionar 5 ao valor atual de a, enquanto o processo B deseja adicionar 2 a a apenas se a < 5. Dependendo de qual processo é executado primeiro, o resultado não será o esperado.Se o processo a for executado primeiro, o valor de a Será 8, enquanto será 10 Se o processo B for executado first.To evite condições de corrida, usamos exclusão mútua. Vou falar sobre isso mais detalhadamente mais adiante neste artigo.

agora deixe-me explicar o problema que tive.

contexto

um dos meus projetos na Teodo consistiu em construir um aplicativo NodeJS que propusesse vários eventos e Concursos para seus clientes. Havia também um recurso de Associação premium que permitia aos usuários participar de eventos gratuitos. Para participar, os usuários só precisam clicar no botão participação na página do evento e recebem um ingresso. Várias participações no mesmo evento não são permitidas desativando o botão após o sucesso da participação.

além disso, há uma verificação de segurança no back-end para evitar que usuários com uma ordem paga existente obtenham outro ticket. Você pode ver o código simplificado abaixo:

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

primeiro, procuro no banco de dados uma ordem existente relacionada a um usuário e a um evento. Se nenhuma ordem existir, uma nova ordem será criada e salva no banco de dados. Caso contrário, nada é feito.

no entanto, algumas pessoas conseguiram obter vários ingressos clicando muitas vezes rapidamente neste botão. Este foi um problema de condição de corrida.

qual foi o problema exatamente ?

a condição anterior não foi suficiente. De fato, mesmo que o JavaScript seja mono-threaded, ele não impede as condições de corrida. Quando você lida com funções assíncronas, o thread não bloqueia sua execução, mas executa a próxima linha que não depende da chamada assíncrona ou continua a execução correspondente a um evento de resposta. Como resultado, duas execuções diferentes podem se entrelaçar.

tome o exemplo abaixo: um usuário faz 2 Pedidos consecutivos para o back-end, e suponha que ele ou ela não tem ordem correspondente a este evento. Como o JavaScript é mono-rosca, a pilha de execução será:

pilha de Execução

Mas a ordem de execução das diferentes linhas da função será:

a execução de Código sem bloqueio

findOrder e createOrder são chamadas assíncronas pois de leitura e gravação no banco de dados. Como consequência, os dois pedidos se entrelaçam. Como você pode ver na figura acima, o segundo findOrder é executado logo após o da primeira solicitação. Portanto, a segunda avaliação de !existOrder será true, uma vez que a chamada foi feita antes de criar um pedido.

conclusão: nosso Usuário receberá 2 ingressos.

solução

tive que encontrar uma maneira de bloquear esta parte do código para executar toda a função antes de permitir que outra solicitação executasse o mesmo código e, portanto, evitar condições de corrida. Eu fiz isso usando mutex, com a biblioteca async-mutex (você pode instalá-lo executando yarn add async-mutex).

um mutex é Um objeto de exclusão mútua, que cria um recurso que pode ser compartilhado entre vários threads de um programa. O recurso pode ser visto como um bloqueio que apenas um thread pode adquirir. Se outro thread quiser adquirir o bloqueio, ele terá que esperar até que o bloqueio seja liberado. Mas cuidado para que o bloqueio sempre seja liberado eventualmente, não importa o que aconteça durante a execução. Caso contrário, isso levará a deadlocks e seu programa será bloqueado.

para não desacelerar a compra de tickets, usei um mutex por usuário, que armazenei em um mapa. Se o Usuário não estiver mapeado para um mutex, uma nova instância será criada no mapa usando o ID do usuário como uma chave e, em seguida, usei o mutex assim :

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

você pode ver que, com o bloco try-catch-finally, garanto que o bloqueio sempre será liberado. Agora, a execução será semelhante à figura abaixo:

execução de código com bloqueio

desta forma, um usuário só poderá participar uma vez.

claro, esta não é a única maneira de resolver esse tipo de problema. As transações também são realmente úteis neste caso.

além disso, tenha em mente que resolveu o problema em uma única instância do servidor. Se você tiver vários servidores, use o mutex distribuído para informar a todos os processos que o bloqueio foi adquirido.

Deixe uma resposta

O seu endereço de email não será publicado.