MySQL · 引擎特性 · InnoDB 事務系統

前言

關係型數據庫的事務機制因其有原子性,一致性等優秀特性深受開發者喜好,相似的思想已經被應用到不少其餘系統上,例如文件系統等。本文主要介紹InnoDB事務子系統,主要包括,事務的啓動,事務的提交,事務的回滾,多版本控制,垃圾清理,回滾段以及相應的參數和監控方法。代碼主要基於RDS 5.6,部分特性已經開源到AliSQL。事務系統是InnoDB最核心的中控系統,涉及的代碼比較多,主要集中在trx目錄,read目錄以及row目錄中的一部分,包括頭文件和IC文件,一共有兩萬兩千多行代碼。mysql

基礎知識

事務ACID: 原子性,指的是整個事務要麼所有成功,要麼所有失敗,對InnoDB來講,只要client收到server發送過來的commit成功報文,那麼這個事務必定是成功的。若是收到的是rollback的成功報文,那麼整個事務的全部操做必定都要被回滾掉,就好像什麼都沒執行過同樣。另外,若是鏈接中途斷開或者server crash事務也要保證會滾掉。InnoDB經過undolog保證rollback的時候能找到以前的數據。一致性,指的是在任什麼時候刻,包括數據庫正常提供服務的時候,數據庫從異常中恢復過來的時候,數據都是一致的,保證不會讀到中間狀態的數據。在InnoDB中,主要經過crash recovery和double write buffer的機制保證數據的一致性。隔離性,指的是多個事務能夠同時對數據進行修改,可是相互不影響。InnoDB中,依據不一樣的業務場景,有四種隔離級別能夠選擇。默認是RR隔離級別,由於相比於RC,InnoDB的RR性能更加好。持久性,值的是事務commit的數據在任何狀況下都不能丟。在內部實現中,InnoDB經過redolog保證已經commit的數據必定不會丟失。sql

多版本控制: 指的是一種提升併發的技術。最先的數據庫系統,只有讀讀之間能夠併發,讀寫,寫讀,寫寫都要阻塞。引入多版本以後,只有寫寫之間相互阻塞,其餘三種操做均可以並行,這樣大幅度提升了InnoDB的併發度。在內部實現中,與Postgres在數據行上實現多版本不一樣,InnoDB是在undolog中實現的,經過undolog能夠找回數據的歷史版本。找回的數據歷史版本能夠提供給用戶讀(按照隔離級別的定義,有些讀請求只能看到比較老的數據版本),也能夠在回滾的時候覆蓋數據頁上的數據。在InnoDB內部中,會記錄一個全局的活躍讀寫事務數組,其主要用來判斷事務的可見性。數據庫

垃圾清理: 對於用戶刪除的數據,InnoDB並非馬上刪除,而是標記一下,後臺線程批量的真正刪除。相似的還有InnoDB的二級索引的更新操做,不是直接對索引進行更新,而是標記一下,而後產生一條新的。這個線程就是後臺的Purge線程。此外,過時的undolog也須要回收,這裏說的過時,指的是undo不須要被用來構建以前的版本,也不須要用來回滾事務。數組

回滾段: 能夠理解爲數據頁的修改鏈,鏈表最前面的是最老的一次修改,最後面的最新的一次修改,從鏈表尾部逆向操做能夠恢復到數據最老的版本。在InnoDB中,與之相關的還有undo tablespace, undo segment, undo slot, undo log這幾個概念。undo log是最小的粒度,所在的數據頁稱爲undo page,而後若干個undo page構成一個undo slot。一個事務最多能夠有兩個undo slot,一個是insert undo slot, 用來存儲這個事務的insert undo,裏面主要記錄了主鍵的信息,方便在回滾的時候快速找到這一行。另一個是update undo slot,用來存儲這個事務delete/update產生的undo,裏面詳細記錄了被修改以前每一列的信息,便於在讀請求須要的時候構造。1024個undo slot構成了一個undo segment。而後若干個undo segemnt構成了undo tablespace。緩存

