分享到

簡介

事務是資料庫將相關語句組合在一起,作為系統的一個獨立單元執行的方式。這些語句作為一個單元處理,所有組成部分要麼成功完成,要麼回滾到原始狀態。這是在需要多個獨立步驟才能實現一個目標時,保持資料一致性的一種方式。

本文將討論什麼是事務以及它們的用處。然後我們將看到 MySQL 如何以各種方式使用事務來精確控制語句如何應用於資料庫。

什麼是事務?

事務是一種將多個語句分組並隔離起來,作為單個操作進行處理的方式。在事務中,命令不是在傳送到伺服器時單獨執行,而是捆綁在一起並在與其他請求分離的上下文中執行。

隔離是事務的重要組成部分。在事務內部,執行的語句只能影響事務本身的環境。從事務內部看,語句可以修改資料,結果立即可見。從外部看,在事務提交之前,不會進行任何更改,此時事務內的所有操作將同時變得可見。

這些特性透過提供原子性(事務中的操作要麼全部提交,要麼全部回滾)和隔離性(在事務外部,直到提交前沒有任何變化,而在內部,每個語句都有即時後果)來幫助資料庫實現 ACID 合規性。這些共同幫助資料庫保持一致性(透過保證不會發生部分資料轉換)。此外,事務中的更改在提交到非易失性儲存之前不會被認為是成功的,這提供了永續性

為了實現這些目標,事務採用了多種不同的策略,不同的資料庫系統使用不同的方法。MySQL 使用一種稱為多版本併發控制 (MVCC) 的系統,它允許資料庫使用資料快照執行這些操作。總而言之,這些系統構成了現代關係型資料庫的基本構建塊之一,使其能夠以抗崩潰的方式安全地處理複雜資料。

一致性失敗的型別

人們使用事務的原因之一是獲得關於資料一致性及其處理環境的某些保證。一致性可以透過許多不同的方式被破壞,這影響了資料庫如何嘗試阻止它們。

根據事務實現的不同,可能出現四種主要的不一致性方式。您對這些場景可能出現的容忍度將影響您如何在應用程式中使用事務。

髒讀

髒讀發生在事務內的語句能夠讀取其他正在進行中的事務寫入的資料時。這意味著即使事務的語句尚未提交,它們也可以被讀取並因此影響其他事務。

這通常被認為是嚴重的一致性破壞,因為事務之間沒有正確隔離。可能永遠不會提交到資料庫的語句會影響其他事務的執行,從而修改它們的行為。

允許髒讀的事務無法對結果資料的一致性做出任何合理的宣告。

不可重複讀

不可重複讀發生在事務外部的提交更改了事務內部可見的資料時。如果在事務內部,相同的資料被讀取兩次但每次檢索到的值不同,則可以識別出這種型別的問題。

與髒讀一樣,允許不可重複讀的事務不能提供事務之間的完全隔離。區別在於,對於不可重複讀,影響事務的語句實際上已在事務外部提交。

幻讀

幻讀是一種特殊型別的不可重複讀,它發生在查詢返回的行在事務內第二次執行時不同。

例如,如果事務內的查詢第一次執行時返回四行,而第二次執行時返回五行,這就是幻讀。幻讀是由事務外部的提交更改了滿足查詢條件的行數引起的。

序列化異常

序列化異常發生在多個併發提交的事務的結果與它們一個接一個提交的結果不同時。這可能發生在任何時候,當一個事務允許兩個提交發生,每個都修改相同的表或資料而不解決衝突。

事務隔離級別

事務並非“一刀切”的解決方案。不同的場景需要在效能和保護之間進行不同的權衡。幸運的是,MySQL 允許您指定所需的事務隔離型別。

大多數資料庫系統提供的隔離級別包括以下內容:

讀未提交 (Read uncommitted)

讀未提交 (Read uncommitted) 是提供最少資料一致性和隔離性保證的隔離級別。雖然使用 read uncommitted 的事務具有一些與事務相關的特性,例如能夠一次性提交多個語句或在發生錯誤時回滾語句,但它們確實允許在許多情況下破壞一致性。

配置了 read uncommitted 隔離級別的事務允許:

  • 髒讀
  • 不可重複讀
  • 幻讀
  • 序列化異常

此隔離級別通常不推薦使用,因為它幾乎不提供任何資料一致性和隔離性保證。這主要適用於您需要將語句分組以實現“全有或全無”的提交模型,但不需要任何完整性保證的情況(這種情況非常罕見)。

讀已提交 (Read committed)

