中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

MySQL中InnoDB內部機制的示例分析

發布時間:2021-11-02 17:11:52 來源:億速云 閱讀:110 作者:小新 欄目:MySQL數據庫

這篇文章主要介紹了MySQL中InnoDB內部機制的示例分析,具有一定借鑒價值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

Read view

InnoDB支持MVCC多版本,其中RC(Read Committed)和RR(Repeatable Read)隔離級別是利用consistent read view(一致讀視圖)方式支持的。 所謂consistent read view就是在某一時刻給事務系統trx_sys打snapshot(快照),把當時trx_sys狀態(包括活躍讀寫事務數組)記下來,之后的所有讀操作根據其事務ID(即trx_id)與snapshot中的trx_sys的狀態作比較,以此判斷read view對于事務的可見性。

Read view中保存的trx_sys狀態主要包括

  • low_limit_id:high water mark,大于等于view->low_limit_id的事務對于view都是不可見的

  • up_limit_id:low water mark,小于view->up_limit_id的事務對于view一定是可見的

  • low_limit_no:trx_no小于view->low_limit_no的undo log對于view是可以purge的

  • rw_trx_ids:讀寫事務數組

RR隔離級別(除了Gap鎖之外)和RC隔離級別的差別是創建snapshot時機不同。 RR隔離級別是在事務開始時刻,確切地說是第一個讀操作創建read view的;RC隔離級別是在語句開始時刻創建read view的。

創建/關閉read view需要持有trx_sys->mutex,會降低系統性能,5.7版本對此進行優化,在事務提交時session會cache只讀事務的read view。

下次創建read view,判斷如果是只讀事務并且系統的讀寫事務狀態沒有發生變化,即trx_sys的max_trx_id沒有向前推進,而且沒有新的讀寫事務產生,就可以重用上次的read view。

Read view創建之后,讀數據時比較記錄最后更新的trx_id和view的high/low water mark和讀寫事務數組即可判斷可見性。

如前所述,如果記錄最新數據是當前事務trx的更新結果,對應當前read view一定是可見的。

除此之外可以通過high/low water mark快速判斷:

  • trx_id < view->up_limit_id的記錄對于當前read view是一定可見的;

  • trx_id >= view->low_limit_id的記錄對于當前read view是一定不可見的;

如果trx_id落在[up_limit_id, low_limit_id),需要在活躍讀寫事務數組查找trx_id是否存在,如果存在,記錄對于當前read view是不可見的。

由于InnoDB的二級索引只保存page最后更新的trx_id,當利用二級索引進行查詢的時候,如果page的trx_id小于view->up_limit_id,可以直接判斷page的所有記錄對于當前view是可見的,否則需要回clustered索引進行判斷。

如果記錄對于view不可見,需要通過記錄的DB_ROLL_PTR指針遍歷history list構造當前view可見版本數據。

回滾段

InnoDB也是采用回滾段的方式構建old version記錄,這跟Oracle方式類似。

記錄的DB_ROLL_PTR指向最近一次更新所創建的回滾段;每條undo log也會指向更早版本的undo log,從而形成一條更新鏈。通過這個更新鏈,不同事務可以找到其對應版本的undo log,組成old version記錄,這條鏈就是記錄的history list。

分配rollback segment

MySQL 5.6對于沒有顯示指定READ ONLY事務,默認為是讀寫事務。在事務開啟時刻分配trx_id和回滾段,并把當前事務加到trx_sys的讀寫事務數組中。

5.7版本對于所有事務默認為只讀事務,遇到第一個寫操作時,只讀事務切換成讀寫事務分配trx_id和回滾段,并把當前事務加到trx_sys的讀寫事務數組中。

分配回滾段的工作在函數trx_assign_rseg_low進行,分配策略是采用round-robin方式。

