本篇以Postgresql為例,探討數據庫的事務、並發控制和鎖機制。
ACID
在關系型數據庫中,一個事務必須具備以下特性,簡稱ACID:
- 原子性(atomicity):事務必須以一個整體單元的形式工作,對於數據的修改要么全部執行,要么全部不執行;
- 一致性(consistency):事務在完成時,必須使所有的數據都保持一致狀態。比如a+b=10,當a改變時,b也將改變;a+b=10不變。
- 隔離性(isolation):一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對並發的其他事務是隔離的,並發執行的各個事務之間不能互相干擾。
- 持久性(durability):事務完成后,對系統的影響是永久的,即便之后機器重啟,斷電,數據也將一致保持
在Postgresql中使用多版本並發控制(MVCC)來維護數據的一致性。相較於鎖定模型,MVCC的主要優點是在MVCC里對讀數據的鎖請求與寫數據的鎖請求不沖突,讀不會阻塞寫,而寫也從不阻塞讀。
此外,PG與其他數據庫最大的區別在於,在PG中大多數DDL可以包含在一個事務中,並支持回滾;此功能使得PG尤其適合作為sharding分布式數據庫系統中的底層數據庫。
事務隔離級別
數據庫中存在以下四種隔離級別:
read uncommitted: 讀未提交
read committed :讀已提交(PostgreSQL中的默認隔離級別)
repeatable read: 重復讀
serializable: 串行化
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 | 序列化異常 |
read uncommitted | 允許,但不在PG中 | 可能 | 可能 | 可能 |
read committed | 不可能 | 可能 | 可能 | 可能 |
repeatable read | 不可能 | 不可能 | 允許,但不在PG中 | 可能 |
serializable | 不可能 | 不可能 | 不可能 | 不可能 |
概念解釋:
臟讀
一個事務讀取了另一個並行未提交事務寫入的數據。
不可重復讀
一個事務重新讀取之前讀取過的數據,發現該數據已經被另一個事務(在初始讀之后提交)修改。主要針對update
begin; select name from tab1 where id=111; #得到name是“張三” #此時另外一個事務對id=111的name更新成了“李四” select name from tab1 where id=111; #得到name是“李四”
幻讀
一個事務重新執行一個返回符合一個搜索條件的行集合的查詢, 發現滿足條件的行集合因為另一個最近提交的事務而發生了改變。主要針對insert 、delete
begin; select name from tab1 where id=111; #得到name是“張三” #此時另外一個事務對id=111的記錄增加一條“李四” select name from tab1 where id=111; #得到name是“李四”、“張三”
序列化異常
成功提交一組事務的結果與這些事務所有可能的串行執行結果都不一致。
在PostgreSQL中,你可以請求四種標准事務隔離級別中的任意一種,但是內部只實現了三種不同的隔離級別,即 PostgreSQL 的讀未提交模式的行為和讀已提交相同。這是因為把標准隔離級別映射到 PostgreSQL 的多版本並發控制架構的唯一合理的方法。
探討:
從上面可以看出不可重復讀和幻讀的區別在於:
不可重復讀重點在於更新,在數據庫控制方面只需要鎖住滿足條件的記錄(可以理解為行鎖);
幻讀重點在於刪除和插入,在數據庫控制方面需要鎖住滿足條件及其相近的記錄(可以理解為表鎖);
如果使用鎖機制來實現這兩種隔離級別,在可重復讀中,該SQL第一次讀取到數據后,就將這些數據加鎖,其它事務無法修改這些數據,就可以實現可重復讀了。但這種方法卻無法鎖住insert的數據,所以當事務A先前讀取了數據,或者修改了全部數據,事務B還是可以insert數據提交,這時事務A就會 發現莫名其妙多了一條之前沒有的數據,這就是幻讀,不能通過行鎖來避免。需要Serializable隔離級別 ,讀用讀鎖,寫用寫鎖,讀鎖和寫鎖互斥,這么做可以有效的避免幻讀、不可重復讀、臟讀等問題,但會極大的降低數據庫的並發能力。(悲觀鎖機制)
因此可以推斷出不可重復讀和幻讀的最大區別在於數據庫采用何種鎖機制來解決他們產生的問題;
在PG中(mysql、oracle也是)為了更高的性能,樂觀鎖為理論基礎的MVCC(多版本並發控制)來避免這兩種問題。
悲觀鎖
正如其名,它指的是對數據被外界(包括本系統當前的其他事務,以及來自外部系統的事務處理)修改持保守態度,因此,在整個數據處理過程中,將數據處 於鎖定狀態。悲觀鎖的實現,往往依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,否則,即使在本系統中實現了加鎖機 制,也無法保證外部系統不會修改數據)。
在悲觀鎖的情況下,為了保證事務的隔離性,就需要一致性鎖定讀。讀取數據時給加鎖,其它事務無法修改這些數據。修改刪除數據時也要加鎖,其它事務無法讀取這些數據。
樂觀鎖
相對悲觀鎖而言,樂觀鎖機制采取了更加寬松的加鎖機制。悲觀鎖大多數情況下依靠數據庫的鎖機制實現,以保證操作最大程度的獨占性。但隨之而來的就是數據庫性能的大量開銷,特別是對長事務而言,這樣的開銷往往無法承受。
而樂觀鎖機制在一定程度上解決了這個問題。樂觀鎖,大多是基於數據版本( Version )記錄機制實現。何謂數據版本?即為數據增加一個版本標識,在基於數據庫表的版本解決方案中,一般是通過為數據庫表增加一個 “version” 字段來實現。讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如 果提交的數據版本號大於數據庫表當前版本號,則予以更新,否則認為是過期數據。
兩階段提交
兩階段提交是分布式系統中保持事務原子性的關鍵
Postgresql中兩段式提交步驟如下:
<第一階段>
(1)應用程序先調用各台數據庫做一些操作,但不提交事務;然后調用事務協調器(這個協調器可能由應用自己實現)中的提交方法。
(2)事務協調器將聯絡事務中涉及的每台數據庫,並通知它們准備提交事務,這是第一階段的開始,PG中一般調用“PREAPARE TRANSACTION”命令。
(3)各台數據庫接受到“PREPARE TRANSACTION”命令后,如果要返回成功,則數據庫必須將自己置於以下狀態:確保后續能在被要求提交事務時提交事務,或者在被要求回滾事務時能夠回滾。因此PG會將已經准備好提交的信息寫入持久存儲區中。如果數據庫無法完成此事務,它會直接返回失敗給事務協調器
(4)事務協調器接收到所有數據庫的響應
<第二階段>
如果任一數據庫在第一階段返回失敗,則事務協調器將會發出“ROLLBACK RREPARED”命令給各個數據庫進行回滾;如果所有數據庫的響應都是成功,則發送“COMMIT PREPARED”命令進行提交。
注:在實際操作中需要將PG的參數“max_prepared_transactions”設置為一個大於零的數字,否則會報錯。
MVCC
MVCC是為了解決讀寫並發時數據不一致的問題,MVCC的方法是寫數據時,舊的版本數據不刪除,並發的讀還能讀到舊版本的數據,這樣就避免了數據不一致。
實現MVCC的方法有兩種:
- 第一種:寫新數據時,將舊數據移到一個單獨的地方,如回滾段中,其他人讀數據就從回滾段中把舊數據讀出來。
- 第二種:寫新數據時,舊數據不刪除不移動,而是把新數據插入。
Postgresql中使用的是第二種,而oracle和mysql中的innodb引擎使用的是第一種。
Postgresql中MVCC的實現
為了實現MVCC,每張表上都添加了四個系統字段:xmin、xmax、cmin、cmax
-
xmin:標記插入該行數據的事務ID。
-
xmax:標記刪除該行數據的事務ID。
- 新插入一行時,將新插入行的xmin填寫為當前的事務ID,xmax填0。
- 修改某一行時,實際上是新插入一行,舊行上的xmin不變,舊行上的xmax改為當前事務ID,新行上的xmin填為當前事務ID,新行上的xmax填為0。
- 刪除一行時,把被刪除行上的xmax填為當前事務ID。
- 關於事務ID:是一個32bit數字,從3開始遞增到最大值,之后再從3開始。
-
cmin:事務內部插入類操作的命令ID。
-
cmax:事務內部刪除類操作的命令ID。
- 每個命令使用事務內一個全局命令標識計數器的當前值作為當前命令標識。
- 事務開始時,命令標識計數器被置為初值0。
- 執行更新性的命令(insert、update、delete、select…for update)時,在SQL執行后命令標識計數器加1。
- 當命令標識計數器經過不斷累加又回到初值0時,報錯“cannot have more than 2^32-1 commands in a transaction”,即一個事務中的命令的個數最多為2^32-1個。
PG中的MVCC實現過程
- 當兩個事務同時訪問記錄時,通過參考xmin和xmax的標記可判斷記錄的版本,然后根據版本號與自己當前的事務標識進行比較,確定自己的數據權限。
- 當刪除數據時,記錄並沒有從數據塊中刪除,空間也沒有立即釋放。PostgreSQL通過運行vaccum進程來回收之前的存儲空間。默認PostgreSQL數據庫中的autovacuum是打開的,也就是說當一個表的更新達到一定數量時,autovacuum自動回收空間。
- 在PostgreSQL中,並不會在事務提交時把這些數據標記成有效,在事務回滾時標記為無效,如果事務提交或回滾時再次標記了數據,那這些數據就有可能會被刷新到磁盤中,而再次導致另一次I/O,從而降低了性能。PostgreSQL是通過記錄事務的狀態到commit log中來實現的。
PostgreSQL把事務狀態記錄在commit log(clog)中,事務的狀態有以下四種。
- TRANSACTION_STATUS_INPROGRESS=0x00表示事務正在進行中。
- TRANSACTION_STATUS_COMMITTED=0x01表示事務已經提交。
- TRANSACTION_STATUS_ABORTED=0x02表示事務已經回滾。
- TRANSACTION_STATUS_SUB_COMMITTED=0x03表示子事務已提交。
事務ID在PG中用xid表示,是一個32bit的數字,有以下三個特殊的事務ID給系統內部使用:
- InvalidTransactionId=0:表小是無效的事務ID
- BootstrapTransactionId:1:表示系統表初始化時的事務ID
- FrozenTransactionId=2:凍結的事務ID
事務ID會一直遞增,當達到最大值后再從頭開始,此時就會遇到事務ID回卷的問題。在PG中當事務ID達到2^31時,舊的事務ID就會變成一個特殊的事務ID,即FrozenTransactionId;當正常的事務ID與凍結的事務ID進行對比時,會認為正常事務ID比凍結事務ID新。
參考文獻
Postgresql從小工到專家