MVCC Postgresql 和 MYSQL 到底誰更......?

經常看到有人寫關於鎖的事情,但常常感覺給人一個感覺,數據庫的ACID 是通過鎖來控制的,實際上數據庫的ACID 控制是複雜的,MVCC 就是一個對資源併發訪問時的提高併發訪問的有效的方法

在首次定義ACID事務屬性時,假定具有可串行性。爲了提供嚴格的可序列化事務結果,使用了2PL(兩階段鎖定)機制。在使用2PL時,每次讀操作都需要一個共享鎖獲取,而寫操作則需要一個獨佔鎖。

保持數據的完整性,作爲一個重要的經驗法則,對事務性處理DBs的所有修改都應該在原子事務下進行。而且,每個事務都應該使數據庫處於一致的狀態,隔離是最難處理的實踐????。從理論上講,非常簡單,他隔離保證了所有事務的執行,即使它們同時運行,也“好像”它們是串行執行的。可實踐中,它要複雜得多在保持合理性能的同時保持隔離.

多版本併發控制(MVCC),會創建行的“先前版本”(“快照”),並將該行的“先前版本”提供給任何可能嘗試併發運行的其他事務,而不是在有人開始讀取該行時鎖定該行。這是有道理的——畢竟,在提交第一個事務之前,不會考慮更改DB的狀態。

寫到這,會比較枯燥,下面就開始講點和實際數據庫貼邊的 MVCC 實現。

就目前掌握的數據庫類型,大致解決MVCC的方式有兩種

1 新的數據與舊數據分離轉移到一個地方,例如undo log,其他人讀數據時,從回滾段中把舊的數據讀出來,Oracle和MySQL中的innodb引擎是這樣做的。

2寫新數據時,舊數據不刪除,而是把新數據插入,新舊數據在一起。PostgreSQL就是使用的這種實現方法。

那麼我們可以對比一下這兩種方式的不同

1  Postgresql 中通過行設計和xact 的方式來解決MVCC的問題, 我們可以通過一個表的查詢 xmin,xmax,cmin,cmax 來查看相關的原理。

下面的這段代碼解釋了PG上關於 tuple 設計上的一些原理

typedef struct HeapTupleFields { TransactionId t_xmin;   /* inserting xact ID *

/TransactionId t_xmax;   /* deleting or locking xact ID *

/union{   CommandId t_cid;   /* inserting or deleting command ID, or both *

/   TransactionId t_xvac; /* VACUUM FULL xact ID */}    t_field3; } HeapTupleFields;

  • t_xmin 表現的是產生這個行或更高這行的事務ID

  • t_xmax 表現的是刪除或鎖定這個元組的事務ID

  • t_cid 包含cmin和cmax兩個字段,標識在一個事務裏面的這些行的操作順序,例如插入5行,那這5行的插入順序是什麼,那些tuple 對那些tuple是可見的,這個是一個事務級的可見性的展示。

  • t_xvac 存儲的是VACUUM FULL 命令的事務ID

當插入一行時,postgres將在該行中存儲XID並將其稱爲xmin。已經提交的並且xmin小於當前事務的XID的每一行對事務都是可見的。這意味着您可以啓動一個事務並插入一行,而在該事務提交之前,其他事務不會看到該行。一旦提交併創建了其他事務,它們就能夠查看新行,因爲它們滿足xmin < XID條件——並且創建該行的事務已經完成。

下面我們看看postgresql 表結構,以city表爲例

一個表中都有的字段 tableoid,cmax,xmax,cmin,xmin

select attname, attnum, atttypid::regtype, attisdropped::text from pg_attribute where attrelid = 'city'::regclass;

我們舉一個例子就能很好的解釋MVCC 的具體操作

我們選擇一個city 表,然後我們開兩個事物,一個更新city_id 1 - 20 另一個事物更新city_id 21 40 

事務1

事務2

不在事務1 和事務2 中看到的

從上面可以總結出

1 每個事務更改操作都會觸發 xmin xmax ,改變 

2 每個事務的更改xmin 只會在自己的事務內部看的到,而xmax 就是別的事務正在更改的信息標記

