天天看點

PgSQL · 特性分析 · MVCC機制淺析

在資料庫中,并發的資料庫操作會面臨髒讀(Dirty Read)、不可重複讀(Nonrepeatable Read)、幻讀(Phantom Read)和串行化異常等問題,為了解決這些問題,在标準的SQL規範中對應定義了四種事務隔離級别:

- RU(Read uncommitted):讀未送出

- RC(Read committed):讀已送出

- RR(Repeatable read):重複讀

- SERIALIZABLE(Serializable):串行化

Isolation Level

Dirty Read

Nonrepeatable Read

Phantom Read

Serialization Anomaly

Read uncommitted

Allowed, but not in PG

Possible

Read committed

Not possible

Repeatable read

Serializable

需要注意的是,在PostgreSQL中:

- RU隔離級别不允許髒讀,實際上和Read committed一樣

- RR隔離級别不允許幻讀

在PostgreSQL中,為了保證事務的隔離性,實作資料庫的隔離級别,引入了MVCC(Multi-Version Concurrency Control)多版本并發控制。

一般MVCC有2種實作方法:

- 寫新資料時,把舊資料轉移到一個單獨的地方,如復原段中,其他人讀資料時,從復原段中把舊的資料讀出來,如Oracle資料庫和MySQL中的innodb引擎。

- 寫新資料時,舊資料不删除,而是把新資料插入。PostgreSQL就是使用的這種實作方法。

兩種方法各有利弊,相對于第一種來說,PostgreSQL的MVCC實作方式優缺點如下:

- 優點

- 無論事務進行了多少操作,事務復原可以立即完成

- 資料可以進行很多更新,不必像Oracle和MySQL的Innodb引擎那樣需要經常保證復原段不會被用完,也不會像oracle資料庫那樣經常遇到“ORA-1555”錯誤的困擾

- 缺點

- 舊版本的資料需要清理。當然,PostgreSQL 9.x版本中已經增加了自動清理的輔助程序來定期清理

- 舊版本的資料可能會導緻查詢需要掃描的資料塊增多,進而導緻查詢變慢

為了實作MVCC機制,必須要:

- 定義多版本的資料。在PostgreSQL中,使用元組頭部資訊的字段來标示元組的版本号

- 定義資料的有效性、可見性、可更新性。在PostgreSQL中,通過目前的事務快照和對應元組的版本号來判斷該元組的有效性、可見性、可更新性

- 實作不同的資料庫隔離級别

接下來,我們會按照上面的順序,首先介紹多版本元組的存儲結構,再介紹事務快照、資料可見性的判斷以及資料庫隔離級别的實作。

為了定義MVCC 中不同版本的資料,PostgreSQL在每個元組的頭部資訊HeapTupleHeaderData中引入了一些字段如下:

其中:

- t_heap存儲該元組的一些描述資訊,下面會具體去分析其字段

- t_ctid存儲用來記錄目前元組或新元組的實體位置

- 由塊号和塊内偏移組成

- 如果這個元組被更新,則該字段指向更新後的新元組

- 這個字段指向自己,且後面t_heap中的xmax字段為空,就說明該元組為最新版本

- t_infomask存儲元組的xmin和xmax事務狀态,以下是t_infomask每位分别代表的含義:

上文HeapTupleHeaderData中的t_heap存儲着元組的一些描述資訊,結構如下:

其中:

- t_xmin 存儲的是産生這個元組的事務ID,可能是insert或者update語句

- t_xmax 存儲的是删除或者鎖定這個元組的事務ID

- t_cid 包含cmin和cmax兩個字段,分别存儲建立這個元組的Command ID和删除這個元組的Command ID

- t_xvac 存儲的是VACUUM FULL 指令的事務ID

這裡需要簡單介紹下PostgreSQL中的事務ID:

- 順序産生,依次遞增

- 沒有資料變更,如INSERT、UPDATE、DELETE等操作,在目前會話中,事務ID不會改變

PostgreSQL主要就是通過t_xmin,t_xmax,cmin和cmax,ctid,t_infomask來唯一定義一個元組(t_xmin,t_xmax,cmin和cmax,ctid實際上也是一個表的隐藏的标記字段),下面以一個例子來表示元組更新前後各個字段的變化。

建立表test,插入資料,并查詢t_xmin,t_xmax,cmin和cmax,ctid屬性

更新test,并查詢t_xmin,t_xmax,cmin和cmax,ctid屬性

使用heap_page_items 方法檢視test表對應page header中的内容

從上面可知,實際上資料庫存儲了更新前後的兩個元組,這個過程中的資料塊中的變化大體如下:

PgSQL · 特性分析 · MVCC機制淺析

Tuple1更新後會插入一個新的Tuple2,而Tuple1中的ctid指向了新的版本,同時Tuple1的xmax從0變為1835,這裡可以被認為被标記為過期(隻有xmax為0的元組才沒過期),等待PostgreSQL的自動清理輔助程序回收掉。