讀已提交 (Read committed) 是一種專門防止髒讀的隔離級別。當事務使用 read committed 一致性級別時,未提交的資料永遠不會影響事務的內部上下文。這透過確保未提交的資料永遠不會影響事務來提供基本的一致性級別。

儘管 read committed 提供比 read uncommitted 更大的保護,但它並不能防止所有型別的不一致。以下問題仍然可能出現:

  • 不可重複讀
  • 幻讀
  • 序列化異常

可重複讀 (Repeatable read)

可重複讀 (repeatable read) 隔離級別建立在 read committed 提供的保證之上。它像以前一樣避免髒讀,同時也能防止不可重複讀。

儘管 repeatable read 可以防止不可重複讀和髒讀,但它仍然可能受到以下隔離問題的影響:

  • 幻讀
  • 序列化異常

在大多數情況下,幻讀也會被阻止,但在某些情況下它們可能仍然發生。在連結的示例中,事務中的 SELECT 查詢返回空結果,即使在另一事務插入並提交了一行之後。然而,執行一個將更新新行的查詢會導致查詢返回資料,即使更新不應該知道由另一事務提交的行。此後,SELECT 查詢會返回資料。

對於 MySQL 的 InnoDB 引擎(大多數情況下的預設引擎),可重複讀隔離方法是預設的。

序列化 (Serializable)

序列化 (Serializable) 隔離級別提供了最高級別的隔離和一致性。它阻止了 repeatable read 級別所能阻止的所有場景,同時消除了序列化異常的可能性。

序列化隔離保證併發事務的提交如同它們一個接一個地執行一樣。如果發生可能引入序列化異常的場景,其中一個事務將出現序列化失敗,而不是向資料集引入不一致。

定義事務

現在我們已經介紹了 MySQL 中事務可以使用的不同隔離級別,接下來我們演示如何定義事務。

在 MySQL 中,預設情況下,顯式標記的事務外部的每個語句實際上都在其自己的單語句事務中執行。要顯式啟動事務塊,您可以使用 START TRANSACTIONBEGIN 命令。START TRANSACTION 形式可以接受 BEGIN 形式不能接受的修飾符,因此 MySQL 建議您使用 START TRANSACTION。要提交事務,請發出 COMMIT 命令。

因此,事務的基本語法如下所示:

START TRANSACTION;
statements
COMMIT;

舉一個更具體的例子,假設我們正在嘗試將 1000 美元從一個賬戶轉移到另一個賬戶。我們希望確保這筆錢始終在一個賬戶中,但絕不會同時在兩個賬戶中。

我們可以將封裝此轉賬的兩個語句包裝在一個事務中,如下所示:

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;
COMMIT;

在這裡,如果沒有將 1000 美元存入 id = 2 的賬戶,則不會從 id = 1 的賬戶中取出 1000 美元。雖然這兩個語句在事務內部是按順序執行的,但它們將同時提交,從而同時在底層資料集上執行。

回滾事務

在一個事務中,所有語句要麼全部提交到資料庫,要麼全部不提交。放棄在事務中進行的語句和修改而不將其應用於資料庫被稱為“回滾”事務。

事務可以自動回滾或手動回滾。如果事務中的某個語句導致錯誤或在其他情況下為避免問題,MySQL 會自動回滾事務。

要手動回滾當前事務中已發出的語句,您可以使用 ROLLBACK 命令。這將取消事務中的所有語句,實質上是將時間倒回到事務開始時。

例如,假設我們正在使用之前使用的相同銀行賬戶示例,如果我們發出 UPDATE 語句後發現我們不小心轉賬了錯誤的金額或使用了錯誤的賬戶,我們可以回滾更改而不是提交它們:

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE id = 3; -- Wrong account number here! Must rollback
/* Gets us back to where we were before the transaction started */
ROLLBACK;

一旦我們 ROLLBACK,1500 美元將仍然在 id = 1 的賬戶中。

回滾時使用儲存點

預設情況下,ROLLBACK 命令會將事務重置到最初呼叫 START TRANSACTIONBEGIN 命令時的狀態。但是,如果我們只想還原事務中的部分語句怎麼辦?

雖然在發出 ROLLBACK 命令時不能指定任意回滾位置,但您可以回滾到您在事務中設定的任何“儲存點”。您可以使用 SAVEPOINT 命令提前在事務中標記位置,然後在需要回滾時引用這些特定位置。

這些儲存點允許您建立中間回滾點。然後,您可以選擇性地回滾您當前位置和儲存點之間所做的任何語句,然後繼續處理您的事務。

要指定一個儲存點,請發出 SAVEPOINT 命令,後跟儲存點的名稱:

SAVEPOINT save_1;

要回滾到該儲存點,請使用 ROLLBACK TO 命令:

ROLLBACK TO save_1;

我們繼續使用之前關於賬戶的例子:

START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
/* Set a save point that we can return to */
SAVEPOINT save_1;
UPDATE accounts
SET balance = balance + 1500
WHERE id = 3; -- Wrong account number here! We can rollback to the save point though!
/* Gets us back to the state of the transaction at `save_1` */
ROLLBACK TO save_1;
/* Continue the transaction with the correct account number */
UPDATE accounts
SET balance = balance + 1500
WHERE id = 4;
COMMIT;

在這裡,我們能夠從犯下的錯誤中恢復,而不會丟失迄今為止在事務中完成的所有工作。回滾後,我們按照計劃使用正確的語句繼續事務。

設定事務的隔離級別

要為事務設定所需的隔離級別,您可以使用帶有 ISOLATION LEVEL 子句的 SET TRANSACTION 語句。SET TRANSACTION 語句允許您修改事務的隔離級別以及讀寫許可權。

預設情況下,SET TRANSACTION 語句僅影響下一個啟動的事務的屬性。它必須在 START TRANSACTIONBEGIN 語句之前給出。基本語法如下:

SET TRANSACTION ISOLATION LEVEL <isolation_level>;
START TRANSACTION;
statements
COMMIT;

<isolation_level> 可以是以下任何一種(前面已詳細描述):

  • READ UNCOMMITTED
  • READ COMMITTED
  • REPEATABLE READ(MySQL 的預設操作模式)
  • SERIALIZABLE

在發出命令時,您還可以提供關鍵詞 GLOBALSESSION 來影響不同的作用域。

如果您輸入 SET GLOBAL TRANSACTION ISOLATION LEVEL,隔離級別將全域性更改為所有未來的會話。任何當前會話將使用舊的隔離級別,因此如果您需要使用事務級別或 SESSION 級別更改這些會話,請務必明確修改這些作用域。

SESSION 修飾符(如 SET SESSION TRANSACTION ISOLATION LEVEL)允許您更改同一會話中任何未來事務的隔離級別。同樣,這不影響任何現有事務。

您可以使用 SET TRANSACTION 修改的另一個方面是事務是讀寫能力還是隻讀。預設情況下,MySQL 中的事務具有讀寫能力。您可以使用 SET TRANSACTION READ ONLY 使會話只讀。如果您想明確將會話設定為讀寫,您也可以發出 SET TRANSACTION READ WRITE

鏈式事務

如果您有多個應該按順序執行的事務,您可以選擇使用 COMMIT AND CHAIN 命令將它們連結在一起。

COMMIT AND CHAIN 命令透過提交其中的語句來完成當前事務。提交處理完成後,它會立即以相同的隔離級別開啟一個新事務。這允許您將另一組語句分組到一個事務中。

該語句的作用與您發出 COMMIT; SET TRANSACTION ISOLATION LEVEL <isolation_level>; START TRANSACTION 完全相同。

SET TRANSACTION ISOLATION LEVEL READ COMMITTED
START TRANSACTION;
UPDATE accounts
SET balance = balance - 1500
WHERE id = 1;
UPDATE accounts
SET balance = balance + 1500
WHERE id = 2;
/* Commit the data and start a new transaction that will take into account the committed data and isolation level from the last transaction */
COMMIT AND CHAIN;
UPDATE accounts
SET balance = balance - 1000
WHERE id = 2;
UPDATE accounts
SET balance = balance + 1000
WHERE id = 3;
COMMIT;

鏈式事務有助於建立多個事務,而無需每次都明確設定事務級別或修改會話或全域性預設值。

結論

事務提供了一些有用的功能,可以幫助您的資料保持一致狀態。然而,各種隔離級別之間存在一些權衡,您應該儘量牢記。確定適用於您用例的適當保護級別可能需要一些探索和思考。如果事務執行時間很長,這一點尤其重要,因為在提交發生之前資料庫可能會發生顯著變化,這可能導致回滾和更多手動工作。

即使存在缺點,事務在許多場景中仍然很有用,因為它們對關係型資料庫應提供的 ACID 保證做出了重大貢獻。瞭解何時使用它們,哪種隔離級別有意義,以及如何避免自動回滾是值得投入的知識。

關於作者
Justin Ellingwood

Justin Ellingwood

Justin 自 2013 年以來一直撰寫關於資料庫、Linux、基礎設施和開發者工具的文章。他目前與妻子和兩隻兔子住在柏林。他通常不必用第三人稱寫作,這讓所有相關方都鬆了一口氣。
© . This site is unofficial and not affiliated with Prisma Data, Inc.