歷史鏈表: insert undo能夠在事務提交/回滾後直接刪除,沒有事務會要求查詢新插入數據的歷史版本,可是update undo則不能夠,由於其餘讀請求可能須要使用update undo構建以前的歷史版本。所以,在事務提交的時候,會把update undo加入到一個全局鏈表(history list)中,鏈表按照事務提交的順序排序,保證最早提交的事務的update undo在前面,這樣Purge線程就能夠從最老的事務開始作清理。這個鏈表若是太長說明有不少記錄沒被完全刪除,也有不少undolog沒有被清理,這個時候就須要去看一下是否有個長事務沒提交致使Purge線程沒法工做。在InnoDB具體實現上,history list其實只是undo segment維度的,全局的history list採用最小堆來實現,最小堆的元素是某個undo segment中最小事務no對應的undopage。當這個undolog被Purge清理後,經過history list找到次小的,而後替換掉最小堆元素中的值,來保證下次Purge的順序的正確性。數據結構

回滾點: 又稱爲savepoint,事務回滾的時候能夠指定回滾點,這樣能夠保證回滾到指定的點,而不是回滾掉整個事務,對開發者來講,這是一個強大的功能。在InnoDB內部實現中,每打一個回滾點,其實就是保存一下當前的undo_no,回滾的時候直接回滾到這個undo_no點就能夠了。架構

核心數據結構

在分析核心的代碼以前,先介紹一下幾個核心的數據結構。這些結構貫穿整個事務系統,理解他們對理解整個InnoDB的工做原理也很有幫助。併發

trx_t: 整個結構體每一個鏈接持有一個,也就是在建立鏈接後執行第一個事務開始,整個結構體就被初始化了,後續這個鏈接的全部事務一直複用裏面的數據結構,直到這個鏈接斷開。同時,事務啓動後,就會把這個結構體加入到全局事務鏈表中(trx_sys->mysql_trx_list),若是是讀寫事務,還會加入到全局讀寫事務鏈表中(trx_sys->rw_trx_list)。在事務提交的時候,還會加入到全局提交事務鏈表中(trx_sys->trx_serial_list)。state字段記錄了事務四種狀態:TRX_STATE_NOT_STARTED, TRX_STATE_ACTIVE, TRX_STATE_PREPARED, TRX_STATE_COMMITTED_IN_MEMORY
這裏有兩個字段值得區分一下,分別是id和no字段。id是在事務剛建立的時候分配的(只讀事務永遠爲0,讀寫事務經過一個全局id產生器產生,非0),目的就是爲了區分不一樣的事務(只讀事務經過指針地址來區分),而no字段是在事務提交前,經過同一個全局id生產器產生的,主要目的是爲了肯定事務提交的順序,保證加入到history list中的update undo有序,方便purge線程清理。
此外,trx_t結構體中還有本身的read_view用來表示當前事務的可見範圍。分配的insert undo slot和update undo slot。若是是隻讀事務,read_only也會被標記爲true。mvc

trx_sys_t: 這個結構體用來維護系統的事務信息,全局只有一個,在數據庫啓動的時候初始化。比較重要的字段有:max_trx_id,這個字段表示系統當前還未分配的最小事務id,若是有一個新的事務,直接把這個值做爲新事務的id,而後這個字段遞增便可。descriptors,這個是一個數組,裏面存放着當前全部活躍的讀寫事務id,當須要開啓一個readview的時候,就從這個字段裏面拷貝一份,用來判斷記錄的對事務的可見性。rw_trx_list,這個主要是用來存放當前系統的全部讀寫事務,包括活躍的和已經提交的事務。按照事務id排序,此外,奔潰恢復後產生的事務和系統的事務也放在上面。mysql_trx_list,這裏面存放全部用戶建立的事務,系統的事務和奔潰恢復後的事務不會在這個鏈表上,可是這個鏈表上可能會有還沒開始的用戶事務。trx_serial_list,按照事務no(trx_t->no)排序的已經提交的事務。rseg_array,這個指向系統全部能夠用的回滾段(undo segments),當某個事務須要回滾段的時候,就從這裏分配。rseg_history_len, 全部提交事務的update undo的長度,也就是上文提到的歷史鏈表的長度,具體的update undo鏈表是存放在這個undo log中以文件指針的形式管理起來。view_list,這個是系統當前全部的readview, 全部開啓的readview的事務都會把本身的readview放在這個上面,按照事務no排序。框架

