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