比賽條件。當 2 個執行緒嘗試在表中的同一實體中更新時
我們都知道比賽條件是我們應該避免的。當使用多個執行緒時,我們不應該嘗試訪問相同的變數或記憶體。
但我有一些棘手的案例。假設我們有一個使用者表。
user id | name | age 1 | Paul | 14
如果一個執行緒嘗試更新“name”列,而另一個執行緒嘗試更新“age”怎麼辦?
這總是安全的嗎?
附加問題,
假設有一些非常糟糕的查詢需要 20 秒才能得到結果。
如果一個執行緒執行這個 SQL 查詢,而另一個執行緒想要執行另一個 SQL 查詢。另一個執行緒應該等待 20 秒?(直到上一個查詢完成它的工作)
大多數關係數據庫管理系統,例如 MySQL,都符合 ACID。ACID 原則中的A和I代表原子性和隔離性。原子性意味著事務完全發生或根本不發生(完全回滾)。隔離意味著事務彼此隔離執行,當它們同時需要訪問同一資源時,基於隔離級別的鎖定機制用於適當地處理該資源上的互動。
由於符合 ACID 的數據庫系統的這兩個屬性,您可能指的通常的程式競爭條件通常是不可能的。隔離級別和鎖定係統可防止同一資源同時發生變化。根據情況(例如,兩個寫入事務,或一個寫入和讀取事務,或兩個讀取事務)和實現的隔離級別,在通常可能發生競爭條件的情況下,會發生鎖定以確保正確的順序突變資源狀態的副本或資源狀態的副本被適當地維護、訪問和變異。
現在,如果您沒有正確設計您的軟體,您仍然可以創建一個*合乎邏輯的競爭條件。*例如,對於您的
user
表,如果您有兩個單獨的查詢,您的應用程序可以執行如下所示:UPDATE user SET name = 'Jon' WHERE name = 'Paul' AND age = 20;
和:
UPDATE user SET age = 21 WHERE name = 'Paul';
因為它們是兩個單獨的查詢,會產生兩個單獨的事務,所以這些事務將彼此隔離並相互獨立地執行。
因此,如果您的目標是按照我上面提出的查詢順序定期更新數據,首先將所有 20 歲的 Pauls 更改為 Johns,然後將其餘 Pauls 的年齡更改為 21 歲,這一切都很好,只要因為兩個單獨的查詢始終按該順序執行,並且第一個查詢從未遇到錯誤。
但是在最終使用者可以在任何時間以任何順序執行任何一個的應用程序中,您可能會遇到邏輯競爭條件,您首先將所有 Paul 的年齡更新為 21 歲,然後將之前的年齡更新為 21 歲。 20 歲將永遠不會被更新為命名為約翰。或者更糟糕的是,如果第一個查詢首先執行並失敗,但第二個查詢隨後成功執行,您將遇到相同的邏輯競爭條件。
您可以對查詢進行編碼以防止這種邏輯競爭條件的方法是將兩者都包裝在顯式事務中。例如:
START TRANSACTION; UPDATE user SET name = 'Jon' WHERE name = 'Paul' AND age = 20; UPDATE user SET age = 21 WHERE name = 'Paul'; COMMIT;
上面現在保證了第一個查詢總是在執行第二個查詢之前首先執行完成。關鍵字是完成,因為如果第一個查詢失敗,那麼第二個查詢將不會執行,而是回滾整個顯式事務,防止出現邏輯競爭條件。
請參閱此StackOverflow 答案中特定於 MySQL 的 ACID 屬性、隔離級別和事務的更多資訊。
請注意,由於沒有更好的術語,我在這個答案中使用資源鬆散地表示數據庫系統上下文中的任何對象、實體或*數據片段。*這是因為上述資訊對於通過 DML 語句(例如
UPDATE
等)的數據突變或通過 DDL 語句(例如ALTER TABLE
等)在數據庫內的對象本身的突變都是正確的。