本篇介紹 PostgreSQL 中的數據庫事務概念和 ACID 屬性,併發事務可能帶來的問題以及 4 種隔離級別,演示瞭如何使用事務控制語句(TCL)對事務進行處理,包括BEGIN
、COMMIT
、ROLLBACK
以及SAVEPOINT
語句。
數據庫事務
數據庫事務是由一個或者多個操作組成的工作單元。一個經典事務示例就是銀行賬戶之間的轉賬,它由發起方的扣款操作和接收方入賬操作組成,兩者必須都成功或者都失敗。
數據庫中的事務具有原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)以及持久性(Durability),也就是 ACID 屬性:
- 原子性保證事務中的操作要麼全部成功,要麼全部失敗,不會只成功一部分。比如從 A 賬戶轉出 1000 元到 B 賬戶,如果從 A 賬戶減去 1000 元成功執行,但是沒有往 B 賬戶增加 1000 元,意味着客戶將會損失 1000 元。用數據庫中的術語來說,這種情況導致了數據庫的不一致性。
- 一致性確保了數據修改的有效性,並且遵循一定的業務規則;例如,上面的銀行轉賬事務中如果一個賬戶扣款成功,但是另一個賬戶加錢失敗,那麼就會出現數據不一致(此時需要回滾已經執行的扣款操作)。另外,數據庫還必須保證滿足完整性約束,比如賬戶扣款之後不能出現餘額爲負數(可以在餘額字段上添加檢查約束)。
- 隔離性決定了併發事務之間的可見性和相互影響程度。例如,賬戶 A 向賬戶 B 轉賬的過程中,賬戶 B 查詢的餘額應該是轉賬之前的數目;如果多人同時向賬戶 B 轉賬,結果也應該保持一致性,就像依次轉賬的結果一樣。SQL 標準定義了 4 種不同的隔離級別,具體參考下文。
- 持久性確保已經提交的事務必須永久生效,即使發生斷電、系統崩潰等故障,數據庫都不會丟失數據。對於 PostgreSQL 而言,使用的是預寫式日誌(WAL)的機制實現事務的持久性。
事務控制語句
我們先來介紹一下 PostgreSQL 提供的事務控制語句,執行以下命令創建示例表:
CREATE TABLE accounts(
id serial PRIMARY KEY,
user_name varchar(50),
balance numeric(10,4)
);
ALTER TABLE accounts ADD CONSTRAINT bal_check CHECK(balance >= 0);
accounts 是一個簡化的賬戶表,主要包含用戶名和餘額信息。我們爲該表插入一條記錄:
insert into accounts(user_name, balance)
values ('UserA', 6000);
select * from accounts;
id|user_name|balance |
--|---------|---------|
1|UserA |6000.0000|
默認情況下,PostgreSQL 自動爲以上INSERT
語句開始一個事務,執行插入操作之後自動提交該事務。
不過,我們也可以手動控制事務的開始和提交。例如:
begin;
insert into accounts(user_name, balance)
values ('UserB', 0);
select * from accounts;
id|user_name|balance |
--|---------|---------|
1|UserA |6000.0000|
2|UserB | 0.0000|
其中,BEGIN
用於開始一個新的事務,PostgreSQL 中也可以使用BEGIN WORK
或者BEGIN TRANSACTION
開始事務;然後插入一條記錄,查詢顯示了兩條記錄。
如果此時打開另一個數據庫連接,查詢 accounts 表只能看到一條記錄。因爲上面的事務還沒有提交,事務的隔離性使得我們無法看到其他事務未提交的修改。
我們將上面的事務進行提交:
commit;
COMMIT
用於提交事務,也可以使用COMMIT WORK
或者COMMIT TRANSACTION
。此時,其他事務就能看到用戶 UserB 的記錄了。
事務除了可以提交之外,也可以被回滾。我們演示一下如何回滾事務:
begin;
insert into accounts(user_name, balance)
values ('UserC', 2000);
select * from accounts;
id|user_name|balance |
--|---------|---------|
1|UserA |6000.0000|
2|UserB | 0.0000|
3|UserC |2000.0000|
開始事務之後,我們又新增了一個賬戶沒有提交;此時可以回滾該事務:
rollback;
select * from accounts;
id|user_name|balance |
--|---------|---------|
1|UserA |6000.0000|
2|UserB | 0.0000|
ROLLBACK
用於回滾當前事務,也可以使用ROLLBACK WORK
或者ROLLBACK TRANSACTION
。回滾之後,事務中的數據修改都會被撤銷,賬戶 UserC 並沒有創建成功。
還有一個與事務控制相關的語句:SAVEPOINT
,用於在事務中定義保存點。例如:
begin;
insert into accounts(user_name, balance)
values ('UserC', 2000);
savepoint sv1;
insert into accounts(user_name, balance)
values ('UserD', 0);
rollback to sv1;
commit;
select * from accounts;
id|user_name|balance |
--|---------|---------|
1|UserA |6000.0000|
2|UserB | 0.0000|
4|UserC |2000.0000|
開始一個事務之後,先插入賬戶 UserC,然後定義了保存點 sv1;接着插入賬戶 UserD,然後回滾到保存點 sv1;此時賬戶 UserD 被撤銷,賬戶 UserC 仍然存在;最後提交事務。
併發與隔離
PostgreSQL 支持多用戶併發訪問,並且保證多個用戶同時訪問相同的數據時不會造成數據的不一致性。當多個用戶同時訪問相同的數據時,如果不進行任何隔離控制,可能導致以下問題:
- 髒讀(dirty read),一個事務能夠讀取其他事務未提交的修改。例如,B 的初始餘額爲 0;A 向 B 轉賬 1000 元但沒有提交;此時 B 能夠看到 A 轉過來的 1000 元,並且成功取款 1000 元;然後 A 取消了轉賬;銀行損失了 1000 元。
- 不可重複讀(nonrepeatable read),一個事務讀取某個記錄後,再次讀取該記錄時數據發生了改變(被其他事務修改並提交)。例如,B 查詢初始餘額爲 1000,取款 1000;同時 A 向 B 轉賬 1000 元並且提交;B 再次查詢發現餘額還是 1000 元,以爲取款機出錯了(當然,通過查詢流水記錄可以發現真相;數據庫的狀態仍然是一致的)。
- 幻讀(phantom read),一個事務按照某個條件查詢一些數據後,再次執行相同查詢時結果的數量發生了變化(另一個事務增加或者刪除了某些數據並且完成提交)。幻讀和非重複讀有點類似,都是由於其他事務修改數據導致的結果變化。
- 更新丟失(lost update),第一類:當兩個事務更新相同的數據時,第一個事務被提交,然後第二個事務被撤銷;那麼第一個事務的更新也會被撤銷(所有隔離級別都不允許發生這種情況)。第二類:當兩個事務同時讀取某一記錄,然後分別進行修改提交;就會造成先提交的事務的修改丟失。
爲了解決併發問題,SQL 標準定義了 4 種不同的事務隔離級別(從低到高):
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 | 更新丟失 |
---|---|---|---|---|
Read Uncommitted | 可能,但 PostgreSQL 不會 | 可能 | 可能 | 可能 |
Read Committed | 不可能 | 可能 | 可能 | 可能 |
Repeatable Read | 不可能 | 不可能 | 可能,但 PostgreSQL 不會 | 不可能 |
Serializable | 不可能 | 不可能 | 不可能 | 不可能 |
事務的隔離級別從低到高依次爲:
- Read Uncommitted(讀未提交):最低的隔離級別,實際上就是不隔離,任何事務都可以看到其他事務未提交的修改;該級別可能產生各種併發異常。不過,PostgreSQL 消除了 Read Uncommitted 級別時的髒讀,因爲它的實現等同於 Read Committed。
- Read Committed(讀已提交):一個事務只能看到其他事務已經提交的數據,解決了髒讀問題,但是存在不可重複讀、幻讀和第二類更新丟失問題。這是 PostgreSQL 的默認隔離級別。
- Repeated Read(可重複讀):一個事務對於同某個數據的讀取結果不變,即使其他事務對該數據進行了修改並提交;不過如果其他事務刪除了該記錄,則無法再查詢到數據(幻讀)。SQL 標準中的可重複讀可能出現幻讀,但是 PostgreSQL 在可重複讀級別消除了幻讀。
- Serializable(可串行化):最高的隔離級別,事務串行化執行,沒有併發。
只有 Serializable 真正實現了事務的完全隔離,但是不支持併發的數據庫系統應用場景非常有限。因此,需要對併發性能和隔離性進行平衡,大多數數據庫(包括 PostgreSQL)的默認隔離級別爲 Read Committed;此時,可以避免髒讀,同時擁有不錯的併發性能。
下面我們來演示一下 Read Committed 隔離級別下的併發事務處理,使用SHOW
命令可以查看當前的隔離級別:
show transaction_isolation;
transaction_isolation|
---------------------|
read committed |
如果需要修改當前事務的隔離級別,可以在事務的最開始執行SET TRANSACTION
命令:
begin;
SET TRANSACTION ISOLATION LEVEL { SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED };
...
下表演示了 PostgreSQL 默認級別(READ COMMITTED)時不會發生髒讀,但是存在不可重複讀、幻讀和更新丟失問題:
事務 1 | 事務 2 |
---|---|
begin; select balance from accounts where id = 1; -- 返回 6000 |
|
begin; update accounts set balance = balance + 1000 where id = 1; select balance from accounts where id = 1; -- 返回 7000 |
|
select balance from accounts where id = 1; -- 仍然返回 6000,沒有髒讀 |
|
commit; – 提交事務 | |
select balance from accounts where id = 1; -- 此時返回 7000,出現不可重複讀 |
|
select * from accounts where id=4; -- 返回 UserC | begin; delete from accounts where id = 4; commit; -- 刪除 UserC 並提交事務 |
select * from accounts where id=4; -- 沒有結果,出現幻讀 | |
select balance from accounts where id = 1; -- 此時返回 7000 |
|
begin; select balance from accounts where id = 1; -- 此時返回 7000 |
|
update accounts set balance = 6000 where id = 1; -- 更新爲 6000 | |
update accounts set balance = 8000 where id = 1; -- 等待事務 1 提交 | |
commit; | |
commt; | |
select balance from accounts where id = 1; -- 返回 8000,而不是自己修改成的 6000,更新丟失 |
在以上過程中,PostgreSQL 使用了鎖加 MVCC(Multiversion Concurrency Control)技術來實現數據的隔離和一致性。MVCC 簡單來說,就是保留每次數據修改之前的舊版本,根據隔離級別決定讀取哪個版本的數據。這種實現的最大好處就是讀操作永遠不會阻塞寫操作、寫操作永遠不會阻塞讀操作。
如果一個事務已經修改某個數據而且未提交,則另一個事務不允許同時修改該數據(必須等待),寫操作一定是相互阻塞的,需要按照順序執行。
對於業務開發人員來說,我們一般使用 PostgreSQL 的默認隔離級別,因爲它的 MVCC 實現消除了大部分的鎖等待問題。雖然此時可能產生不可重複讀、幻讀和更新丟失,但是並不會導致數據的不一致性,因爲這些都是其他事務的正常操作。
歡迎點贊👍、評論📝、收藏❤️!