trx_purge_t: Purge線程使用的結構體,全局只有一個,在系統啓動的時候初始化。view,是一個readview,Purge線程不會嘗試刪除全部大於view->low_limit_no的undolog。limit,全部小於這個值的undolog均可以被truncate掉,由於標記的日誌已經被刪除且不須要用他們構建以前的歷史版本。此外,還有rseg,page_no, offset,hdr_page_no, hdr_offset這些字段,主要用來保存最後一個還未被purge的undolog。

read_view_t: InnDB爲了判斷某條記錄是否對當前事務可見,須要對此記錄進行可見性判斷,這個結構體就是用來輔助判斷的。每一個鏈接都的trx_t裏面都有一個readview,在事務須要一致性的讀時候(不一樣隔離級別不一樣),會被初始化,在讀結束的時候會釋放(緩存)。low_limit_no,這個主要是給purge線程用,readview建立的時候,會把當前最小的提交事務id賦值給low_limit_no,這樣Purge線程就能夠把全部已經提交的事務的undo日誌給刪除。low_limit_id, 全部大於等於此值的記錄都不該該被此readview看到,能夠理解爲high water mark。up_limit_id, 全部小於此值的記錄都應該被此readview看到,能夠理解爲low water mark。descriptors, 這是一個數組,裏面存了readview建立時候全部全局讀寫事務的id,除了事務本身作的變動外,此readview應該看不到descriptors中事務所作的變動。view_list,每一個readview都會被加入到trx_sys中的全局readview鏈表中。

trx_id_t: 每一個讀寫事務都會經過全局id產生器產生一個id,只讀事務的事務id爲0,只有當其切換爲讀寫事務時候再分配事務id。爲了保證在任何狀況下(包括數據庫不斷異常恢復),事務id都不重複,InnoDB的全局id產生器每分配256(TRX_SYS_TRX_ID_WRITE_MARGIN)個事務id,就會把當前的max_trx_id持久化到ibdata的系統頁上面。此外,每次數據庫重啓,都從系統頁上讀取,而後加上256(TRX_SYS_TRX_ID_WRITE_MARGIN)。

trx_rseg_t: undo segment內存中的結構體。每一個undo segment都對應一個。update_undo_list表示已經被分配出去的正在使用的update undo鏈表,insert_undo_list表示已經被分配出去的正在使用的insert undo鏈表。update_undo_cached和insert_undo_cached表示緩存起來的undo鏈表,主要爲了快速使用。last_page_no, last_offset, last_trx_no, last_del_marks表示這個undo segment中最後沒有被Purge的undolog。

事務的啓動

在InnoDB裏面有兩種事務,一種是讀寫事務,就是會對數據進行修改的事務,另一種是隻讀事務,僅僅對數據進行讀取。讀寫事務須要比只讀事務多作如下幾點工做:首先,須要分配回滾段,由於會修改數據,就須要找地方把老版本的數據給記錄下來,其次,須要經過全局事務id產生器產生一個事務id,最後,把讀寫事務加入到全局讀寫事務鏈表(trx_sys->rw_trx_list),把事務id加入到活躍讀寫事務數組中(trx_sys->descriptors)。所以,能夠看出,讀寫事務確實須要比只讀事務多作很多工做,在使用數據庫的時候儘量把事務申明爲只讀。

start transaction語句啓動事務。這種語句和begin work,begin等效。這些語句默認是以只讀事務的方式啓動。start transaction read only語句啓動事務。這種語句就把thd->tx_read_only置爲true,後續若是作了DML/DDL等修改數據的語句,會返回錯誤ER_CANT_EXECUTE_IN_READ_ONLY_TRANSACTIONstart transaction read write語句啓動事務。這種語句會把thd->tx_read_only置爲true,此外,容許super用戶在read_only參數爲true的狀況下啓動讀寫事務。start transaction with consistent snapshot語句啓動事務。這種啓動方式還會進入InnoDB層,並開啓一個readview。注意,只有在RR隔離級別下,這種操做纔有效,不然會報錯。

