PG的併發控制

PG的併發控制


MVCC併發控制
MVCC
S2PL
OCC
PCC
基於鎖定的併發控制
X鎖
Exlusive Locks排他鎖
事務A鎖定了T表,可以進行訪問和修改
事務B不能對T表加鎖,也不能進行訪問和修改
S鎖
Share locks共享鎖
事務A獲取了T表共享鎖,事務A可以對對象T進行訪問
事務B獲取T表的共享鎖

​ Adivsory lock

PG的鎖控制–PCC

1 PostgreSQL中的多版本併發控制-MVCC

MVCC , Multi - Version Concurrency Control , 多版本控制併發

1.1 爲什麼需要MVCC

數據庫在併發操作下,如果數據正在寫,而用戶又在讀,可能會出現數據不一致的問題,比如一行數據只寫入了前半部分,後半部分還沒有寫入,而此時用戶讀取這行數據時就會出現前半部分是新數據,後半部分是舊數據的現象,造成前後數據不一致問題,解決這個問題最好的方法就是讀寫加鎖,寫的時候不允許讀,讀的時候不允許寫,不過這樣就降低了數據庫的併發性能,因此便引入了MVCC的概念,它的目的便是實現讀寫事務相互不阻塞,從而提高數據庫的併發性能。

1.2 不同的MVCC機制

實現MVCC的機制有兩種:

1、寫入數據時,把舊版本數據移到其他地方,如回滾等操作,在回滾中把數據讀出來。

2、寫入數據庫時,保留舊版本的數據,並插入新數據

像oracle數據庫使用的是第一種方式,postgresql使用的是第二種方式。

1.3 MVCC 設計的幾個概念

HeapTupleHeaderData佈局

類型 長度 描述
t_xmin TransactionId 4 bytes 插入XID標誌
t_xmax TransactionId 4 bytes 刪除XID標誌
t_cid CommandId 4 bytes 插入和/或刪除CID標 志(覆蓋t_xvac)
t_xvac TransactionId 4 bytes VACUUM操作移動一個 行版本的XID
類型 長度 描述
t_ctid ItemPointerData 6 bytes 當前版本的TID或者指 向更新的行版本
t_infomask2 uint16 2 bytes 一些屬性,加上多個 標誌位
t_infomask uint16 2 bytes 多個標誌位
t_hoff uint8 1 byte 到用戶數據的偏移量

cmin和cmax插入和刪除元組的事務序列標識
Xmin和xmax事務對其他事務的可見性

1、事務ID

在postgresql中,每個事務都存在一個唯一的ID,也稱爲xid,可通過txid_current()函數獲取當前的事務ID,取值大小爲42億,邏輯上無限
txd視爲一個環形狀標識
tid不會在BEGN;開始分配,只有在一個BEGN中,執行第一條命令的時候,事務管理器纔會分配id,並啓動一個事務

txid保留的三個tid
0標識無效的tid
1標識啓動的tid( initdb)
2標識凍結的xid( freeze)
對於td=1382大於1382的tid,屬於未來,小於td=1382,是屬於過去的
屬於未來的d對於wd=1382的事務是不可見的
屬於過去的d對於tid=1382的事務是可見的
txid邏輯上無限
4B42億

2、tupe

每一行數據,稱爲一行元祖,一個tupe

3、ctid

tuple中的隱藏字段,代表tuple的物理位置

4、xmin

tuple 中的隱藏字段,在創建一個tuple時,記錄此值爲當前的事務ID

5、xmax

tuple 中的隱藏字段,默認爲0,在刪除時,記錄此值爲當前的事務的ID

6、cmin/cmax

tuple中的隱藏字段,表示同一個事務中多個語句的順序,從0開始

1.4 MVCC的工作機制

Postgresql中的MVCC就是通過以上幾個隱藏字段協作同實現的,下面舉幾個例子來看下工作機制:

1.4.1 插入數據實例

1、首先我們開啓事務插入一條數據,其中ctid代表數據的物理位置,xmin爲當前事務ID,xmax爲0

postgre=# create table test(id int,name varchar(50));
CREATE TABLE
postgre=#  begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          587
(1 row)

postgre=#  insert into test(id,name) values(1,'a');
INSERT 0 1
postgre=#  insert into test(id,name) values(2,'b');
INSERT 0 1
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,1) |  587 |    0 |    0 |    0 |  1 | a
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
(2 rows)

postgre=# commit
postgre-# ;
COMMIT
postgre=#
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,1) |  587 |    0 |    0 |    0 |  1 | a
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
(2 rows)

postgre=#

繼續在上一個事務中再插入一條數據,因爲在同一個事務中,可以看到cmin,cmax按順序增長

1.4.2 修改數據實例

修改ID爲1的數據name爲d,此時ID爲1的ctid變爲了(0,4),同時開啓另外一個窗口,可以看到ID爲1的xmax標識爲修改數據時的事務ID,既代表詞條tuple已刪除。

– 第一個窗口

postgre=#  insert into test(id,name) values(3,'c');
INSERT 0 1
postgre=# begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          589
(1 row)

postgre=#  update test set name = 'd' where id ='1';
UPDATE 1
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
 (0,4) |  589 |    0 |    0 |    0 |  1 | d
(3 rows)

– 第二個窗口

postgre=# begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          590
(1 row)

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,1) |  587 |  589 |    0 |    0 |  1 | a
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
(3 rows)

postgre=#

在commit之前,看到的ID1的值還是a,雖然當前590>587,589;但是589沒有提交,只是在xmax上變成了589說明修改或者delete過了,

