簡介
交易是一種機制,將多個陳述式封裝到單一操作中,供資料庫處理。資料庫不需個別輸入陳述式,而是能夠將指令群組解釋並作為一個協調的單元來執行。這有助於確保資料集在許多密切相關的陳述式過程中保持一致性。
在本指南中,我們將首先討論什麼是交易以及它們為何有益。之後,我們將看看 PostgreSQL 如何實作交易,以及使用交易時您擁有的各種選項。
什麼是交易?
交易是一種將多個陳述式組合和隔離在一起,作為單一操作處理的方式。在交易中,指令不是像發送到伺服器時那樣個別執行,而是捆綁在一起,並在與其他請求不同的上下文中執行。
隔離是交易的重要組成部分。在交易內部,執行的陳述式只能影響交易本身的環境。從交易內部來看,陳述式可以修改資料,並且結果會立即可見。從外部來看,在交易提交之前不會進行任何變更,此時交易內的所有動作會立即變得可見。
這些功能透過提供原子性(交易中的動作要么全部提交,要么全部回滾)和隔離性(在交易外部,在提交之前不會發生任何變更,而在內部,陳述式會產生後果),幫助資料庫實現 ACID 相容性。這些共同幫助資料庫維持一致性(透過保證不會發生部分資料轉換)。此外,在交易中的變更提交到非揮發性儲存裝置之前,不會回傳為成功,這提供了持久性。
為了實現這些目標,交易採用了許多不同的策略,不同的資料庫系統使用不同的方法。PostgreSQL 使用一種稱為多版本並行控制 (MVCC) 的系統,該系統允許資料庫使用資料快照執行這些動作,而無需不必要的鎖定。總之,這些系統構成了現代關聯式資料庫的基本建構區塊之一,使其能夠以抗崩潰的方式安全地處理複雜資料。
一致性失敗的類型
人們使用交易的一個原因是為了獲得關於資料一致性及其處理環境的某些保證。一致性可能會以許多不同的方式被破壞,這會影響資料庫嘗試防止它們的方式。
根據交易實作方式,主要有四種可能發生不一致的情況。您對這些情況可能發生的容忍度將影響您在應用程式中使用交易的方式。
髒讀
髒讀發生在交易中的陳述式能夠讀取其他進行中交易寫入的資料時。這表示即使交易的陳述式尚未提交,它們也可以被讀取,進而影響其他交易。
這通常被認為是嚴重違反一致性的行為,因為交易彼此之間沒有適當隔離。可能永遠不會提交到資料庫的陳述式可能會影響其他交易的執行,從而修改它們的行為。
允許髒讀的交易無法對結果資料的一致性做出任何合理的聲明。
不可重複讀取
不可重複讀取發生在交易外部的提交變更了交易內看到的資料時。如果在交易中,相同的資料被讀取兩次,但在每次實例中檢索到不同的值,您可以識別出此類問題。
與髒讀一樣,允許不可重複讀取的交易不提供交易之間的完全隔離。不同之處在於,對於不可重複讀取,影響交易的陳述式實際上已在交易外部提交。
幻讀
幻讀是一種特定類型的不可重複讀取,當查詢傳回的列在交易內第二次執行時不同時發生。
例如,如果交易中的查詢第一次執行時傳回四列,但第二次執行時傳回五列,則這是幻讀。幻讀是由交易外部的提交變更了滿足查詢的列數引起的。
序列化異常
序列化異常發生在多個並行提交的交易的結果,與它們一個接一個提交的結果不同時。當交易允許兩個提交發生,並且每個提交都修改相同的表格或資料而沒有解決衝突時,都可能發生這種情況。
序列化異常是一種特殊類型的問題,早期的交易類型對此毫無概念。這是因為早期的交易是使用鎖定實作的,如果另一個交易正在讀取或變更同一筆資料,則一個交易無法繼續。
交易隔離等級
交易並非「一體適用」的解決方案。不同的情境需要在效能和保護之間進行不同的權衡。幸運的是,PostgreSQL 允許您指定所需的交易隔離類型。
大多數資料庫系統提供的隔離等級包括以下幾種
讀取未提交
讀取未提交是提供的關於維護資料一致性和隔離性保證最少的隔離等級。雖然使用 read uncommitted
的交易具有通常與交易相關聯的某些功能,例如一次提交多個陳述式或在發生錯誤時回滾陳述式的能力,但它們確實允許許多可能破壞一致性的情況。
使用 read uncommitted
隔離等級配置的交易允許
- 髒讀
- 不可重複讀取
- 幻讀
- 序列化異常
此隔離等級實際上未在 PostgreSQL 中實作。雖然 PostgreSQL 識別隔離等級名稱,但在內部,它實際上不受支援,而是會使用「讀取已提交」(如下所述)。
讀取已提交
讀取已提交是一種隔離等級,專門用於防止髒讀。當交易使用 read committed
一致性等級時,未提交的資料永遠不會影響交易的內部上下文。這透過確保未提交的資料永遠不會影響交易來提供基本的一致性等級。
雖然 read committed
比 read uncommitted
提供更高的保護,但它不能防止所有類型的不一致。這些問題仍然可能發生
- 不可重複讀取
- 幻讀
- 序列化異常
如果未指定其他隔離等級,PostgreSQL 將預設使用 read committed
等級。
可重複讀取
可重複讀取隔離等級建立在 read committed
提供的保證之上。它像以前一樣避免了髒讀,但也防止了不可重複讀取。
這表示在交易外部提交的任何變更都永遠不會影響交易內讀取的資料。除非直接由交易內的陳述式引起,否則在交易開始時執行的查詢在交易結束時永遠不會有不同的結果。
雖然 repeatable read
隔離等級的標準定義僅要求防止髒讀和不可重複讀取,但 PostgreSQL 也在此等級防止幻讀。這表示交易外部的提交無法變更滿足查詢的列數。
由於在交易中看到的資料狀態可能與資料庫中最新的資料不同,因此如果兩個資料集無法協調,交易可能會在提交時失敗。因此,此隔離等級的一個缺點是,如果提交時發生序列化失敗,您可能必須重試交易。
PostgreSQL 的 repeatable read
隔離等級阻止了大多數類型的一致性問題,但序列化異常仍然可能發生。
可序列化
可序列化隔離等級提供最高等級的隔離性和一致性。它防止了 repeatable read
等級所做的所有情境,同時也消除了序列化異常的可能性。
可序列化隔離保證並行交易的提交如同它們是一個接一個執行的。如果發生可能引入序列化異常的情境,其中一個交易將發生序列化失敗,而不是將不一致性引入資料集。
定義交易
現在我們已經介紹了 PostgreSQL 可以在交易中使用的不同隔離等級,讓我們示範如何定義交易。
在 PostgreSQL 中,每個在明確標記的交易之外的陳述式實際上都在其自己的單一陳述式交易中執行。若要明確啟動交易區塊,您可以使用 BEGIN
或 START TRANSACTION
指令(它們是同義詞)。若要提交交易,請發出 COMMIT
指令。
因此,交易的基本語法如下所示
BEGIN;statementsCOMMIT;
作為更具體的範例,假設我們嘗試將 1000 美元從一個帳戶轉移到另一個帳戶。我們希望確保這筆錢始終在兩個帳戶之一中,但永遠不會同時在兩個帳戶中。
我們可以將共同封裝此轉帳的兩個陳述式包裝在如下所示的交易中
BEGIN;UPDATE accountsSET balance = balance - 1000WHERE id = 1;UPDATE accountsSET balance = balance + 1000WHERE id = 2;COMMIT;
在這裡,除非同時將 1000 美元放入 id = 2
的帳戶中,否則不會從 id = 1
的帳戶中取出 1000 美元。雖然這兩個陳述式在交易內部循序執行,但它們將被提交,因此將同時在基礎資料集上執行。
回滾交易
在交易中,所有陳述式或沒有任何陳述式將被提交到資料庫。放棄交易中進行的陳述式和修改,而不是將它們應用於資料庫,稱為「回滾」交易。
交易可以自動或手動回滾。如果交易中的一個陳述式導致錯誤,PostgreSQL 會自動回滾交易。如果選擇的隔離等級不允許序列化錯誤,它也會回滾交易。
若要手動回滾在目前交易期間給出的陳述式,您可以使用 ROLLBACK
指令。這將取消交易中的所有陳述式,本質上是將時間倒轉到交易開始時。
例如,假設我們使用與之前相同的銀行帳戶範例,如果我們在發出 UPDATE
陳述式後發現我們意外轉帳了錯誤的金額或使用了錯誤的帳戶,我們可以回滾變更而不是提交它們
BEGIN;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE 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
指令將交易重設為首次呼叫 BEGIN
或 START TRANSACTION
指令時的狀態。但是,如果我們只想還原交易中的某些陳述式怎麼辦?
雖然您無法在發出 ROLLBACK
指令時指定要回滾到的任意位置,但您可以回滾到您在整個交易過程中設定的任何「儲存點」。您可以提前使用 SAVEPOINT
指令標記交易中的位置,然後在需要回滾時參考這些特定位置。
這些儲存點允許您建立中間回滾點。然後,您可以選擇還原您目前位置和儲存點之間進行的任何陳述式,然後繼續處理您的交易。
若要指定儲存點,請發出 SAVEPOINT
指令,後跟儲存點的名稱
SAVEPOINT save_1;
若要回滾到該儲存點,請使用 ROLLBACK TO
指令
ROLLBACK TO save_1;
讓我們繼續我們一直在使用的以帳戶為中心的範例
BEGIN;UPDATE accountsSET balance = balance - 1500WHERE id = 1;/* Set a save point that we can return to */SAVEPOINT save_1;UPDATE accountsSET balance = balance + 1500WHERE 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 accountsSET balance = balance + 1500WHERE id = 4;COMMIT;
在這裡,我們能夠從我們犯的錯誤中恢復,而不會丟失我們到目前為止在交易中所做的所有工作。回滾後,我們使用正確的陳述式繼續按計劃進行交易。
設定交易的隔離等級
若要設定您希望交易使用的隔離等級,您可以將 ISOLATION LEVEL
子句新增至您的 START TRANSACTION
或 BEGIN
指令。基本語法如下所示
BEGIN ISOLATION LEVEL <isolation_level>;statementsCOMMIT;
<isolation_level>
可以是以下任何一種(在前面詳細說明過)
READ UNCOMMITTED
(由於此等級未在 PostgreSQL 中實作,因此將導致READ COMMITTED
)READ COMMITTED
REPEATABLE READ
SERIALIZABLE
SET TRANSACTION
指令也可以用於在交易啟動後設定隔離等級。但是,您只能在執行任何查詢或資料修改指令之前使用 SET TRANSACTION
,因此它不允許提高靈活性。
鏈式交易
如果您有多個應該循序執行的交易,您可以選擇使用 COMMIT AND CHAIN
指令將它們鏈接在一起。
COMMIT AND CHAIN
指令透過提交其中的陳述式來完成目前的交易。在處理完提交後,它會立即開啟一個新交易。這允許您將另一組陳述式組合在一個交易中。
該陳述式的工作方式與您發出 COMMIT; BEGIN
完全相同
BEGIN;UPDATE accountsSET balance = balance - 1500WHERE id = 1;UPDATE accountsSET balance = balance + 1500WHERE id = 2;/* Commit the data and start a new transaction that will take into account the committed from the last transaction */COMMIT AND CHAIN;UPDATE accountsSET balance = balance - 1000WHERE id = 2;UPDATE accountsSET balance = balance + 1000WHERE id = 3;COMMIT;
鏈式交易在功能方面沒有提供太多新功能,但它對於在自然邊界提交資料,同時繼續專注於相同類型的操作可能很有幫助。
結論
交易並非萬靈丹。各種隔離等級都有許多權衡,並且了解您需要保護哪些類型的一致性可能需要思考和規劃。對於長時間運行的交易尤其如此,在這種情況下,基礎資料可能會發生顯著變化,並且與其他並行交易發生衝突的可能性會增加。
話雖如此,交易機制提供了很大的彈性和效能。它在確保即使在執行相互關聯的並行操作時也能維持 ACID 保證方面大有幫助。了解何時以及如何正確使用交易來執行複雜、安全的操作非常寶貴。
如果您使用 JavaScript 或 TypeScript,可以使用 Prisma 來管理您的 PostgreSQL 資料庫。任何使用 transactions API 的操作都將使用 PostgreSQL 伺服器的預設隔離等級。除了互動式地使用 transactions,Prisma 也透過巢狀寫入以及批次或大量操作提供 transaction 行為。您可以閱讀 Prisma 的 transaction 指南以了解更多資訊。