上述的幾種啓動方式,都會先去檢查前一個事務是否已經提交,若是沒有則先提交,而後釋放MDL鎖。此外,除了with consistent snapshot的方式會進入InnoDB層,其餘全部的方式都只是在Server層作個標記,沒有進入InnoDB作標記,在InnoDB看來全部的事務在啓動時候都是隻讀狀態,只有接受到修改數據的SQL後(InnoDB接收到才行。由於在start transaction read only模式下,DML/DDL都被Serve層擋掉了)才調用trx_set_rw_mode函數把只讀事務提高爲讀寫事務。

新建一個鏈接後,在開始第一個事務前,在InnoDB層會調用函數innobase_trx_allocate分配和初始化trx_t對象。默認的隔離級別爲REPEATABLE_READ,而且加入到mysql_trx_list中。注意這一步僅僅是初始化trx_t對象,可是真正開始事務的是函數trx_start_low,在trx_start_low中,若是當前的語句只是一條只讀語句,則先以只讀事務的形式開啓事務,不然按照讀寫事務的形式,這就須要分配事務id,分配回滾段等。

事務的提交

相比於事務的啓動,事務的提交就複雜許多。這裏只介紹事務在InnoDB層的提交過程,Server層涉及到與Binlog的XA事務暫時不介紹。入口函數爲innobase_commit

函數有一個參數commit_trx來控制是否真的提交,由於每條語句執行結束的時候都會調用這個函數,而不是每條語句執行結束的時候事務都提交。若是這個參數爲true,或者配置了autocommit=1, 則進入提交的核心邏輯。不然釋放由於auto_inc而形成的表鎖,而且記錄undo_no(回滾單條語句的時候用到,相關參數innodb_rollback_on_timeout)。
提交的核心邏輯:

  1. 依據參數innobase_commit_concurrency來判斷是否有過多的線程同時提交,若是太多則等待。
  2. 設置事務狀態爲committing,咱們能夠在show processlist看到(trx_commit_for_mysql)。
  3. 使用全局事務id產生器生成事務no,而後把事務trx_t加入到trx_serial_list。若是當前的undo segment沒有設置最後一個未Purge的undo,則用此事務no更新(trx_serialisation_number_get)。
  4. 標記undo,若是這個事務只使用了一個undopage且使用量小於四分之三個page,則把這個page標記爲(TRX_UNDO_CACHED)。若是不知足且是insert undo則標記爲TRX_UNDO_TO_FREE,不然undo爲update undo則標記爲TRX_UNDO_TO_PURGE。標記爲TRX_UNDO_CACHED的undo會被回收,方便下次從新利用(trx_undo_set_state_at_finish)。
  5. 把update undo放入所在undo segment的history list,並遞增trx_sys->rseg_history_len(這個值是全局的)。同時更新page上的TRX_UNDO_TRX_NO, 若是刪除了數據,則重置delete_mark(trx_purge_add_update_undo_to_history)。
  6. 把undate undo從update_undo_list中刪除,若是被標記爲TRX_UNDO_CACHED,則加入到update_undo_cached隊列中(trx_undo_update_cleanup)。
  7. 在系統頁中更新binlog名字和偏移量(trx_write_serialisation_history)。
  8. mtr_commit,至此,在文件層次事務提交。這個時候即便crash,重啓後依然能保證事務是被提交的。接下來要作的是內存數據狀態的更新(trx_commit_in_memory)。
  9. 若是是隻讀事務,則只須要把readview從全局readview鏈表中移除,而後重置trx_t結構體裏面的信息便可。若是是讀寫事務,狀況則複雜點,首先須要是設置事務狀態爲TRX_STATE_COMMITTED_IN_MEMORY,其次,釋放全部行鎖,接着,trx_t從rw_trx_list中移除,readview從全局readview鏈表中移除,另外若是有insert undo則在這裏移除(update undo在事務提交前就被移除,主要是爲了保證添加到history list的順序),若是有update undo,則喚醒Purge線程進行垃圾清理,最後重置trx_t裏的信息,便於下一個事務使用。

事務的回滾

InnoDB的事務回滾是經過undolog來逆向操做來實現的,可是undolog是存在undopage中,undopage跟普通的數據頁同樣,遵循bufferpool的淘汰機制,若是一個事務中的不少undopage已經被淘汰出內存了,那麼在回滾的時候須要從新把這些undopage從磁盤中撈上來,這會形成大量io,須要注意。此外,因爲引入了savepoint的概念,事務不只能夠所有回滾,也能夠回滾到某個指定點。