第一個窗口commit後在第二個窗口查詢顯示,提交之後就可以看到了過去的變化情況了。

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
 (0,4) |  589 |    0 |    0 |    0 |  1 | d
(3 rows)

postgre=#

1.4.3 刪除數據實例

刪除ID爲1的數據,另開啓一個窗口,可以看到ID爲1的xmax爲刪除操作的事務ID,代表此條tuple刪除。

– 第一個窗口操作如下

postgre=# begin transaction;
BEGIN
postgre=#  select txid_current();
 txid_current
--------------
          591
(1 row)

postgre=# delete from test where id = 1;
DELETE 1
postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
(2 rows)

– 第二個窗口操作如下

postgre=#  begin transaction;
BEGIN
postgre=# select txid_current();
 txid_current
--------------
          592
(1 row)

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
 (0,4) |  589 |  591 |    0 |    0 |  1 | d
(3 rows)

postgre=#

– 第一個窗口提交事務,第二個不提交事務,查看第二個窗口的數據信息
– 第一個窗口操作
postgres=# commit;
COMMIT
– 查看第二個窗口信息

postgre=# select ctid,xmin,xmax,cmin,cmax,* from test;
 ctid  | xmin | xmax | cmin | cmax | id | name
-------+------+------+------+------+----+------
 (0,2) |  587 |    0 |    1 |    1 |  2 | b
 (0,3) |  588 |    0 |    0 |    0 |  3 | c
(2 rows)

postgre=#

1.4.4 數據操作總結來說

1、數據文件中同一邏輯行存在多個版本

2、每個版本通過隱藏字段記錄着它的創建事務的ID,刪除事務ID等信息

3、通過一定的邏輯保證每個事務能夠看到一個特定的版本

讀寫事務工作在不同的版本上,以保證讀寫不衝突。

1.5 快照

15.1、快照模型

快照,顧名思義表示某個時間點的狀態.在日常生活中,我們使用相機拍攝,拍下來的畫面可以理解爲拍攝對象的快照;與此類似,連接數據庫執行操作(如查詢)時數據庫的狀態也可以視爲快照.
爲了方便討論,假定數據庫事務隔離級別爲READ COMMITTED,考慮以下場景:
在這裏插入圖片描述

假設在時間點T4執行操作OP,在T4獲取數據庫狀態快照:
其中,TR1/TR4爲已完結事務,TR2/TR3/TR5是正在進行中的事務,TR6是在T4後發生的事務.顯然,TR1和TR4事務對數據庫的修改對操作OP可見;TR6是未來發生的事務,對操作OP不可見;TR2/TR3/TR5正在進行中,同樣也不可見.
就此場景而言,理論上我們只需要知道”拍攝”快照的時間T4,就可以控制數據庫變化的可見性了,簡單而言就是:在此時間前完結的事務,可見,否則不可見.

二、PostgreSQL工程實踐

按上一小節的介紹,可以看到快照天生具有時間屬性,但在PG中,事務號並沒有使用時間戳等信息而是使用自然數列(1,2,…,n,…).
考慮以下場景(簡單起見,假設無論是隻讀還是修改事務,均分配事務號):
在這裏插入圖片描述

XID=315的事務執行操作OP,獲取數據庫快照,與上一個場景類型類似,TR1和TR4事務執行的數據庫修改對操作OP可見;TR6是未來發生的事務,對操作OP不可見;TR2/TR3/TR5正在進行中,同樣也不可見.理論上來說,我們只需要知道該查詢的事務號(如315)就可以控制可見性了.但在工程實踐上,PostgreSQL並沒有簡單的使用事務號作爲快照,而是使用了最後一個已結束的事務號+1(即snapshot中的xmax)作爲歷史和未來的分界:
1、大於等於xmax的,屬於未來事務,不可見;
2、小於xmax的,已結束事務可見,進行中事務不可見。
 出於性能的考慮,爲了進一步區分小於xmax的事務,引入了xmin和xip_list:
 1)小於xmin的事務,視爲已結束事務,可見;
 2)大於等於xmin小於xmax,進行中的事務,不可見,否則可見。
仍以上述場景爲例(詳見下圖):
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-GijhiuVE-1589646242705)(D:\SYBASE&informix&DB2\2 Postgresql\3 markdown筆記\體系架構\晟數體系架構7天之旅\5 併發控制.assets\b00f39ad0162aaea.png)]

最後已結束事務爲200,最早仍活躍事務爲125,則:
xmax = 200 + 1 = 201
xmin = 125
snapshot = 125 : 201 : 140

獲取快照
通過函數txid_current_snapshot()可獲取當前的快照信息:

11:05:16 (xdb@[local]:5432)testdb=# select txid_current_snapshot(); txid_current_snapshot ----------------------- 2404:2404:(1 row)11:24:11 (xdb@[local]:5432)testdb=# 

輸出格式爲xmin : xmax : xip_list
其中:
xmin:最早仍活躍的事務ID(以下簡稱XID),早於此XID的事務要麼被提交併可見,要麼回滾要麼丟棄。
xmax:最後已完結事務(COMMITTED/ABORTED)的事務ID + 1。
xip_list:在”拍攝”快照時仍進行中的事務ID。該列表包含xmin和xmax之間的活動事務ID。
總結一下,簡單來說,對於給定的XID:
XID ∈ [1,xmin),過去的事務,對此快照均可見;
XID ∈ [xmin,xmax),不考慮子事務的情況,仍處於IN_PROGRESS狀態的,不可見;COMMITED狀態,可見;ABORTED狀態,不可見;
XID ∈ [xmax,∞),未來的事務,對此快照均不可見;

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