MySQL性能優化之Innodb事務系統,值得收藏
今天主要分享下Innodb事務系統的一些優化相關,以下基于mysql 5.7。
一、Innodb中的事務、視圖、多版本
1. 事務
在Innodb中,每次開啟一個事務時,都會為該session分配一個事務對象。而為了對全局所有的事務進行控制和協調,有一個全局對象trx_sys,對trx_sys相關成員的操作需要trx_sys->mutex鎖。
mysql數據庫遵循的是兩段鎖協議,將事務分成兩個階段,加鎖階段和解鎖階段(所以叫兩段鎖)
- 加鎖階段:在該階段可以進行加鎖操作。在對任何數據進行讀操作之前要申請并獲得S鎖(共享鎖,其它事務可以繼續加共享鎖,但不能加排它鎖),在進行寫操作之前要申請并獲得X鎖(排它鎖,其它事務不能再獲得任何鎖)。加鎖不成功,則事務進入等待狀態,直到加鎖成功才繼續執行。
- 解鎖階段:當事務釋放了一個封鎖以后,事務進入解鎖階段,在該階段只能進行解鎖操作不能再進行加鎖操作。
2. 視圖
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對當前事務是不可見的。
那么如何判斷可見性呢?
InnoDB表數據的組織方式為主鍵聚簇索引。由于采用索引組織表結構,記錄的ROWID是可變的(索引頁分裂的時候,Structure Modification Operation,SMO),因此二級索引中采用的是(索引鍵值, 主鍵鍵值)的組合來唯一確定一條記錄。無論是聚簇索引,還是二級索引,其每條記錄都包含了一個DELETED BIT位,用于標識該記錄是否是刪除記錄。除此之外,聚簇索引記錄還有兩個系統列:DATA_TRX_ID,DATA_ROLL_PTR。DATA _TRX_ID表示產生當前記錄項的事務ID;DATA _ROLL_PTR指向當前記錄項的undo信息。
聚簇索引行結構(與多版本一致讀有關的部分,DELETED BIT省略):
二級索引行結構:
從聚簇索引行結構,與二級索引行結構可以看出,聚簇索引中包含版本信息(事務號+回滾指針),二級索引不包含版本信息。
對于聚集索引,每次修改記錄時,都會在記錄中保存當前的事務ID,同時舊版本記錄存儲在UNDO中;對于二級索引,則在二級索引頁中存儲了更新當前頁的最大事務ID,如果該事務ID大于readview->up_limit_id(對于上例,up_limit_id值為2),那么就需要回聚集索引判斷記錄可見性;如果小于2, 那么總是可見的,可以直接讀取。
3. 多版本(MVCC)
為了便于理解MVCC的實現原理,這里簡單介紹一下undo log的工作過程
在不考慮redo log 的情況下利用undo log工作的簡化過程為:
說明:
- 為了保證數據的持久性數據要在事務提交之前持久化
- undo log的持久化必須在在數據持久化之前,這樣才能保證系統崩潰時,可以用undo log來回滾事務
MVCC只在READ COMMITED 和 REPEATABLE READ 兩個隔離級別下工作。READ UNCOMMITTED總是讀取最新的數據行,而不是符合當前事務版本的數據行。而SERIALIZABLE 則會對所有讀取的行都加鎖。
(1) SELECT
InnoDB 會根據兩個條件來檢查每行記錄:
- InnoDB只查找版本(DB_TRX_ID)早于當前事務版本的數據行(行的系統版本號<=事務的系統版本號,這樣可以確保數據行要么是在開始之前已經存在了,要么是事務自身插入或修改過的)
- 行的刪除版本號(DB_ROLL_PTR)要么未定義(未更新過),要么大于當前事務版本號(在當前事務開始之后更新的)。這樣可以確保事務讀取到的行,在事務開始之前未被刪除。
(2) INSERT
InnoDB為新插入的每一行保存當前系統版本號作為行版本號
(3) DELETE
InnoDB為刪除的每一行保存當前的系統版本號作為行刪除標識
(4) UPDATE
InnoDB為插入一行新記錄,保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識。
Innodb的多版本數據使用UNDO來維護的,例如聚集索引記錄(1) =>(2)=>(3),從1更新成2,再更新成3,就會產生兩條undo記錄。
二、Innodb事務系統優化
在MySQL 5.7版本里,針對性的對事務系統做了比較深入的優化,主要解決了下面幾個問題。
1. 視圖對象的創建需要trx_sys->mutex鎖保護
trx_sys->mutex是事務系統最核心的全局鎖對象,持有該鎖進行的操作都不應該耗時過長。對于read view對象,完全可以將其緩存下來重復使用。這樣就避免了持有鎖分配視圖內存。
因此在MySQL 5.7版本中,實例啟動時就分配1024個視圖對象;同時維護兩個鏈表,一個是已使用的視圖鏈表,一個是空閑的視圖鏈表;當需要分配新的視圖時,總是從空閑視圖鏈表中分配,如果沒有,再新分配一個。
2. 視圖對象中保存全局事務ID時,需要掃描事務鏈表
為了判斷事務視圖的可見性,在打開一個視圖時需要拷貝當時活躍的事務ID。
在5.7中,事務系統維持了一個全局事務ID數組,每個活躍讀寫事務的ID都被加入到其中,在事務提交時從其中刪除,這樣打開視圖時只需要使用memcpy 拷貝該數組即可,無需遍歷鏈表。在讀寫鏈表較長(高并發下)的場景,該優化可以顯著的提升性能。
3. 用戶需要顯式開啟只讀事務,才會放入只讀事務鏈表
mysql5.7將只讀事務鏈表從其中徹底移除了,取而代之的是,所有事務都以只讀模式打開。
例如如下事務序列:
- BEGIN;
- SELECT; //事務開始,不分配事務ID,不分配回滾段;
- UPDATE; //分配事務ID并插入全局事務數組和事務對象集合中,分配回滾段;
- COMMIT;
而對于BEGIN;SELECT;SELECT;COMMIT這樣的序列,整個事務周期既不分配事務ID,也不分配回滾段。
4. 隱式鎖轉換為顯式鎖的開銷
Innodb對于類似INSERT操作,采用的是隱式鎖的方式,隱式鎖不是鎖,只是一種稱呼而已,只有在需要的時候,才會轉換為顯式鎖。例如如下:
- Session 1: BEING; INSERT INTO t1(pk, val) VALUES (1,2); //不創建鎖對象
- Session 2: UPDATE t1 SET valval=val+1 WHERE pk=1; //創建兩個鎖對象,一個是為session1創建一個記錄鎖對象,另外一個是給自己創建一個等待類型的記錄鎖對象,然后session2加入鎖等待隊列。
在Session 2中為Session1創建鎖對象的過程即是所謂的隱式鎖向顯式鎖轉換。 當session2掃描到session 1插入的記錄時,發現session 1的事務依然活躍,就會進入轉換邏輯。
在5.6版本中,其轉換過程如下:
- 持有lock_sys->mutex
- 2持有trx_sys->mutex;根據事務ID,掃描讀寫事務鏈表,找到對應的事務對象;釋放trx_sys->mutex;
- 創建顯式鎖對象
- 釋放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的時間,從而提升性能。