回滾的上層函數是innobase_rollback_trx,主要流程以下:

  1. 若是是隻讀事務,則直接返回。
  2. 判斷當前是回滾整個事務仍是部分事務,若是是部分事務,則記錄下須要保留多少個undolog,多餘的都回滾掉,若是是所有回滾,則記錄0(trx_rollback_step)。
  3. 從update undo和insert undo中找出最後一條undo,從這條undo開始回滾(trx_roll_pop_top_rec_of_trx)。
  4. 若是是update undo則調用row_undo_mod進行回滾,標記刪除的記錄清理標記,更新過的數據回滾到最老的版本。若是是insert undo則調用row_undo_ins進行回滾,插入操做,直接刪除彙集索引和二級索引。
  5. 若是是在奔潰恢復階段且須要回滾的undolog個數大於1000條,則輸出進度。
  6. 若是全部undo都已經被回滾或者回滾到了指定的undo,則中止,而且調用函數trx_roll_try_truncate把undolog刪除(因爲不須要使用unod構建歷史版本,因此不須要留給Purge線程)。
    此外,須要注意的是,回滾的代碼因爲是嵌入在query graphy的框架中,所以有些入口函數不太好找。例如,肯定回滾範圍的是在函數trx_rollback_step中,真正回滾的操做是在函數row_undo_step中,二者都是在函數que_thr_step被調用。

多版本控制MVCC

數據庫須要作好版本控制,防止不應被事務看到的數據(例如還沒提交的事務修改的數據)被看到。在InnoDB中,主要是經過使用readview的技術來實現判斷。查詢出來的每一行記錄,都會用readview來判斷一下當前這行是否能夠被當前事務看到,若是能夠,則輸出,不然就利用undolog來構建歷史版本,再進行判斷,知道記錄構建到最老的版本或者可見性條件知足。

在trx_sys中,一直維護這一個全局的活躍的讀寫事務id(trx_sys->descriptors),id按照從小到大排序,表示在某個時間點,數據庫中全部的活躍(已經開始但還沒提交)的讀寫(必須是讀寫事務,只讀事務不包含在內)事務。當須要一個一致性讀的時候(即建立新的readview時),會把全局讀寫事務id拷貝一份到readview本地(read_view_t->descriptors),當作當前事務的快照。read_view_t->up_limit_id是read_view_t->descriptors這數組中最小的值,read_view_t->low_limit_id是建立readview時的max_trx_id,即必定大於read_view_t->descriptors中的最大值。當查詢出一條記錄後(記錄上有一個trx_id,表示這條記錄最後被修改時的事務id),可見性判斷的邏輯以下(lock_clust_rec_cons_read_sees):

若是記錄上的trx_id小於read_view_t->up_limit_id,則說明這條記錄的最後修改在readview建立以前,所以這條記錄能夠被看見。

若是記錄上的trx_id大於等於read_view_t->low_limit_id,則說明這條記錄的最後修改在readview建立以後,所以這條記錄確定不能夠被看家。

若是記錄上的trx_id在up_limit_id和low_limit_id之間,且trx_id在read_view_t->descriptors之中,則表示這條記錄的最後修改是在readview建立之時,被另一個活躍事務所修改,因此這條記錄也不能夠被看見。若是trx_id不在read_view_t->descriptors之中,則表示這條記錄的最後修改在readview建立以前,因此能夠看到。

基於上述判斷,若是記錄不可見,則嘗試使用undo去構建老的版本(row_vers_build_for_consistent_read),直到找到能夠被看見的記錄或者解析完全部的undo。
針對RR隔離級別,在第一次建立readview後,這個readview就會一直持續到事務結束,也就是說在事務執行過程當中,數據的可見性不會變,因此在事務內部不會出現不一致的狀況。針對RC隔離級別,事務中的每一個查詢語句都單獨構建一個readview,因此若是兩個查詢之間有事務提交了,兩個查詢讀出來的結果就不同。從這裏能夠看出,在InnoDB中,RR隔離級別的效率是比RC隔離級別的高。此外,針對RU隔離級別,因爲不會去檢查可見性,因此在一條SQL中也會讀到不一致的數據。針對串行化隔離級別,InnoDB是經過鎖機制來實現的,而不是經過多版本控制的機制,因此性能不好。