也就是說,PostgreSQL通過HeapTupleHeaderData 的幾個特殊的字段,給元組設定了不同的版本号,元組的每次更新操作都會産生一條新版本的元組,版本之間從舊到新形成了一條版本鍊(舊的ctid指向新的元組)。

不過這裡需要注意的是,更新操作可能會使表的每個索引也産生新版本的索引記錄,即對一條元組的每個版本都有對應版本的索引記錄。這樣帶來的問題就是浪費了存儲空間,舊版本占用的空間隻有在進行VACCUM時才能被回收,增加了資料庫的負擔。

為了減緩更新索引帶來的影響,8.3之後開始使用HOT機制。定義符合下面條件的為HOT元組:

- 索引屬性沒有被修改

- 更新的元組新舊版本在同一個page中,其中新的被稱為HOT元組

更新一條HOT元組不需要引入新版本的索引,當通過索引擷取元組時首先會找到最舊的元組,然後通過元組的版本鍊找到HOT元組。這樣HOT機制讓擁有相同索引鍵值的不同版本元組共用一個索引記錄,減少了索引的不必要更新。

為了實作元組對事務的可見性判斷,PostgreSQL引入了事務快照SnapshotData,其具體資料結構如下:

這裡注意區分SnapshotData的xmin,xmax和HeapTupleFields的t_xmin,t_xmax

事務快照是用來存儲資料庫的事務運作情況。一個事務快照的建立過程可以概括為:

- 檢視目前所有的未送出并活躍的事務,存儲在數組中

- 選取未送出并活躍的事務中最小的XID,記錄在快照的xmin中

- 選取所有已送出事務中最大的XID,加1後記錄在xmax中

- 根據不同的情況,指派不同的satisfies,建立不同的事務快照

其中根據xmin和xmax的定義,事務和快照的可見性可以概括為:

- 當事務ID小于xmin的事務表示已經被送出,其涉及的修改對目前快照可見

- 事務ID大于或等于xmax的事務表示正在執行,其所做的修改對目前快照不可見

- 事務ID處在 [xmin, xmax)區間的事務, 需要結合活躍事務清單與事務送出日志CLOG,判斷其所作的修改對目前快照是否可見,即SnapshotData中的satisfies。

satisfies是PostgreSQL提供的對于事務可見性判斷的統一操作接口。目前在PostgreSQL 10.0中具體實作了以下幾個函數:

HeapTupleSatisfiesMVCC:判斷元組對某一快照版本是否有效

HeapTupleSatisfiesUpdate:判斷元組是否可更新

HeapTupleSatisfiesDirty:判斷目前元組是否已髒

HeapTupleSatisfiesSelf:判斷tuple對自身資訊是否有效

HeapTupleSatisfiesVacuum:用在VACUUM,判斷某個元組是否對任何正在運作的事務可見,如果是,則該元組不能被VACUUM删除

HeapTupleSatisfiesAny:所有元組都可見

HeapTupleSatisfiesHistoricMVCC:用于CATALOG 表

上述幾個函數的參數都是 (HeapTuple htup, Snapshot snapshot, Buffer buffer),其具體邏輯和判斷條件,本文不展開具體讨論,有興趣的可以參考《PostgreSQL資料庫核心分析》的7.10.2 MVCC相關操作。

- 當vacuum時,可以直接跳過這些page

- 進行index-only scan時,可以先檢查下Visibility Map。這樣減少fetch tuple時的可見性判斷,進而減少IO操作,提高性能

另外visibility map相對整個relation,還是小很多,可以cache到記憶體中。

PostgreSQL中根據擷取快照時機的不同實作了不同的資料庫隔離級别(對應代碼中函數GetTransactionSnapshot):

讀未送出/讀已送出:每個query都會擷取最新的快照CurrentSnapshotData

重複讀:所有的query 擷取相同的快照都為第1個query擷取的快照FirstXactSnapshot

串行化:使用鎖系統來實作

為了保證事務的原子性和隔離性,實作不同的隔離級别,PostgreSQL引入了MVCC多版本機制,概括起就是:

- 通過元組的頭部資訊中的xmin,xmax以及t_infomask等資訊來定義元組的版本

- 通過事務送出日志來判斷目前資料庫各個事務的運作狀态

- 通過事務快照來記錄目前資料庫的事務總體狀态

- 根據使用者設定的隔離級别來判斷擷取事務快照的時間

如上文所講,PostgreSQL的MVCC實作方法有利有弊。其中最直接的問題就是表膨脹,為了解決這個問題引入了AutoVacuum自動清理輔助程序,将MVCC帶來的垃圾資料定期清理,這部分内容我們将在下期月報進行分析,敬請期待。