從5.6開始支持獨立的undo表空間,InnoDB支持128個undo回滾段,請參照第1篇文章。

  • rseg0:預留在系統表空間ibdata中

  • rseg1~rseg32:這32個回滾段存放于臨時表的系統表空間中

  • rseg33~rseg127:根據配置存放到獨立undo表空間中(如果沒有打開獨立Undo表空間,則存放于ibdata中)

trx_assign_rseg_low判斷,如果支持獨立的undo表空間,在undo表空間有可用回滾段的情況下避免使用系統表空間的回滾段。

rseg->skip_allocation為TRUE表示rseg所在的表空間要被truncate,應該避免使用此rseg分配回滾段。此種情況,必須保證有至少2個活躍的undo表空間,并且至少2個活躍的undo slot。

分配成功時,遞增rseg->trx_ref_count,保證rseg的表空間不會被truncate。

臨時表操作不記redo log,最終調用get_next_noredo_rseg函數進行分配;其他情況調用get_next_redo_rseg。

回滾段實際上是undo文件組織方式,每個回滾段維護了一個段頭頁(segment header),該page劃分了1024個slot(TRX_RSEG_N_SLOTS),每個slot對應到一個undo log對象。

理論上,InnoDB最多支持 96 (128 - 32 /* temp-tablespace */) * 1024個普通事務。

但如果是臨時表的事務,可能還需要多分配1個slot(臨時表的系統表空間)。

  • 只讀階段為臨時表分配的,在臨時表的系統表空間中分配

  • 讀寫階段在undo表空間分配

分配undo log

Insert數據只對當前事務或者提交之后可見,所以insert的undo log在事務commit后就可以釋放了。

Update/delete的undo記錄通常用來維護old version記錄,為查詢提供服務;只有當trx_sys中沒有任何view需要訪問那個old version的數據時才可以被釋放。

InnoDB對insert和update/delete分配不同的undo slot

  • insert的undo slot記在trx->rsegs.m_redo.insert_undo,調用trx_undo_assign_undo分配

  • update的undo slot記在trx->rsegs.m_redo.undate_undo,調用trx_undo_assign_undo分配

trx_undo_assign_undo

I. 檢查cached隊列是否有緩存的undo log(內存中數據結構是trx_undo_t)

  • 如果存在,把這個undo log從cached隊列移除

  • reuse的邏輯:

    a.insert undo:重新初始化undo page的header信息(trx_undo_insert_header_reuse),并在redo log記一條MLOG_UNDO_HDR_REUSE日志

    b.update undo:在undo page的header上分配新的undo header(trx_undo_header_create),并在redo log記一條MLOG_UNDO_HDR_CREATE日志

  • 預留xid空間

  • 重新初始化undo(trx_undo_mem_init_for_reuse)把undo->state設置為TRX_UNDO_ACTIVE,并把undo->state寫入到第一個undo page的TRX_UNDO_SEG_HDR+TRX_UNDO_STATE位置上

注1:TRX_UNDO_SEG_HDR表示segment header起始offset 注2:undo segment與事務trx是一一對應關系,undo segment header的狀態(TRX_UNDO_STATE)跟事務當前狀態也是一一對應的

如下圖(引自第1篇文章)

MySQL中InnoDB內部機制的示例分析

undo segment是個獨立的段,每個undo segment包含1個header page(第1個undo page)和若干個記錄undo日志的undo page。

第1個undo page中存儲的是元信息: 首先存儲的是undo page的元信息,位于TRX_UNDO_PAGE_HDR到TRX_UNDO_SEG_HDR之間。

