<b>背景知識</b>
為了便于了解下文,我們先簡單梳理下innodb中的事務、視圖、多版本的相關背景知識。
在innodb中,每次開啟一個事務時,都會為該session配置設定一個事務對象。而為了對全局所有的事務進行控制和協調,有一個全局對象trx_sys,對trx_sys相關成員的操作需要trx_sys->mutex鎖。
innodb使用一種稱做readview(視圖)的對象來判斷事務的可見性(也就是acid中的隔離性)。根據可見性原則,某個新開啟的事務不應該看到其他未送出的事務。 innodb在執行一個select或者顯式開啟start transaction with consistent snapshot (後者隻應用于repeatable-read隔離級别) 會建立一個視圖對象。對于rr隔離級别,視圖的生命周期到事務送出結束,對于rc隔離級别,則每條查詢開始時重配置設定事務。
通常一個視圖中包含建立視圖的事務id,以及在建立視圖時活躍的事務id數組。例如,當開啟一個視圖時,目前事務的事務id為5, 事務連結清單上活躍事務id為{2,5,6,9,12},那麼就會把{2,6,9,12}存儲到目前的視圖中(5是目前事務的id,不記錄到視圖中),{2,6,9,12}對應的事務所做的修改對目前事務而言都是不可見的,小于2的事務id對目前事務都是可見的,大于12的事務id對目前事務是不可見的。
那麼如何判斷可見性呢? 對于聚集索引,每次修改記錄時,都會在記錄中儲存目前的事務id,同時舊版本記錄存儲在undo中;對于二級索引,則在二級索引頁中存儲了更新目前頁的最大事務id,如果該事務id大于readview->up_limit_id(對于上例,up_limit_id值為2),那麼就需要回聚集索引判斷記錄可見性;如果小于2, 那麼總是可見的,可以直接讀取。
innodb的多版本資料使用undo來維護的,例如聚集索引記錄(1) =>(2)=>(3),從1更新成2,再更新成3,就會産生兩條undo記錄。當然這不是本文讨論的重點。後續在單獨針對臨時表的優化時會談及undo相關的知識。
<b>innodb事務系統優化</b>
在mysql 5.7版本裡,針對性的對事務系統做了比較深入的優化,主要解決了下面幾個問題。
問題一:視圖對象的建立需要trx_sys->mutex鎖保護
trx_sys->mutex是事務系統最核心的全局鎖對象,持有該鎖進行的操作都不應該耗時過長。對于read view對象,完全可以将其緩存下來重複使用。這樣就避免了持有鎖配置設定視圖記憶體。
是以在mysql 5.7版本中,執行個體啟動時就配置設定1024個視圖對象;同時維護兩個連結清單,一個是已使用的視圖連結清單,一個是空閑的視圖連結清單;當需要配置設定新的視圖時,總是從空閑視圖連結清單中配置設定,如果沒有,再新配置設定一個。
在percona server中也做了類似的優化,但與5.7不同的是,其不集中管理所有的視圖,而是每個事務對象(trx_t)上都挂載一個預配置設定的視圖對象,在事務對象銷毀時釋放(事務對象本身對session而言也是重用的)。
問題二:視圖對象中儲存全局事務id時,需要掃描事務連結清單
正如上面描述的,為了判斷事務視圖的可見性,在打開一個視圖時需要拷貝當時活躍的事務id。在5.5及之前版本需要周遊所有的活躍事務,而在5.6中,将事務連結清單拆分成了隻讀事務連結清單,和讀寫事務連結清單,這樣我們隻需要周遊讀寫事務連結清單,拷貝事務id即可。
在5.7中,事務系統維持了一個全局事務id數組,每個活躍讀寫事務的id都被加入到其中,在事務送出時從其中删除,這樣打開視圖時隻需要使用memcpy 拷貝該數組即可,無需周遊連結清單。在讀寫連結清單較長(高并發下)的場景,該優化可以顯著的提升性能。不過就該優化點而言,percona serve同樣走在了前面,相同的思路實作在percona server 5.6中。
問題三: 使用者需要顯式開啟隻讀事務,才會放入隻讀事務連結清單
盡量在5.6中已經将事務連結清單拆分成了隻讀事務連結清單和讀寫事務連結清單(autocommit的select不加入任何連結清單),但使用者需要顯式的指定事務以隻讀模式打開(start transaction read only)或者設定session變量tx_read_only。
顯然這種方式對使用者而言是極不友好的,是以在5.7中做了比較徹底的改變,将隻讀事務連結清單從其中徹底移除了,取而代之的是,所有事務都以隻讀模式打開。
例如如下事務序列:
begin;
select; //事務開始,不配置設定事務id,不配置設定復原段;
update; //配置設定事務id并插入全局事務數組和事務對象集合中,配置設定復原段;
commit;
而對于begin;select;select;commit這樣的序列,整個事務周期既不配置設定事務id,也不配置設定復原段。
那麼問題來了,既然隻讀的事務不配置設定事務id,那麼如何标示事務呢,在5.7中,使用事務對象的位址來進行計算得到一個唯一的事務id。執行’show engine innodb status’不再顯示活躍的隻讀事務,隻能通過innodb_trx表來查詢。這是一個需要注意的點,因為很多人都是通過前者來找到長時間未送出的事務。
另外一個比較有意思的小優化是,對于autocommit的隻讀查詢,關閉視圖時,并不是立刻從視圖連結清單中移除,而是設定一個簡單的close标記;該session下次需要打開該read view時,如果這期間沒有任何讀寫事務,就可以直接重用上次的read view,清楚close标記,這樣打開、關閉視圖都無需擷取trx_sys->mutex。
問題四:隐式鎖轉換為顯式鎖的開銷
innodb對于類似insert操作,采用的是隐式鎖的方式,隐式鎖不是鎖,隻是一種稱呼而已,隻有在需要的時候,才會轉換為顯式鎖。例如如下:
session 1: being; insert into t1(pk, val) values (1,2); //不建立鎖對象
session 2: update t1 set val=val+1 where pk=1; //建立兩個鎖對象,一個是為session1建立一個記錄鎖對象,另外一個是給自己建立一個等待類型的記錄鎖對象,然後session2加入鎖等待隊列。
在session 2中為session1建立鎖對象的過程即是所謂的隐式鎖向顯式鎖轉換。 當session2掃描到session 1插入的記錄時,發現session 1的事務依然活躍,就會進入轉換邏輯。
在5.6版本中,其轉換過程如下:
1.持有lock_sys->mutex
2. 持有trx_sys->mutex;
根據事務id,掃描讀寫事務連結清單,找到對應的事務對象;
釋放trx_sys->mutex;
3.建立顯式鎖對象
4.釋放lock_sys->mutex
可以看到,在該操作的過程中,全程持有lock_sys->mutex,持有鎖的原因是防止事務送出掉。當讀寫事務連結清單非常長時(例如高并發寫入時),這種開銷将是不可接受的。
在5.7版本中,上述邏輯則優化成:
1. 持有trx_sys->mutex
根據事務id找到對應的事務對象(直接查找trx_sys->rw_trx_set,其儲存了trx_id和事務對象的映射關系,是以無需掃描讀寫事務連結清單)
增加事務對象引用計數(++trx->n_ref)
釋放trx_sys->mutex
2. 持有lock_sys->mutex;
建立顯式鎖對象;
釋放lock_sys->mutex;
3.遞減事務對象引用計數
在事務commit,釋放記錄鎖前,會先判斷引用記錄數是否為0,如果不為0,表示正有其他事務為其轉換顯式鎖,這時候需要等待,直到計數為0,才能進入釋放事務記錄鎖階段。
總的來說,該優化減少了隐式鎖轉換時持有lock_sys->mutex的時間,進而提升性能。
除了上述提到的幾點事務優化外,在5.7版本中還對事務系統部分的代碼做了重構,完全用c++重寫;引入了一個pool結構,事務對象和鎖對象都可以緩存複用。大家可以閱讀幾個相關的worklog,以更好的了解上述優化:
<a href="http://dev.mysql.com/worklog/task/?id=6047">http://dev.mysql.com/worklog/task/?id=6047</a>
<a href="http://dev.mysql.com/worklog/task/?id=6578">http://dev.mysql.com/worklog/task/?id=6578</a>
<a href="http://dev.mysql.com/worklog/task/?id=6899">http://dev.mysql.com/worklog/task/?id=6899</a>
<a href="http://dev.mysql.com/worklog/task/?id=6906">http://dev.mysql.com/worklog/task/?id=6906</a>