因爲readview的建立涉及到拷貝全局活躍讀寫事務id,因此須要加上trx_sys->mutex這把大鎖,爲了減小其對性能的影響,關於readview有不少優化。例如,若是先後兩個查詢之間,沒有產生新的讀寫事務,那麼前一個查詢建立的readview是能夠被後一個查詢複用的。

垃圾回收Purge線程

Purge線程主要作兩件事,第一,數據頁內標記的刪除操做須要從物理上刪除,爲了提升刪除效率和空間利用率,由後臺Purge線程解析undolog按期批量清理。第二,當數據頁上標記的刪除記錄已經被物理刪除,同時undo所對應的記錄已經能被全部事務看到,這個時候undo就沒有存在的必要了,所以Purge線程還會把這些undo給truncate掉,釋放更多的空間。

Purge線程有兩種,一種是Purge Worker(srv_worker_thread), 另一種是Purge Coordinator(srv_purge_coordinator_thread),前者的主要工做就是從隊列中取出Purge任務,而後清理已經被標記的記錄。後者的工做除了清理刪除記錄外,還須要把Purge任務放入隊列,喚醒Purge Worker線程,此外,它還要truncate undolog。

咱們先來分析一下Purge Coordinator的流程。啓動線程後,會進入一個大的循環,循環的終止條件是數據庫關閉。在循環內部,首先是自適應的sleep,而後纔會進入核心Purge邏輯。sleep時間與全局歷史鏈表有關係,若是歷史鏈表沒有增加,且總數小於5000,則進入sleep,等待事務提交的時候被喚醒(srv_purge_coordinator_suspend)。退出循環後,也就是數據庫進入關閉的流程,這個時候就須要依據參數innodb_fast_shutdown來肯定在關閉前是否須要把全部記錄給清除。接下來,介紹一下核心Purge邏輯。

  1. 首先依據當前的系統負載來肯定須要使用的Purge線程數(srv_do_purge),即若是壓力小,只用一個Purge Cooridinator線程就能夠了。若是壓力大,就多喚醒幾個線程一塊兒作清理記錄的操做。若是全局歷史鏈表在增長,或者全局歷史鏈表已經超過innodb_max_purge_lag,則認爲壓力大,須要增長處理的線程數。若是數據庫處於不活躍狀態(srv_check_activity),則減小處理的線程數。
  2. 若是歷史鏈表很長,超過innodb_max_purge_lag,則須要從新計算delay時間(不超過innodb_max_purge_lag_delay)。若是計算結果大於0,則在後續的DML中須要先sleep,保證不會太快產生undo(row_mysql_delay_if_needed)。
  3. 從全局視圖鏈表中,克隆最老的readview,全部在這個readview開啓以前提交的事務所產生的undo都被認爲是能夠清理的。克隆以後,還須要把最老視圖的建立者的id加入到view->descriptors中,由於這個事務修改產生的undo,暫時還不能刪除(read_view_purge_open)。
  4. 從undo segment的最小堆中,找出最先提交事務的undolog(trx_purge_get_rseg_with_min_trx_id),若是undolog標記過delete_mark(表示有記錄刪除操做),則把先關undopage信息暫存在purge_sys_t中(trx_purge_read_undo_rec)。
  5. 依據purge_sys_t中的信息,讀取出相應的undo,同時把相關信息加入到任務隊列中。同時更新掃描過的指針,方便後續truncate undolog。
  6. 循環第4步和第5步,直到全局歷史鏈表爲空,或者接下到view->low_limit_no,即最老視圖建立時已經提交的事務,或者已經解析的page數量超過innodb_purge_batch_size
  7. 把全部的任務都放入隊列後,就能夠通知全部Purge Worker線程(若是有的話)去執行記錄刪除操做了。刪除記錄的核心邏輯在函數row_purge_record_func中。有兩種狀況,一種是數據記錄被刪除了,那麼須要刪除全部的彙集索引和二級索引(row_purge_del_mark),另一種是二級索引被更新了(老是先刪除+插入新記錄),因此須要去執行清理操做。
  8. 在全部提交的任務都已經被執行完後,就能夠調用函數trx_purge_truncate去刪除update undo(insert undo在事務提交後就被清理了)。每一個undo segment分別清理,從本身的histrory list中取出最先的一個undo,進行truncate(trx_purge_truncate_rseg_history)。truncate中,最終會調用fseg_free_page來清理磁盤上的空間。

