簡介
事務是一種將多個語句封裝成資料庫可處理的單個操作的機制。資料庫能夠將這組命令作為一個內聚單元進行解釋和執行,而不是逐個輸入單個語句。這有助於確保在許多密切相關的語句執行過程中資料集的一致性。
在本指南中,我們將首先討論什麼是事務以及它們為何有益。之後,我們將探討 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 美元不會在不將 1000 美元轉入 id = 2 的賬戶的情況下從 id = 1 的賬戶中取出。雖然這兩個語句在事務
回滾事務
在一個事務中,所有語句都將提交到資料庫,或者都不提交。放棄事務中進行的語句和修改,而不是將其應用於資料庫,這被稱為“回滾”事務。
事務可以自動或手動回滾。如果事務中的某個語句導致錯誤,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;
在這裡,我們能夠在不丟失迄今為止在事務中完成的所有工作的情況下,從我們所犯的錯誤中恢復。回滾後,我們按照計劃使用正確的語句繼續事務。
設定事務的隔離級別
要設定事務的隔離級別,您可以在 START TRANSACTION 或 BEGIN 命令中新增 ISOLATION LEVEL 子句。基本語法如下:
BEGIN ISOLATION LEVEL <isolation_level>;statementsCOMMIT;
<isolation_level> 可以是以下任何一種(前面已詳細描述):
READ UNCOMMITTED(將導致READ COMMITTED,因為 PostgreSQL 未實現此級別)READ COMMITTEDREPEATABLE READSERIALIZABLE
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 資料庫。任何使用 事務 API 的操作都將使用 PostgreSQL 伺服器的預設隔離級別。作為互動式使用事務的替代方案,Prisma 還透過巢狀寫入和批次操作提供事務行為。您可以閱讀Prisma 的事務指南瞭解更多資訊。
