• <menu id="gyiem"><menu id="gyiem"></menu></menu>
  • <menu id="gyiem"><code id="gyiem"></code></menu>

    SQL優化(六) MVCC PostgreSQL實現事務和多版本并發控制的精華

    原創文章,轉載請務必將下面這段話置于文章開頭處(保留超鏈接)。
    本文轉發自技術世界原文鏈接 http://www.luozeyang.com/sql/mvcc/

    PostgreSQL針對ACID的實現機制

    數據庫ACID

    數據庫事務包含如下四個特性

    • 原子性(Atomicity) 指一個事務要么全部執行,要么不執行。也即一個事務不可能只執行一半就停止(哪怕是因為意外也不行)。比如從取款機取錢,這個事務可以分成兩個步驟:1)劃卡;2)出錢。不可能劃了卡,而錢卻沒出來。這兩步必須同時完成,或者同時不完成。
    • 一致性(Consistency) 事務的運行不可改變數據庫中數據的一致性,事務必須將數據庫中的數據從一個正確的狀態帶到另一個正確的狀態。事務在開始時,完全可以假定數據庫中的數據是處于正確(一致)狀態的,而不必作過多驗證(從而提升效率),同時也必須保證事務結束時數據庫數據處于正確(一致)狀態。例如,完整性約束了a+b=10,一個事務改變了a,那么b也應該隨之改變。
    • 隔離性(Isolation) 在并發數據操作時,不同的事務擁有各自的數據空間,其操作不會對對方產生干擾。隔離性允許事務行為獨立或隔離于其它事務并發運行。
    • 持久性(Durability)事務執行成功以后,該事務對數據庫所作的更改是持久的保存在數據庫之中,不會無緣無故的回滾。

    ACID在PostgreSQL中的實現原理

    事務的實現原理可以解讀為RDBMS采取何種技術確保事務的ACID特性,PostgreSQL針對ACID的實現技術如下表所示。

    ACID 實現技術
    原子性(Atomicity) MVCC
    一致性(Consistency) 約束(主鍵、外鍵等)
    隔離性 MVCC
    持久性 WAL

    從上表可以看到,PostgreSQL主要使用MVCC和WAL兩項技術實現ACID特性。實際上,MVCC和WAL這兩項技術都比較成熟,主流關系型數據庫中都有相應的實現,但每個數據庫中具體的實現方式往往存在較大的差異。本文將介紹PostgreSQL中的MVCC實現原理。

    PostgreSQL中的MVCC原理

    事務ID

    在PostgreSQL中,每個事務都有一個唯一的事務ID,被稱為XID。注意:除了被BEGIN - COMMIT/ROLLBACK包裹的一組語句會被當作一個事務對待外,不顯示指定BEGIN - COMMIT/ROLLBACK的單條語句也是一個事務。

    數據庫中的事務ID遞增。可通過txid_current()函數獲取當前事務的ID。

    隱藏多版本標記字段

    PostgreSQL中,對于每一行數據(稱為一個tuple),包含有4個隱藏字段。這四個字段是隱藏的,但可直接訪問。

    • xmin 在創建(insert)記錄(tuple)時,記錄此值為插入tuple的事務ID
    • xmax 默認值為0.在刪除tuple時,記錄此值
    • cmin和cmax 標識在同一個事務中多個語句命令的序列值,從0開始,用于同一個事務中實現版本可見性判斷

    下面通過實驗具體看看這些標記如何工作。在此之前,先創建測試表

    1
    2
    3
    4
    5
    CREATE TABLE test 
    (
    id INTEGER,
    value TEXT
    );

    開啟一個事務,查詢當前事務ID(值為3277),并插入一條數據,xmin為3277,與當前事務ID相等。符合上文所述——插入tuple時記錄xmin,記錄未被刪除時xmax為0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    postgres=> BEGIN;
    BEGIN
    postgres=> SELECT TXID_CURRENT();
    txid_current
    --------------
    3277
    (1 row)

    postgres=> INSERT INTO test VALUES(1, 'a');
    INSERT 0 1
    postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    1 | a | 3277 | 0 | 0 | 0
    (1 row)

    繼續通過一條語句插入2條記錄,xmin仍然為當前事務ID,即3277,xmax仍然為0,同時cmin和cmax為1,符合上文所述cmin/cmax在事務內隨著所執行的語句遞增。雖然此步驟插入了兩條數據,但因為是在同一條語句中插入,故其cmin/cmax都為1,在上一條語句的基礎上加一。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    INSERT INTO test VALUES(2, 'b'), (3, 'c');
    INSERT 0 2
    postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    1 | a | 3277 | 0 | 0 | 0
    2 | b | 3277 | 0 | 1 | 1
    3 | c | 3277 | 0 | 1 | 1
    (3 rows)

    將id為1的記錄的value字段更新為’d’,其xmin和xmax均未變,而cmin和cmax變為2,在上一條語句的基礎之上增加一。此時提交事務。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    UPDATE test SET value = 'd' WHERE id = 1;
    UPDATE 1
    postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    2 | b | 3277 | 0 | 1 | 1
    3 | c | 3277 | 0 | 1 | 1
    1 | d | 3277 | 0 | 2 | 2
    (3 rows)

    postgres=> COMMIT;
    COMMIT

    開啟一個新事務,通過2條語句分別插入2條id為4和5的tuple。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    BEGIN;
    BEGIN
    postgres=> INSERT INTO test VALUES (4, 'x');
    INSERT 0 1
    postgres=> INSERT INTO test VALUES (5, 'y');
    INSERT 0 1
    postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    2 | b | 3277 | 0 | 1 | 1
    3 | c | 3277 | 0 | 1 | 1
    1 | d | 3277 | 0 | 2 | 2
    4 | x | 3278 | 0 | 0 | 0
    5 | y | 3278 | 0 | 1 | 1
    (5 rows)

    此時,將id為2的tuple的value更新為’e’,其對應的cmin/cmax被設置為2,且其xmin被設置為當前事務ID,即3278

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    UPDATE test SET value = 'e' WHERE id = 2;
    UPDATE 1
    postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    3 | c | 3277 | 0 | 1 | 1
    1 | d | 3277 | 0 | 2 | 2
    4 | x | 3278 | 0 | 0 | 0
    5 | y | 3278 | 0 | 1 | 1
    2 | e | 3278 | 0 | 2 | 2

    在另外一個窗口中開啟一個事務,可以發現id為2的tuple,xin仍然為3277,但其xmax被設置為3278,而cmin和cmax均為2。符合上文所述——若tuple被刪除,則xmax被設置為刪除tuple的事務的ID。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    BEGIN;
    BEGIN
    postgres=> SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    2 | b | 3277 | 3278 | 2 | 2
    3 | c | 3277 | 0 | 1 | 1
    1 | d | 3277 | 0 | 2 | 2
    (3 rows)

    這里有幾點要注意

    • 新舊窗口中id為2的tuple對應的value和xmin、xmax、cmin/cmax均不相同,實際上它們是該tuple的2個不同版本
    • 在舊窗口中,更新之前,數據的順序是2,3,1,4,5,更新后變為3,1,4,5,2。因為在PostgreSQL中更新實際上是將舊tuple標記為刪除,并插入更新后的新數據,所以更新后id為2的tuple從原來最前面變成了最后面
    • 在新窗口中,id為2的tuple仍然如舊窗口中更新之前一樣,排在最前面。這是因為舊窗口中的事務未提交,更新對新窗口不可見,新窗口看到的仍然是舊版本的數據

    提交舊窗口中的事務后,新舊窗口中看到數據完全一致——id為2的tuple排在了最后,xmin變為3278,xmax為0,cmin/cmax為2。前文定義中,xmin是tuple創建時的事務ID,并沒有提及更新的事務ID,但因為PostgreSQL的更新操作并非真正更新數據,而是將舊數據標記為刪除,并插入新數據,所以“更新的事務ID”也就是“創建記錄的事務ID”。

    1
    2
    3
    4
    5
    6
    7
    8
    9
     SELECT *, xmin, xmax, cmin, cmax FROM test;
    id | value | xmin | xmax | cmin | cmax
    ----+-------+------+------+------+------
    3 | c | 3277 | 0 | 1 | 1
    1 | d | 3277 | 0 | 2 | 2
    4 | x | 3278 | 0 | 0 | 0
    5 | y | 3278 | 0 | 1 | 1
    2 | e | 3278 | 0 | 2 | 2
    (5 rows)

    MVCC保證原子性

    原子性(Atomicity)指得是一個事務是一個不可分割的工作單位,事務中包括的所有操作要么都做,要么都不做。

    對于插入操作,PostgreSQL會將當前事務ID存于xmin中。對于刪除操作,其事務ID會存于xmax中。對于更新操作,PostgreSQL會將當前事務ID存于舊數據的xmax中,并存于新數據的xin中。換句話說,事務對增、刪和改所操作的數據上都留有其事務ID,可以很方便的提交該批操作或者完全撤銷操作,從而實現了事務的原子性。

    MVCC保證事物的隔離性

    隔離性(Isolation)指一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的數據對并發的其他事務是隔離的,并發執行的各個事務之間不能互相干擾。

    標準SQL的事務隔離級別分為如下四個級別

    隔離級別 臟讀 不可重復讀 幻讀
    未提交讀(read uncommitted) 可能 可能 可能
    提交讀(read committed) 不可能 可能 可能
    可重復讀(repeatable read) 不可能 不可能 可能
    串行讀(serializable) 不可能 不可能 不可能

    從上表中可以看出,從未提交讀到串行讀,要求越來越嚴格。

    注意,SQL標準規定,具體數據庫實現時,對于標準規定不允許發生的,絕不可發生;對于可能發生的,并不要求一定能發生。換句話說,具體數據庫實現時,對應的隔離級別只可更嚴格,不可更寬松。

    事實中,PostgreSQL實現了三種隔離級別——未提交讀和提交讀實際上都被實現為提交讀。

    下面將討論提交讀和可重復讀的實現方式

    MVCC提交讀

    提交讀只可讀取其它已提交事務的結果。PostgreSQL中通過pg_clog來記錄哪些事務已經被提交,哪些未被提交。具體實現方式將在下一篇文章《SQL優化(七) WAL PostgreSQL實現事務和高并發的重要技術》中講述。

    MVCC可重復讀

    相對于提交讀,重復讀要求在同一事務中,前后兩次帶條件查詢所得到的結果集相同。實際中,PostgreSQL的實現更嚴格,不緊要求可重復讀,還不允許出現幻讀。它是通過只讀取在當前事務開啟之前已經提交的數據實現的。結合上文的四個隱藏系統字段來講,PostgreSQL的可重復讀是通過只讀取xmin小于當前事務ID且已提交的事務的結果來實現的。

    PostgreSQL中的MVCC優勢

    • 使用MVCC,讀操作不會阻塞寫,寫操作也不會阻塞讀,提高了并發訪問下的性能
    • 事務的回滾可立即完成,無論事務進行了多少操作
    • 數據可以進行大量更新,不像MySQL和Innodb引擎和Oracle那樣需要保證回滾段不會被耗盡

    PostgreSQL中的MVCC缺點

    事務ID個數有限制

    事務ID由32位數保存,而事務ID遞增,當事務ID用完時,會出現wraparound問題。

    PostgreSQL通過VACUUM機制來解決該問題。對于事務ID,PostgreSQL有三個事務ID有特殊意義:

    • 0代表invalid事務號
    • 1代表bootstrap事務號
    • 2代表frozon事務。frozon transaction id比任何事務都要老

    可用的有效最小事務ID為3。VACUUM時將所有已提交的事務ID均設置為2,即frozon。之后所有的事務都比frozon事務新,因此VACUUM之前的所有已提交的數據都對之后的事務可見。PostgreSQL通過這種方式實現了事務ID的循環利用。

    大量過期數據占用磁盤并降低查詢性能

    由于上文提到的,PostgreSQL更新數據并非真正更改記錄值,而是通過將舊數據標記為刪除,再插入新的數據來實現。對于更新或刪除頻繁的表,會累積大量過期數據,占用大量磁盤,并且由于需要掃描更多數據,使得查詢性能降低。

    PostgreSQL解決該問題的方式也是VACUUM機制。從釋放磁盤的角度,VACUUM分為兩種

    • VACUUM 該操作并不要求獲得排它鎖,因此它可以和其它的讀寫表操作并行進行。同時它只是簡單的將dead tuple對應的磁盤空間標記為可用狀態,新的數據可以重用這部分磁盤空間。但是這部分磁盤并不會被真正釋放,也即不會被交還給操作系統,因此不能被系統中其它程序所使用,并且可能會產生磁盤碎片。
    • VACUUM FULL 需要獲得排它鎖,它通過“標記-復制”的方式將所有有效數據(非dead tuple)復制到新的磁盤文件中,并將原數據文件全部刪除,并將未使用的磁盤空間還給操作系統,因此系統中其它進程可使用該空間,并且不會因此產生磁盤碎片。

    SQL優化系列

    郭俊 Jason wechat
    歡迎關注作者微信公眾號【大數據架構】
    您的贊賞將支持作者繼續原創分享
    速赢彩app 秦皇岛 | 鸡西 | 连云港 | 泗洪 | 东营 | 齐齐哈尔 | 吉林长春 | 张家界 | 临海 | 阿拉尔 | 改则 | 遂宁 | 巴音郭楞 | 南京 | 韶关 | 邹城 | 台山 | 宜都 | 上饶 | 鄂州 | 南平 | 内蒙古呼和浩特 | 楚雄 | 林芝 | 沭阳 | 定州 | 阿拉尔 | 七台河 | 四川成都 | 湖北武汉 | 馆陶 | 信阳 | 广元 | 乌兰察布 | 明港 | 天水 | 屯昌 | 燕郊 | 通化 | 邯郸 | 阳泉 | 保亭 | 榆林 | 广汉 | 天水 | 吉林 | 黄山 | 柳州 | 漯河 | 西双版纳 | 深圳 | 湘潭 | 姜堰 | 贵港 | 昭通 | 黔东南 | 辽阳 | 吕梁 | 龙口 | 池州 | 温岭 | 云浮 | 雄安新区 | 禹州 | 基隆 | 忻州 | 昆山 | 普洱 | 濮阳 | 渭南 | 乐平 | 十堰 | 涿州 | 海东 | 灌云 | 黑河 | 定西 | 山南 | 萍乡 | 邹城 | 鞍山 | 滨州 | 十堰 | 鄢陵 | 许昌 | 任丘 | 姜堰 | 蓬莱 | 陵水 | 海西 | 黔南 | 寿光 | 惠东 | 吉林长春 | 宜春 | 无锡 | 涿州 | 泗洪 | 邳州 | 徐州 | 燕郊 | 衡水 | 福建福州 | 丹阳 | 偃师 | 襄阳 | 佳木斯 | 周口 | 神农架 | 宁德 | 赣州 | 邯郸 | 肥城 |