《PostgreSQL 開發指南》第 23 篇 事務與併發控制

本篇介紹 PostgreSQL 中的數據庫事務概念和 ACID 屬性,併發事務可能帶來的問題以及 4 種隔離級別,演示瞭如何使用事務控制語句(TCL)對事務進行處理,包括BEGINCOMMITROLLBACK以及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 實現消除了大部分的鎖等待問題。雖然此時可能產生不可重複讀、幻讀和更新丟失,但是並不會導致數據的不一致性,因爲這些都是其他事務的正常操作。

歡迎點贊👍、評論📝、收藏❤️!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章