TRX_UNDO_PAGE_START:指向page中第一個undo log TRX_UNDO_PAGE_FREE:指向page中下一個undo log要寫到的位置 TRX_UNDO_PAGE_NODE:undo segment所有page組成一個雙向鏈表,每個page的TRX_UNDO_PAGE_NODE字段作為連接件,第一個undo page中的TRX_UNDO_PAGE_LIST作為表頭

 /* undo page header */ #define TRX_UNDO_PAGE_HDR FSEG_PAGE_DATA #define TRX_UNDO_PAGE_TYPE 0 /*!< TRX_UNDO_INSERT or
                    TRX_UNDO_UPDATE */ #define TRX_UNDO_PAGE_START 2 /*!< Byte offset where the undo log
                    records for the LATEST transaction
                    start on this page (remember that
                    in an update undo log, the first page
                    can contain several undo logs) */ #define TRX_UNDO_PAGE_FREE 4 /*!< On each page of the undo log this
                    field contains the byte offset of the
                    first free byte on the page */ #define TRX_UNDO_PAGE_NODE 6 /*!< The file list node in the chain
                    of undo log pages */ /*-------------------------------------------------------------*/ #define TRX_UNDO_PAGE_HDR_SIZE (6 + FLST_NODE_SIZE) /*!< Size of the transaction undo
                    log page header, in bytes */

之后是undo segment的元信息,位于TRX_UNDO_SEG_HDR到TRX_UNDO_SEG_HDR+TRX_UNDO_SEG_HDR_SIZE

TRX_UNDO_STATE:表示undo segment的狀態,一個undo segment可以包含多個undo log,但至多只有1個active undo log,也就是最近的undo log TRX_UNDO_LAST_LOG:指向最近的undo log的header信息 TRX_UNDO_FSEG_HEADER:存儲的是undo segment對應的file segment信息,在fseg_create_general中設置(4字節space id,4字節的page no,2字節的page offset)

undo segment從buffer pool移除被persist到磁盤時,就寫到file segment指定的位置上

 #define TRX_UNDO_SEG_HDR    (TRX_UNDO_PAGE_HDR + TRX_UNDO_PAGE_HDR_SIZE) #define TRX_UNDO_STATE      0   /*!< TRX_UNDO_ACTIVE, ... */ #define TRX_UNDO_LAST_LOG   2   /*!< Offset of the last undo log header on the segment header page, 0 if none */ #define TRX_UNDO_FSEG_HEADER    4   /*!< Header for the file segment which the undo log segment occupies */ #define TRX_UNDO_PAGE_LIST  (4 + FSEG_HEADER_SIZE) /*!< Base node for the list of pages in
                    the undo log segment; defined only on
                    the undo log segment's first page */ /*-------------------------------------------------------------*/ /** Size of the undo log segment header */ #define TRX_UNDO_SEG_HDR_SIZE   (4 + FSEG_HEADER_SIZE + FLST_BASE_NODE_SIZE)

再之后是undo log header信息,所有的undo log header都存儲在第一個undo page上。

II. 從cached隊列分配undo失敗時,需要真正分配一個undo segment(trx_undo_seg_create)

首先要從rseg分配一個slot(trx_rsegf_undo_find_free),每個rseg至多支持1024個slot。找到空slot返回index。

如果當前rseg已滿,trx_undo_seg_create返回DB_TOO_MANY_CONCURRENT_TRXS向上層報錯,表示并發事務太多無法創建undo segment。

然后在rseg對應的table space創建一個新的file segment,file segment信息記在segment header的TRX_UNDO_FSEG_HEADER(fseg_create_general)。

trx_undo_seg_create在創建file segment之后,把新創建segment的page no寫到rseg對應slot上建立映射關系,并返回新創建segment的page。

file segment與undo segment的映射關系,還有rseg[slot]與file segment對應page的映射關系都是在trx_undo_seg_create綁定的。cached undo不會更新這兩個映射關系。

III. trx_undo_seg_create返回的page上創建新的undo header;上層負責初始化trx_undo_t數據結構

trx_undo_create為新創建的undo header創建內存數據結構trx_undo_t(trx_undo_mem_create),把undo->state設置為TRX_UNDO_ACTIVE。

IV. 分配好的trx_undo_t會加入到事務的insert_undo_list或者update_undo_list隊列上

寫入undo log

trx_undo_assign_undo分配undo之后,就可往其中寫入undo記錄。寫入的page來自undo->last_page_no,初始情況下等于hdr_page_no。

