PostgreSQL嵌套事務提交流程研究
父事務最終提交代碼流程圖
事務提交調用流程
其中值得拿出來講的,主要是TransactionIdSetTreeStatus這個方法。
這裏涉及到一個概念,子事務。在PG這個地方,子事務的概念主要指:事務從開始到結束,期間可以
savepoint,之後rollback到savepoint而不是事務起點,在實際情況中多有應用,因此這裏父事務與子事務(比如事務最終提交,但期間有回滾的情況,或者事務期間多次savepoint)必須儘可能原子性的方式寫入,否則事務可見性就會出現問題。
在代碼註釋裏面,對這裏的寫入做了一個比較直觀的例子:
比如一個事務t,有子事務
t1,t2,t3,t4,其中t,t1被映射到clog頁p1,t2和t3在p2,t4在頁p3。那麼寫入的時候,順序如下:
-
設置p2 的t2 t3爲子提交,之後設置p3的t4位子提交
-
設置t1爲子提交,之後設置t爲已提交,之後設置t1爲已提交
-
設置 t2 t3 爲已提交,設置t4位已提交
對於回滾,實際上也是調用TransactionIdSetTreeStatus方法,只是上層函數是TransactionIdAbortTree,設置的標記是TRANSACTIONSTATUSABORTED,也就是記錄事務爲中斷。語義上來說,對於事務中斷,由於事務的原子性要求,中斷的事務數據就是不可見的了,沒啥問題。
TransactionIdSetTreeStatus()講解
代碼註釋
在提交日誌中記錄事務條目的最終狀態
事務及其子事務樹。注意確保這是
高效且儘可能原子。
xid是用於設置狀態的單個xid。通常是
頂級提交或中止的頂級transactionid。它可以
當我們記錄事務中止時,它也是子事務。
subxids是長度爲nsubxids的xid數組,表示子事務
在xid樹中。在各種情況下,nsubxids可以爲零。
記錄異步時,lsn必須是提交記錄的WAL位置
提交。對於同步提交,它可以是InvalidXLogRecPtr,因爲
調用者保證在這種情況下已經清除了提交記錄。它
對於中止情況,也應爲InvalidXLogRecPtr。
在提交的情況下,原子性受是否所有子軸都在
與xid相同的CLOG頁面。如果全部都是,那麼鎖將被抓住
僅一次,狀態將設置爲直接提交。除此以外
我們必須
1.設置子提交的所有與子目錄不在同一頁面上的子目錄
主要的xid
2.在同一頁面上自動設置提交的主xid和子xid
3.再次遍歷第一束並將其設置爲已提交
請注意,就併發檢查程序而言,主要交易
整個提交仍然是原子的。
示例:
TransactionId t提交併具有子下標t1,t2,t3,t4
t在頁面p1上,t1也在p1上,t2和t3在p2上,t4在p3上
1.更新第2-3頁:
第2頁:將t2,t3設置爲子提交
第3頁:將t4設爲子提交
2.更新第1頁:
將t1設爲子提交,
然後將t設置爲commit,
然後將t1設置爲commit
3.更新第2-3頁:
第2頁:將t2,t3設置爲已提交
第3頁:將t4設置爲已提交
注意:這是一個低級例程,不是首選入口點
用於大多數用途; transam.c中的函數是預期的調用方。
XXX考慮在需要的頁面上發佈FADVISE_WILLNEED,
但尚未緩存,並提示頁面不會掉出
尚未緩存。
分析
這裏涉及到一個概念:子事務。在PG這個地方,子事務的概念主要指:事務從開始到結束,期間可以savepoint,之後rollback到savepoint而不是事務起點,在實際情況中多有應用,因此這裏父事務與子事務(比如事務最終提交,但期間有回滾的情況,或者事務期間多次savepoint)必須儘可能原子性的方式寫入,否則事務可見性就會出現問題。
TransactionIdSetTreeStatus的代碼註釋中,對這裏的寫入做了一個比較直觀的例子:
比如一個事務t,有子事務
t1,t2,t3,t4,其中t,t1被映射到clog頁p1,t2和t3在p2,t4在頁p3。那麼寫入的時候,順序如下:
-
設置p2 的t2 t3爲子提交,之後設置p3的t4位子提交
-
設置t1爲子提交,之後設置t爲已提交,之後設置t1爲已提交
-
設置 t2 t3 爲已提交,設置t4位已提交
對於回滾,實際上也是調用TransactionIdSetTreeStatus方法,只是上層函數是
TransactionIdAbortTree,設置的標記是TRANSACTIONSTATUSABORTED,也就是記錄事務爲中斷。語義上來說,對於事務中斷,由於事務的原子性要求,中斷的事務數據就是不可見的了,沒啥問題。
子事務subtrans?
當我們使用savepoint時,會產生子事務,子事務和父事務一樣,可能消耗XID。一旦爲子事務分配了XID,那麼就涉及CLOG的原子操作了。因爲要保證父事務和所有的子事務的CLOG一致性。
當不消耗XID時,需要通過SubTransactionId來區分子事務。
參考: src/backend/access/transam/README
《Transaction and Subtransaction Numbering》
事務和子事務都可以有XID,子事務和事務一樣,在真正需要XID的時候纔會分配XID,
也就是說,一個事務,如果它有子事務,可能消耗多個XID。
另外需要注意,如果子事務要分配XID,必須先給它的父事務分配一個XID,才能給子事務分配XID,因爲要確保子事務的XID是在父事務後分配的。
README原文
Transaction and Subtransaction Numbering
----------------------------------------
Transactions and subtransactions are assigned permanent XIDs only when/if
they first do something that requires one — typically, insert/update/delete
a tuple, though there are a few other places that need an XID assigned.
If a subtransaction requires an XID, we always first assign one to its
parent. This maintains the invariant that child transactions have XIDs later
than their parents, which is assumed in a number of places.
The subsidiary actions of obtaining a lock on the XID and entering it into
ux_subtrans and UX_PROC are done at the time it is assigned.
A transaction that has no XID still needs to be identified for various
purposes, notably holding locks. For this purpose we assign a "virtual
transaction ID" or VXID to each top-level transaction. VXIDs are formed from
two fields, the backendID and a backend-local counter; this arrangement allows
assignment of a new VXID at transaction start without any contention for
shared memory. To ensure that a VXID isn’t re-used too soon after backend
exit, we store the last local counter value into shared memory at backend
exit, and initialize it from the previous value for the same backendID slot
at backend start. All these counters go back to zero at shared memory
re-initialization, but that’s OK because VXIDs never appear anywhere on-disk.
Internally, a backend needs a way to identify subtransactions whether or not
they have XIDs; but this need only lasts as long as the parent top transaction
endures. Therefore, we have SubTransactionId, which is somewhat like
CommandId in that it’s generated from a counter that we reset at the start of
each top transaction. The top-level transaction itself has SubTransactionId 1,
and subtransactions have IDs 2 and up. (Zero is reserved for
InvalidSubTransactionId.) Note that subtransactions do not have their
own VXIDs; they use the parent top transaction’s VXID.
子事務日誌
嵌套事務形成了一個事務樹,因此只需要通過指定事務,在子事務日誌中逐級向上回溯尋找其父事務,直到遇到一個事務的父事務id爲無效事務id,則說明該事務爲所要尋找的根事務id。
subtrans日誌的健壯性要求和clog日誌是完全不同的,因爲需要記錄的只是當前打開事務的子事務信息。所以在系統奔潰或重啓時並不保存數據。由於在系統奔潰時不需要保存數據,因此也不需要和xlog進行交互,也沒有對應的redo函數。在數據庫啓動時,只要使當前活躍的子事務頁面爲全0就可以了。
分析
父子關係的嵌套事務日誌,即: ux_subtrans 日誌
在Clog同步的時候,通過將父子事務以原子化的方式,一併修改是否已提交的狀態
有關事務的父子嵌套關係,其他節點只需要瞭解其是否最終提交,忽略其父子關係
子事務的提交狀態分爲”sub-commited”和”最終commited”,即其他節點只需瞭解所要xid(包括sub-xid)的提交狀態爲“最終committed”,而忽略“sub-commited”。
參考
PostgreSQL的clog—從事務回滾速度談起
https://ssl.zzidc.com/chanpinzixun/2019/0801/654.html
pg_clog的原子操作與pg_subtrans(子事務)