事務的復活

在奔潰恢復後,也就是全部的前滾redo都應用完後,數據庫須要作undo回滾,至於哪些事務須要提交,哪些事務須要回滾,這取決於undolog和binlog的狀態。啓動階段,事務相關的代碼邏輯主要在函數trx_sys_init_at_db_start中,簡單分析一下。

  1. 首先建立管理undo segment的最小堆,堆中的元素是每一個undo segment提交最先的事務id和相應undo segment的指針,也就是說經過這個元素能夠找到這個undo segment中最老的未被Purge的undo。經過這個最小堆,能夠找到全部undo segment中最老未被Purge的undo,方便Purge線程操做。
  2. 建立全局的活躍讀寫事務id數組。主要是給readview使用。
  3. 初始化全部的undo segment。主要是從磁盤讀取undolog的內容,構建內存中的undo slot和undo segment,同時也構建每一個undo segment中的history list,由於若是是fast shutdown,被標記爲刪除的記錄可能還沒來得及被完全清理。此外,也構建每一個undo segment中的inset_undo_list和update_undo_list,理論上來講,若是數據庫關閉的時候全部事務都正常提交了,這兩個鏈表都爲空,若是數據庫非正常關閉,則鏈表非空(trx_undo_mem_create_at_db_start, trx_rseg_mem_create)。
  4. 從系統頁裏面讀取max_trx_id,而後加上TRX_SYS_TRX_ID_WRITE_MARGIN來保證trx_id不會重複,即便在很極端的狀況下。
  5. 遍歷全部的undo segment,針對每一個undo segment,分別遍歷inset_undo_list和update_undo_list,依據undo的狀態來複活事務。
  6. insert/update undo的處理邏輯:若是undolog上的狀態是TRX_UNDO_ACTIVE,則事務也被設置爲TRX_STATE_ACTIVE,若是undolog上的狀態是TRX_UNDO_PREPARED,則事務也被設置爲TRX_UNDO_PREPARED(若是force_recovery不爲0,則設置爲TRX_STATE_ACTIVE)。若是undolog狀態是TRX_UNDO_CACHED,TRX_UNDO_TO_FREE,TRX_UNDO_TO_PURGE,那麼都任務事務已經提交了(trx_resurrect_inserttrx_resurrect_update)。
  7. 除了從undolog中復活出事務的狀態信息,還須要復活出當前的鎖信息(trx_resurrect_table_locks),此外還須要把事務trx_t加入到rw_trx_list中。
  8. 全部事務信息復活後,InnoDB會作個統計,告訴你有多少undo須要作,所以能夠在錯誤日誌中看到相似的話: InnoDB: 120 transaction(s) which must be rolled back or cleaned up. InnoDB: in total 20M row operations to undo。
  9. 若是事務中操做了數據字典,好比建立刪除表和索引,則這個事務會在奔潰恢復結束後直接回滾,這個是個同步操做,會延長奔潰恢復的時間(recv_recovery_from_checkpoint_finish)。若是事務中沒有操做數據字典,則後臺會開啓一個線程,異步回滾事務,因此咱們經常發現,在數據庫啓動後,錯誤日誌裏面依然會有不少事務正在回滾的信息。