update undo包含一個重要的部分:記錄的當前回滾段指針要寫到undo log里面,以便維護記錄的歷史數據鏈。

read view需要讀老版本數據時,會通過記錄中當前的回滾段指針開始向前找到可見版本的數據。

完成Undo log寫入后,構建新的回滾段指針并返回(trx_undo_build_roll_ptr),這個指針也就是clustered索引記錄的DB_ROLL_PTR。

回滾段指針包括rseg->id、日志所在的page no、以及page內偏移量,需要記錄到clustered索引記錄中。這里rseg->id用來確定rseg->space,真正用于定位undo log位置的其實是<rseg->space, undo->page,undo->page_offset>三元組。

事務prepare

設置undo->state為TRX_UNDO_PREPARED,并把這個狀態寫到第一個undo page的(TRX_UNDO_SEG_HDR+TRX_UNDO_STATE)位置上。

除此之外,prepare階段還要更新xid信息。

事務commit

在事務commit階段,需要把undo->state設置為完成狀態,并把undo加到undo segment的history list。正在提交的undo header被指向history list的第一項,表示當前事務history list最近的undo。

undo->state完成狀態包括3種,在trx_undo_set_state_at_finish設置

  • undo只占一個page,而且第一個undo page已使用的空間小于3/4 (TRX_UNDO_PAGE_REUSE_LIMIT):狀態設置為TRX_UNDO_CACHED

  • 不滿足1的情況下,如果是insert_undo(TRX_UNDO_INSERT):狀態設置為TRX_UNDO_TO_FREE

  • 不滿足1和2的情況下,狀態設置為TRX_UNDO_TO_PURGE,表示undo可能需要purge線程清理

cached undo會被到cached隊列上,這個隊列就是trx_undo_assign_undo提到的cached隊列

設置完undo->state之后,需要把這個狀態寫入到第一個undo page的(TRX_UNDO_SEG_HDR+TRX_UNDO_STATE)位置上

把undo加到undo segment header的history list

Insert的old version沒有實際意義,所以insert undo在事務commit時就可以釋放了。

trx_undo_set_state_at_finish里面有cached策略,如果只占1個undo page,并且undo page已使用的空間不足pagesize的3/4可以被reuse,其實大部分insert undo都屬于這種情況。

Update undo需要維護history list。這里先提一下trx->no,它維護了事務trx commit順序,跟事務的trx_id一樣,也是使用max_trx_id遞增產生。

另外,purge_sys(purge的全局數據結構)維護個最小堆,每個rollback segment第1次事務提交時向最小堆插入數據,旨在找到trx_no最小的rollback segment進行purge。后面每次處理完1個rseg后,會把下一個undo記錄的trx_no壓入到這個最小堆,作為rseg的cursor。

事務commit時按照trx->no順序,把事務當前的undo log掛到undo segment history list的表頭,指向事務最近的undo log。

History list里的undo都是已提交事務的,當前事務所修改的undo log都記錄在這里,按照從新->老方式排列,最老的undo log在尾部。

undo加入到history list的方式是:以undo log的TRX_UNDO_HISTORY_NODE作為連接件,加入到第一個undo page的TRX_RSEG_HISTORY。

一般來說,每次調用trx_purge_add_update_undo_to_history都會把undo加入到history list,只有在undo page無法被reuse時才更新history list大小(可以認為是個優化,最后一次更新history length)。

在此之后,trx_purge_add_update_undo_to_history會把undo log header的TRX_UNDO_TRX_NO更新為trx_no。

如果undo->del_marks是FALSE,這個函數也會更新TRX_UNDO_DEL_MARKS(undo segment創建或者reuse被初始化為TRUE),澄清這不是delete marker。

如果undo segment自創建以來(也可能是上次purge完成之后)中第1個事務commit,還需要更新purge有關的一些參數,指向下次purge從哪里開始執行。

老版本數據purge