這樣MVCC 的初步功能就可以進行下去了,所以postgresql 沒有頁鎖,只有表鎖和行鎖。

這樣做的優點就是事務的回滾非常迅速,但需要經常性的 vacuum

反觀MYSQL 的MVCC 採用的是undo log的方式,這和ORACLE 的方式雷同,MVCC 的功能實現並不是在每行中實現的,innodb存儲引擎對undo的管理採用段的方式,rollback segment稱爲回滾段,每個回滾段中有1024個undo log segment。(MYSQL 8 已經有改變)


使所有回滾段(rsegs)駐留在所選的UNDO表空間中不活動。Inactive意味着這些回滾段不會分配給新的事務。清除系統將繼續釋放不再需要的回滾段。這將分配給回滾段的頁面標記爲空閒,並減少回滾的邏輯大小。

通過上面的一個UNDO 表空間的大概的流程,可以提出幾個問題

1 回滾段是有數量限制的,回滾段的數量限制就是這個數據庫系統的同一個時間可以執行事務的數量的限制,每個回滾段維護一個頁頭,每個頁面會劃分1024slot 每個slot 會對應一個事務,所以MYSQL 5.7(8.0重新設計了UNDOLOG)另外即使是隻讀的事務,只要有對臨時表的寫入,也是分配回滾段的。

例如MYSQL的事務在prepare 階段,insert undo 和 update undo的狀態爲prepare,調用trx_undo_set_state_at_prepare,對對應的undo log slot頭頁面(trx_undo_t::hdr_page_no),將頁面段頭的TRX_UNDO_STATE設置爲TRX_UNDO_PREPARED,同時修改其他對應字段。

在commit 階段,Undo狀態爲TRX_UNDO_CACHED,則加入到回滾段的insert_undo_cached鏈表上,或者將該undo所佔的segment及其所佔用的回滾段的slot全部釋放掉,修改當前回滾段的大小,並釋放undo對象所佔的內存,如果是Update_undo操作,則insert_undo不放到History list上。最後事務提交後將回滾段的計數器減一。

其實就是將事務ID 和 回滾段的指針連接起來,同時MYSQL的行中也有兩個字段來記錄,針對MYSQL 表每一行 都有 6個字節的 db_trx_id , 7個字節的 db_roll_ptr ,undo log對於update或者delete操作,每一行都保存了一個事務Id,修改事務Id爲當前Session的事務id,生成數據行事務之前的版本,將當前行的回滾指針指向事務之前的版本。對於insert操作,將當前行的回滾指針指爲空,因爲insert沒有事務操作之前的版本。

數據庫如果在執行事務的過程中想要回滾,必然要考慮併發和回滾,這就造成隨着併發和回滾的需求,導致佔用更多的磁盤空間,而在事務提交後就需要清理掉這些無用的東西,POSTGRESQL 叫 VACUUM ,MYSQL 叫 Purge ,在InnoDB中,更新後的行的最新版本只保留在表中。舊版本的行在回滾段,而刪除後的行版本則保留在原處,並標記爲以後的清理。因此,須從表本身清理標記任何已刪除的行,並從回滾段中清除任何更新後的舊版本的行。查找被刪除的記錄所需的所有信息。

所以從設計結構上來說postgresql 的結構設計要簡單,MYSQL ORACLE 的結構設計要複雜,並且POSTGRESQL 也沒有redo等結構,所以針對POSTGRESQL 最大的問題就是VACUUM , 而MYSQL  INNODB ,則會面對redo ,undo ,purge 等方面的I/O 壓力。

純個人認爲,postgresql 在不考慮vacuum 的情況下,性能上的瓶頸要小於MYSQL 方面的複雜結構上產生的影響(可以在非頻繁工作期間進行一些其他的回收方式)。postgresql 在使用中要給出的磁盤空間要有餘量,mysql  在這方面上要好一些。

所以單純說那個 better ,沒有什麼意義,有意義的是你掌握了多少他們的特性  knowledge

一起加羣,互相提高

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