Mutexを使用してNodeJSの競合条件を処理する

JavaScriptがシングルスレッドであっても、非同期性による競合条件のような並行性の問題がある可能性があり この記事は、相互排除を使用してこれをどのように処理したかを説明することを目的としています。 しかし、最初に、問題に入り、私がそれをどのように解決したかを伝える前に、競合状態が何であるかを思い出してみましょう。

競合状態

競合状態は、複数のプロセスが同じ共有リソースにアクセスしようとし、少なくとも一方がその値を変更しようとすると発生します。 たとえば、共有値a=3と2つのプロセスAとBがあるとします。a<5の場合にのみ、プロセスAがaの現在の値に5を追加し、プロセスBがaに2を追 どのプロセスが最初に実行されるかによって、結果は期待されるものではありません。プロセスAが最初に実行された場合、aの値は8になりますが、プロセスBが実行された場合は10になりますfirst.To 競合状態を避け、我々は相互排除を使用します。 この記事の後半で詳しく説明します。

さて、私が持っていた問題を説明しましょう。

私のプロジェクトの一つは、顧客のために複数のイベントやコンテストを提案するNodeJSアプリケーションを構築することでした。 また、ユーザーが無料のイベントに参加できるプレミアムメンバーシップ機能もありました。 参加するには、イベントページの参加ボタンをクリックするだけで、チケットを受け取ることができます。 同じイベントへの複数の参加は、参加が成功した後にボタンを無効にすることによって許可されません。

さらに、バックエンドには、既存の有料注文を持つユーザーが別のチケットを取得するのを防ぐためのセキュリティチェックがあります。 以下の簡略化されたコードを見ることができます:

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

まず、ユーザーとイベントに関連する既存の注文をデータベースで検索します。 注文が存在しない場合は、新しい注文が作成され、データベースに保存されます。 それ以外の場合は、何も行われません。

しかし、このボタンを何度も素早くクリックすることで、いくつかのチケットを手に入れることができた人もいました。 これは競合状態の問題でした。

正確には何が問題でしたか?

以前の条件では十分ではなかった。 実際、JavaScriptがモノスレッドであっても、競合状態を防ぐことはできません。 非同期関数を処理する場合、スレッドは実行をブロックしませんが、非同期呼び出しに依存しない次の行を実行するか、応答イベントに対応する実 その結果、二つの異なる実行が絡み合うことができます。

以下の例を取る:ユーザーがバックエンドに2つの連続した要求を行い、彼または彼女はこのイベントに対応する順序を持っていないと仮定します。 JavaScriptはモノスレッドであるため、実行スタックは次のようになります:

実行スタック

しかし、関数の異なる行の実行順序は次のようになります:

ロックなしでのコード実行

findOrder そしてcreateOrderは、データベースの読み取りと書き込みを行うため、非同期呼び出しです。 結果として、2つの要求が絡み合ってしまいます。 上の図でわかるように、2番目のfindOrderは最初のリクエストの直後に実行されます。 したがって、注文を作成する前に呼び出しが行われているため、!existOrderの2番目の評価はtrueになります。

結論:私たちのユーザーは2枚のチケットを受け取ります。

解決策

別の要求が同じコードを実行できるようにする前に、コードのこの部分をロックして関数全体を実行する方法を見つけなければならなかったので、競合条件を避けなければなりませんでした。 これは、async-mutexライブラリを使用してmutexを使用して行いました(yarn add async-mutexを実行してインストールできます)。

ミューテックスは、プログラムの複数のスレッド間で共有できるリソースを作成する相互排他オブジェクトです。 リソースは、1つのスレッドのみが取得できるロックと見なすことができます。 別のスレッドがロックを取得する場合は、ロックが解放されるまで待機する必要があります。 しかし、実行中に何が起こっても、ロックは常に最終的に解放されるべきであることに注意してください。 それ以外の場合は、デッドロックにつながり、プログラムはブロックされます。

チケットの購入を遅くしないために、マップに保存したユーザーごとに一つのミューテックスを使用しました。 ユーザーがミューテックスにマップされていない場合、ユーザー idをキーとして使用してマップに新しいインスタンスが作成され、次のようにミューテックスを使 :

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

try-catch-finallyブロックを使用すると、ロックが常に解放されることがわかります。 これで、実行は次の図のようになります:

ロック

を使用したコード実行この方法では、ユーザーは一度だけ参加することができます。

もちろん、この種の問題を解決する唯一の方法ではありません。 トランザクションは、この場合にも本当に便利です。

また、単一のサーバーインスタンスで問題を解決したことに注意してください。 複数のサーバーがある場合は、分散ミューテックスを使用して、ロックが取得されたことをすべてのプロセスに知らせる必要があります。

コメントを残す

メールアドレスが公開されることはありません。