舊版本數據不再被任何view訪問就可以被刪除了。5.6以上版本支持獨立purge線程,用戶可以通過參數Innodb_purge_threads設置purge線程個數。

有兩類purge線程:

  • coordinator thread:srv_purge_coordinator_thread,全局只有1個

  • worker thread:srv_worker_thread,系統有innodb_purge_threads - 1個

coordinator thread負責啟動worker thread參與到purge工作中。

增加purge線程的策略是:trx_sys->rseg_history_len比上次循環變大了或者rseg_history_len超過某一閾值,需要引進更多的worker thread。

減少purge線程的策略是:如果之前使用多個purge 線程,trx_sys->rseg_history_len并沒有變大,可能需要減少worker thread。

在進行purge之前,首先要確定purge線程要做哪些工作,也就是說哪些undo log可以被purged。

purge也是通過read view來確定工作范圍,被稱為purge view。如果系統有活躍read view,就選取最老的read view作為purge view。

如果不存在就給trx_sys的狀態打個snapshot,作為purge view,可以被purge的undo log其trx_no一定是小于系統中所有已提交事務的trx->no。

這里插一句,在事務commit時,會把產生的trx->no加入到trx_sys->serialisation_list鏈表,這個鏈表是按照trx->no升序次序排列,也就是維護了trx commit順序。

InnoDB初始化的時候會初始化purge_sys數據結構,其中一個工作就是創建purge graph。

這是總共3層結構的圖:

  • 第1層是fork節點

  • 第2次是thrd節點(表示purge thread)

  • 第3層是node節點(表示purge task)

所有的thrd節點被鏈入到fork->thrs鏈表中;fork地址存儲在purge_sys->query,可以通過purge_sys直接訪問。

執行purge的時候總是遍歷purge_sys->query->thrs鏈表,給每個purge線程分配purge任務(trx_purge_attach_undo_recs)。

解析undo log的調用路徑如下:

 srv_purge_coordinator_thread -> srv_do_purge -> trx_purge ->
        trx_purge_attach_undo_recs -> trx_purge_fetch_next_rec -> 
               trx_purge_get_next_rec

purge_sys->next_stored為FALSE時,表示rseg_iter當前指向的rseg無效,需要把rseg_iter移到下一個有效的rseg(TrxUndoRsegsIterator::set_next)。

purge_sys->purge_queue維護了一個最小堆,每次pop最頂元素,可以得到trx_no最小的rollback segment(TrxUndoRsegsIterator::set_next)。

5.7支持臨時表的noredo的rollback segment,set_next遇到redo rollback segment和noredo rollback segment同時存在的情況會一股腦把這兩個rollback segment都pop出來加入到 purge_sys->rseg_iter->m_trx_undo_rsegs數組中,也在TrxUndoRsegsIterator::set_next實現。

如果沒有rollback segment需要purge話,purge_sys->rseg設置為NULL,purge線程會去睡眠(trx_purge_choose_next_log)。

一般情況下都是有rollback segment需要處理的,purge_sys->rseg更新成purge_sys->rseg_iter->m_trx_undo_rsegs的第1項(至多2項)。

purge_sys中的相應成員也要更新,指向當前rseg上次purge到的位置(TrxUndoRsegsIterator::set_next)。

update undo的del_marks域正常情況下都是TRUE,因為update/delete操作都需要對old value進行標記刪除。

如果purge_sys->rseg->last_del_marks是FALSE的話,表示這是一個dummy的undo log,不需要做物理刪除。這種情況下,把purge_sys->offset設置成0,做個標記表示這個undo log不需要被purged(trx_purge_read_undo_rec)。

正常情況下purge_sys->rseg->last_del_marks是TRUE,可以通過<purge_sys->rseg->space, purge_sys->hdr_page_no, purge_sys->hdr_offset>讀取undo log記錄(trx_purge_read_undo_rec)。

