首頁  >  文章  >  後端開發  >  處理競爭條件:一個實際範例

處理競爭條件:一個實際範例

PHPz
PHPz原創
2024-07-18 14:47:221113瀏覽

在你的職業生涯中,你會遇到薛丁格的貓問題,這種情況有時有效,有時無效。競爭條件是這些挑戰之一(是的,只是其中之一!)。

在這篇文章中,我將展示一個真實的範例,示範如何重現問題並討論使用 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
);

我們有一條重要的商業規則:

  • 每個班次必須隨時有至少一名待命醫生。

如您可能已經猜到的,實作簡單的 API 可能會導致競爭條件情境。考慮這個假設的情況:

傑克和約翰在同一班次期間都在醫院待命。幾乎同時,他們決定請假。一種成功了,但另一種依賴有關有多少醫生輪班的過時資訊。結果,兩人最終都下班了,違反了業務規則,並在沒有值班醫生的情況下離開了特定的班次:

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

測試嘗試同時叫停兩名醫生,使用不同的方法到達端點:shiftId=1使用諮詢鎖shiftId=2使用可序列化交易隔離,且shiftId=3是一個簡單的實作,沒有並發控制。

k6結果將輸出自訂指標來指示哪個shiftId違反了業務規則:

     ✓ 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 更輕鬆地設定儲存庫依賴項。

解決競爭條件

當我們的應用程式根據陳舊資料做出決策時,就會出現問題。如果兩項交易幾乎同時運行並且都試圖叫停醫生輪班,則可能會發生這種情況。一筆交易按預期成功,但另一筆交易由於依賴過時的信息,也錯誤地成功。我們怎麼才能防止這種不良行為呢?有幾種方法可以實現這一點,我將探索 PostgreSQL 支援的兩個選項,儘管在其他資料庫管理系統中也可以找到類似的解決方案。

可串行化交易隔離

可序列化快照隔離會自動偵測並防止異常情況,例如我們的應用程式表現出的寫入偏差。

我不會深入探討事務隔離背後的理論,但它是許多流行資料庫管理系統中的常見主題。您可以透過搜尋快照隔離來找到很好的資料,例如 PostgreSQL 官方文件中關於交易隔離的這篇文章。此外,這是多年前提出此解決方案的論文。空談是廉價的,所以讓我們來看看程式碼:

首先,啟動交易並將隔離等級設定為Serialized:

    // 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;

每當因並發執行而出現不一致的情況時,可序列化隔離等級將允許一個交易成功,並自動回滾其他交易並顯示此訊息,因此您可以安全地重試:

ERROR:  could not serialize access due to read/write dependencies among transactions
  • 您可以在函數 updateWithSerializedIsolation 中找到完整的範例。

諮詢鎖

確保執行業務規則的另一種方法是明確鎖定特定班次的資源。我們可以在交易層級使用諮詢鎖來實現這一點。這種類型的鎖完全由應用程式控制。您可以在這裡找到更多相關資訊。

要注意的是,鎖定可以在會話層級和事務層級套用。您可以探索此處提供的各種功能。在我們的例子中,我們將使用 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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:單例設計模式下一篇:單例設計模式