分享到

簡介

交易是資料庫將相關語句分組在一起,作為系統的單一單元執行的方法。 這些語句被視為一個單元處理,所有組成部分要么成功完成,要么恢復到其原始狀態。 這是為需要多個個別步驟才能實現單一目標的變更維護一致性的一種方法。

在本文中,我們將討論什麼是交易以及它們的用途。 然後,我們將了解 MySQL 如何以各種方式使用交易來精確控制語句如何應用於資料庫。

什麼是交易?

交易是一種將多個語句分組和隔離在一起,以便作為單一操作處理的方法。 在交易中,命令不是像發送到伺服器時那樣單獨執行每個命令,而是將命令捆綁在一起,並在與其他請求不同的上下文中執行。

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

這些功能通過提供原子性(交易中的動作要么全部提交,要么全部回滾)和隔離性(在交易外部,在提交之前沒有任何變更,而在內部,每個語句都有直接的後果),幫助資料庫實現 ACID 合規性。 這些共同幫助資料庫維護一致性(通過保證不會發生部分資料轉換)。 此外,在交易中的變更提交到非揮發性儲存之前,不會將其返回為成功,這提供了持久性

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

一致性失敗的類型

人們使用交易的一個原因是為了獲得關於其資料一致性以及處理資料的環境的某些保證。 一致性可能會以許多不同的方式被破壞,這會影響資料庫嘗試阻止它們的方式。

根據交易實作,可能出現四種主要的不一致性方式。 您對這些情況可能發生的情境的容忍度將影響您在應用程式中使用交易的方式。

髒讀

髒讀發生在交易中的語句能夠讀取其他進行中交易寫入的資料時。 這表示即使交易的語句尚未提交,它們也可以被讀取,從而影響其他交易。

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

允許髒讀的交易無法對結果資料的一致性做出任何合理的聲明。

不可重複讀

不可重複讀發生在交易外部的提交變更了交易內看到的資料時。 如果在交易中,相同的資料被讀取兩次,但在每個實例中檢索到不同的值,您可以識別出此類問題。

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

幻讀

幻讀是一種特定類型的不可重複讀,當查詢返回的列在交易中第二次執行時不同時發生。

例如,如果交易中的查詢第一次執行時返回四列,但第二次執行時返回五列,則這是幻讀。 幻讀是由於交易外部的提交變更了滿足查詢的列數而引起的。

序列化異常

序列化異常發生在同時提交的多個交易的結果會導致與它們依序提交不同的結果時。 只要交易允許兩個提交發生,每個提交都修改相同的表格或資料而不解決衝突,就可能發生這種情況。

交易隔離級別

交易並非「一體適用」的解決方案。 不同的情境需要在效能和保護之間進行不同的權衡。 幸運的是,MySQL 允許您指定所需的交易隔離類型。

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

讀取未提交

讀取未提交是提供的關於維護資料一致性和隔離性最少保證的隔離級別。 雖然使用 read uncommitted 的交易具有通常與交易相關的某些功能,例如一次提交多個語句或在發生錯誤時回滾語句的能力,但它們確實允許許多可能破壞一致性的情況。

使用 read uncommitted 隔離級別配置的交易允許

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

通常不建議使用此隔離級別,因為它幾乎不提供關於資料一致性和隔離性的保證。 如果您需要將語句分組到「全有或全無」的提交模型中,但不需要任何完整性保證(非常罕見的情況),則這主要有用。

讀取已提交

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

雖然 read committedread uncommitted 提供更大的保護,但它不能防止所有類型的不一致性。 這些問題仍然可能出現

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

可重複讀

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

雖然 repeatable read 可以防止不可重複讀和髒讀,但它仍然可能遭受這些隔離問題

  • 幻讀
  • 序列化異常

在大多數情況下,幻讀也將被阻止,但在某些情況下,它們仍然可能發生。 在連結的範例中,即使在另一個交易插入和提交列之後,交易中的 SELECT 查詢也沒有返回結果。 但是,發出將更新新列的查詢會導致查詢返回資料,即使更新不應知道由另一個交易提交的列。 此後,SELECT 查詢返回資料。

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

可序列化

可序列化隔離級別提供最高級別的隔離性和一致性。 它防止了 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

自 2013 年以來,Justin 一直在撰寫關於資料庫、Linux、基礎架構和開發人員工具的文章。 他目前與妻子和兩隻兔子住在柏林。 他通常不必以第三人稱寫作,這對所有相關方來說都是一種解脫。