并把purge_sys以下四個域設置成undo log記錄相應的信息(trx_purge_read_undo_rec)。

 purge_sys->offset = offset; /* undo log記錄的offset */ purge_sys->page_no = page_no; /* undo log記錄的pageno */ purge_sys->iter.undo_no = undo_no; /* undo log記錄的undo_no,trx內部undo的序列號 */ purge_sys->iter.undo_rseg_space = undo_rseg_space; /* undo log的tablespace */

為了保證purge_sys以上4個域一定是指向下一個有效undo log,每次讀取undo log時都會捎帶著讀取下一個undo log,并把上面這四個域更新為下一個undo log的信息,方面后續訪問(trx_purge_get_next_rec)。

如果是dummy undo,trx_purge_get_next_rec會去讀prev_undo(trx_purge_rseg_get_next_history_log),用prev_log信息更新rseg中下一個purge信息。

在此之后,還會把rseg->last_trx_no壓入最小堆,待后面繼續處理這個rseg。 然后調用trx_purge_choose_next_log選擇下一個處理的rseg,并讀取第一個undo log(trx_purge_get_next_rec)。

就這樣挨個讀取undo log,trx_purge_attach_undo_recs中有一個大循環,每次調用trx_purge_fetch_next_rec讀到一個undo log后,把它存放到purge節點(purge graph的第三級節點) node->undo_recs數組里面,循環下一次執行切換到下一個thr(purge 線程)。

循環的結束條件是:

  • 沒有新的undo log

  • 處理過的undo log達到batch size(一般是300)

達到循環結束條件后,trx_purge_attach_undo_recs返回。如果n_purge_threads > 1 (需要worker線程參與purge),coordinator線程會以round-robin方式啟動n_purge_threads - 1個worker線程。

不管有沒有worker線程參與purge,coordinator線程都會調用que_run_threads(在trx_purge上下文)去處理purge任務。

purge任務如何處理呢?通俗的說purge就是刪除被標記delete marker的記錄項。

大致過程如下:

 srv_purge_coordinator_thread -> srv_do_purge -> trx_purge ->
        que_run_threads -> que_run_threads_low -> que_thr_step
               row_purge_step -> row_purge -> row_purge_record ->
                       row_purge_del_mark -> row_purge_remove_sec_if_poss

一般刪除的原則是先刪除二級索引再刪除clustered索引(row_purge_del_mark)。

另一種情況是聚集索引in-place更新了,但二級索引上的記錄順序可能發生變化,而二級索引的更新總是標記刪除 + 插入,因此需要根據回滾段記錄去檢查二級索引記錄序是否發生變化,并執行清理操作(row_purge_upd_exist_or_extern)。

前面提到過在parse undo log時,可能遇到dummy undo log。返回到row_purge執行時需要判讀是否是dummy undo,如果是就什么也不做。

truncate undo space

trx_purge在處理完一個batch(通常是300)之后,調用trx_purge_truncate_historypurge_sys對每一個rseg嘗試釋放undo log(trx_purge_truncate_rseg_history)。

大致過程是:把每個purge過的undo log從history list移除,如果undo segment中所有的undo log都被釋放,可以嘗試釋放undo segment,這里隱式釋放file segment到達釋放存儲空間的目的。

感謝你能夠認真閱讀完這篇文章,希望小編分享的“MySQL中InnoDB內部機制的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持億速云,關注億速云行業資訊頻道,更多相關知識等著你來學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

开封县| 临清市| 大荔县| 盖州市| 安岳县| 崇州市| 石景山区| 舟曲县| 信阳市| 荆州市| 乐陵市| 七台河市| 尼玛县| 桐乡市| 澄迈县| 遂宁市| 绥江县| 库尔勒市| 望都县| 常山县| 利津县| 黔江区| 荃湾区| 明星| 临西县| 句容市| 庆安县| 平湖市| 葫芦岛市| 高邑县| 澳门| 南川市| 孝昌县| 肃南| 肇东市| 中宁县| 桂东县| 石楼县| 平昌县| 高雄市| 博爱县|