ホームページ  >  記事  >  バックエンド開発  >  競合状態への対処: 実践的な例

競合状態への対処: 実践的な例

PHPz
PHPzオリジナル
2024-07-18 14:47:221113ブラウズ

キャリアの中で、シュレーディンガーの猫の問題、うまくいくこともあればうまくいかないこともある状況に遭遇するでしょう。競合状態はこれらの課題の 1 つです (そうです、1 つだけです!)。

このブログ投稿全体を通じて、実際の例を示し、問題を再現する方法を示し、PostgreSQL でシリアル化可能なトランザクション分離と勧告ロックを使用して競合状態を処理する戦略について説明します。

「データ集約型アプリケーションの設計」、第 7 章 - トランザクション「弱い分離レベル」からインスピレーションを得た

実践例を含む Github リポジトリ

アプリケーション

このアプリケーションは、病院の医師のオンコールシフトを管理します。競合状態の問題に焦点を当てるために、シナリオを単純化してみましょう。私たちのアプリは、この単一のテーブルを中心に解決します:

CREATE TABLE shifts (
    id SERIAL PRIMARY KEY,
    doctor_name TEXT NOT NULL,
    shift_id INTEGER NOT NULL,
    on_call BOOLEAN NOT NULL DEFAULT FALSE
);

重要なビジネス ルールがあります:

  • 各シフトには常に少なくとも 1 人の待機医師がいる必要があります。

ご想像のとおり、単純な API を実装すると、競合状態のシナリオが発生する可能性があります。次の仮定の状況を考えてみましょう:

ジャックとジョンは二人とも同じシフトで病院で当直中です。ほぼ同時に休暇申請を決定する。 1 つは成功しましたが、もう 1 つはシフトに勤務している医師の数に関する古い情報に依存しています。その結果、どちらも最終的にシフトを離れることになり、ビジネス ルールに違反し、待機医師がいない状態で特定のシフトを離れることになります。

John --BEGIN------doctors on call: 2-------leave on call-----COMMIT--------> (t)
          \                 \                      \             \
           \                 \                      \             \ 
Database ------------------------------------------------------------------> (t)
               /               /                      /              /
              /               /                      /              /
Jack ------BEGIN------doctors on call: 2-----leave on call----COMMIT-------> (t)

問題の再現

このアプリケーションは、Golang で実装されたシンプルな API です。この競合状態のシナリオを再現するスクリプトの実行方法については、GitHub リポジトリを確認してください。要約すると、次のことを行う必要があります:

  1. サーバーを起動します:yarn nxserve Hospital-shifts
  2. k6 テストを実行して競合状態シナリオを再現します:yarn nx test Hospital-shifts

テストは 2 人の医師を同時に呼び止めようとします。異なるアプローチでエンドポイントにアクセスします。shiftId=1勧告ロック を使用し、shiftId=2 は シリアル化可能なトランザクション分離、およびshiftId=3は、同時実行制御のない単純な実装です。

k6 の結果は、どのシフト ID がビジネス ルールに違反したかを示すカスタム メトリクスを出力します。


     ✓ at least one doctor on call for shiftId=1
     ✓ at least one doctor on call for shiftId=2
     ✗ at least one doctor on call for shiftId=3
      ↳  36% — ✓ 123 / ✗ 217
Yarn、Go、K6、Docker などのツールが必要になります。または、DevBox を使用してリポジトリの依存関係を簡単にセットアップできます。

競合状態への対処

この問題は、アプリケーションが古いデータに基づいて決定を下すときに発生します。これは、2 つのトランザクションがほぼ同時に実行され、どちらも医師のシフトをキャンセルしようとした場合に発生する可能性があります。 1 つのトランザクションは期待どおりに成功しますが、もう 1 つのトランザクションも古い情報に依存しているため、誤って成功します。この望ましくない動作を防ぐにはどうすればよいでしょうか?これを実現するにはいくつかの方法があります。ここでは PostgreSQL を利用した 2 つのオプションを検討します。ただし、同様のソリューションは他のデータベース管理システムでも見つかります。

シリアル化可能なトランザクションの分離

シリアル化可能なスナップショット分離は、アプリケーションによって示された書き込みスキューなどの異常を自動的に検出して防止します。