事務運維相關命令和參數

  1. 首先介紹一下information_schema中的三張表: innodb_trx, innodb_locks和innodb_lock_waits。因爲這些表幾乎須要查詢全部事務子系統的核心數據結構,爲了減小查詢對系統性能的影響,InnoDB預留了一塊內存,內存裏面存了相關數據的副本,若是兩次查詢的時間小於0.1秒(CACHE_MIN_IDLE_TIME_US),則訪問的都是同一個副本。若是超過0.1秒,則這塊內存會作一次更新,每次更新會把三張表用到的全部數據統一更新一遍,由於這三張表常常須要作錶鏈接操做,因此一塊兒更新能保證數據的一致性。這裏簡單介紹一下innodb_trx表中的字段,另外兩張表涉及到事物鎖的相關信息,因爲篇幅限制,後續有機會在介紹。
    trx_id: 就是trx_t中的事務id,若是是隻讀事務,這個id跟trx_t的指針地址有關,因此多是一個很大的數字(trx_get_id_for_print)。
    trx_weight: 這個是事務的權重,計算方法就是undolog數量加上事務已經加上鎖的數量。在事務回滾的時候,優先選擇回滾權重小的事務,有非事務引擎參與的事務被認爲權重是最大的。
    trx_rows_modified:這個就是當前事務已經產生的undolog數量,每更新一條記錄一次,就會產生一條undo。
    trx_concurrency_tickets: 每次這個事務須要進入InnoDB層時,這個值都會減一,若是減到0,則事務須要等待(壓力大的狀況下)。
    trx_is_read_only: 若是是以start transaction read only啓動事務的,那麼這個字段是1,不然爲0。
    trx_autocommit_non_locking: 若是一個事務是一個普通的select語句(後面沒有跟for update, share lock等),且當時的autocommit爲1,則這個字段爲1,不然爲0。
    trx_state: 表示事務當前的狀態,只能有RUNNING, LOCK WAIT, ROLLING BACK, COMMITTING這幾種狀態, 是比較粗粒度的狀態。
    trx_operation_state: 表示事務當前的詳細狀態,相比於trx_state更加詳細,例若有rollback to a savepoint, getting list of referencing foreign keys, rollback of internal trx on stats tables, dropping indexes等。

  2. 與事務相關的undo參數
    innodb_undo_directory: undo文件的目錄,建議放在獨立的一塊盤上,尤爲在常常有大事務的狀況下。
    innodb_undo_logs: 這個是定義了undo segment的個數。在給讀寫事務分配undo segment的時候,拿這個值去作輪訓分配。
    Innodb_available_undo_logs: 這個是一個status變量,在啓動的時候就肯定了,表示的是系統上分配的undo segment。舉個例子說明其與innodb_undo_logs的關係:假設系統初始化的時候innodb_undo_logs爲128,則在文件上必定有128個undo segment,Innodb_available_undo_logs也爲128,可是啓動起來後,innodb_undo_logs動態被調整爲100,則後續的讀寫事務只會使用到前100個回滾段,最後的20多個不會使用。
    innodb_undo_tablespaces: 存放undo segment的物理文件個數,文件名爲undoN,undo segment會比較均勻的分佈在undo tablespace中。

  3. 與Purge相關的參數
    innodb_purge_threads: Purge Worker和Purge Coordinator總共的個數。在實際的實現中,使用多少個線程去作Purge是InnoDB根據實時負載進行動態調節的。
    innodb_purge_batch_size: 一次性處理的undolog的數量,處理完這個數量後,Purge線程會計算是否須要sleep。
    innodb_max_purge_lag: 若是全局歷史鏈表超過這個值,就會增長Purge Worker線程的數量,也會使用sleep的方式delay用戶的DML。
    innodb_max_purge_lag_delay: 這個表示經過sleep方式delay用戶DML最大的時間。

  4. 與回滾相關的參數
    innodb_lock_wait_timeout: 等待行鎖的最大時間,若是超時,則會滾當前語句或者整個事務。發生回滾後返回相似錯誤:Lock wait timeout exceeded; try restarting transaction。
    innodb_rollback_on_timeout: 若是這個參數爲true,則當發生由於等待行鎖而產生的超時時,回滾掉整個事務,不然只回滾當前的語句。這個就是隱式回滾機制。主要是爲了兼容以前的版本。

總結

本文簡單介紹了InnoDB事務子系統的幾個核心模塊,在MySQL 5.7上,事務模塊還有不少特性,例如高優先級事務,事務對象池等。與事務相關的還有事務鎖系統,因爲篇幅限制,本文不介紹,詳情能夠參考本期月報的這篇。此外,在阿里雲最新發布的POLARDB for MySQL的版本中,因爲涉及到共享存儲架構,咱們對事務子系統又進行了大量的改造,後續的月報會詳細介紹。

相關文章
相關標籤/搜索