トランザクション分離の背後にある理論については深く掘り下げませんが、これは多くの一般的なデータベース管理システムで共通のトピックです。トランザクション分離に関する PostgreSQL 公式ドキュメントのこの記事のように、スナップショット分離を検索すると、適切な資料が見つかります。さらに、この解決策を数年前に提案した論文を次に示します。話は簡単なので、コードを見てみましょう:

まず、トランザクションを開始し、分離レベルを Serializable に設定します。


    // Init transaction with serializable isolation level
    tx, err := db.BeginTxx(c.Request().Context(),    &sql.TxOptions{
        Isolation: sql.LevelSerializable,
    })
その後、操作の実行に進みます。私たちの場合は、次の関数を実行します:


CREATE OR REPLACE FUNCTION update_on_call_status_with_serializable_isolation(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN)
RETURNS VOID AS $$
DECLARE
    on_call_count INT;
BEGIN
    -- Check the current number of doctors on call for this shift
    SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE;

    IF on_call_to_update = FALSE AND on_call_count = 1 THEN
        RAISE EXCEPTION '[SerializableIsolation] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update;
    ELSE
        UPDATE shifts s
        SET on_call = on_call_to_update
        WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update;
    END IF;

END;
$$ LANGUAGE plpgsql;
同時実行により矛盾したシナリオが発生した場合、シリアル化可能な分離レベルにより 1 つのトランザクションが成功し、他のトランザクションはこのメッセージとともに自動的にロールバックされるため、安全に再試行できます。


ERROR:  could not serialize access due to read/write dependencies among transactions
    完全な例は関数 updateWithSerializableIsolation にあります。
アドバイザリーロック

ビジネス ルールを確実に適用するもう 1 つの方法は、特定のシフトに対してリソースを明示的にロックすることです。これは、

トランザクション レベルでアドバイザリー ロックを使用して実現できます。このタイプのロックはアプリケーションによって完全に制御されます。詳細については、こちらをご覧ください。

ロックはセッション レベルとトランザクション レベルの両方で適用できることに注意することが重要です。ここで利用可能なさまざまな機能を探索できます。この例では、 pg_try_advisory_xact_lock(key bigint) → boolean を使用します。これにより、コミットまたはロールバック後にロックが自動的に解放されます。


BEGIN;

-- Attempt to acquire advisory lock and handle failure with EXCEPTION
    IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN
        RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update;
    END IF;

-- Perform necessary operations

-- Commit will automatically release the lock
COMMIT;
アプリケーションで使用される完全な関数は次のとおりです:


-- Function to Manage On Call Status with Advisory Locks, automatic release when the trx commits
CREATE OR REPLACE FUNCTION update_on_call_status_with_advisory_lock(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN)
RETURNS VOID AS $$
DECLARE
    on_call_count INT;
BEGIN
    -- Attempt to acquire advisory lock and handle failure with NOTICE
    IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN
        RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update;
    END IF;

    -- Check the current number of doctors on call for this shift
    SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE;

    IF on_call_to_update = FALSE AND on_call_count = 1 THEN
        RAISE EXCEPTION '[AdvisoryLock] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update;
    ELSE
        UPDATE shifts s
        SET on_call = on_call_to_update
        WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update;
    END IF;
END;
$$ LANGUAGE plpgsql;
  • You can find the complete example in the function updateWithAdvisoryLock.

Conclusion

Dealing with race conditions, like the write skew scenario we talked about, can be pretty tricky. There's a ton of research and different ways to solve these problems, so definitely check out some papers and articles if you're curious.

These issues can pop up in real-life situations, like when multiple people try to book the same seat at an event or buy the same spot in a theater. They tend to appear randomly and can be hard to figure out, especially if it's your first time dealing with them.

When you run into race conditions, it's important to look into what solution works best for your specific situation. I might do a benchmark in the future to compare different approaches and give you more insights.

I hope this post has been helpful. Remember, there are tools out there to help with these problems, and you're not alone in facing them!


Dealing with Race Conditions: A Practical Example iamseki / dev-to

Implementations of dev.to blog posts

以上が競合状態への対処: 実践的な例の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。