前言
接上文,繼續學習後續章節。
第四章&第五章《深入淺出索引》
這兩章節主要介紹的索引結構及其如何合理建立索引,但是我覺得講的比較簡單。
總結回顧下吧,其實在我之前的文章《資料庫原理研究與優化》對索引這塊已有詳細的介紹了,並對如何合理建立和使用索引給出了建議。
索引的常見模型:雜湊表,有序陣列和搜尋樹。
雜湊表:典型的k-v儲存結構,比如memcached就用的雜湊表作為索引結構。
有序陣列:陣列的優缺點不用多數了,對於插入刪除成本太高,一般只用於插入場景很少的時候,比如存一寫靜態的資料。
搜尋樹:二叉搜尋樹,AVL樹,B樹,B+樹這些基本概念沒什麼好說的,屬於基礎了。InnoDB使用的索引結構就是B+樹,之所有使用B+樹,是因為它更符合磁碟的訪問模式。
InnoDB的索引模型
在InnoDB中索引被分為主鍵索引和非主鍵索引,我們常說InnoDB中資料即索引,索引即資料。
主鍵索引:主鍵索引的葉子節點儲存了具體的資料,所有又叫聚簇索引。
非主鍵索引:又叫輔助索引,輔助索引的葉子節點儲存的是對應的主鍵ID。
所以通過輔助索引查詢時,為了獲取其他資料,需要再去主鍵索引中查詢一遍獲取具體資料,這就叫回表。
顯然回表查詢時會額外增加查詢時間的,為了減少回表操作,我們可通過覆蓋索引來解決。
覆蓋索引:當我們根據輔助索引查詢時,需要得知的資料已經在輔助索引中,就不需要回表了,比如查詢結果只需要返回主鍵ID,那麼輔助索引就已經存了,就不需要再回表查詢了。
瞭解了索引模型,在如何優化SQL方面,其實就是圍繞如何合理使用索引,使得查詢效率更高。
索引的維護
如何主鍵是遞增的,那麼在插入資料時,只需要按索引順序插入即可。
但當有索引欄位的資料並不是按順序插入時,當要插入的位置的資料頁已經寫滿了,那就需要申請一個新的資料頁,把部分資料移動到新的資料頁,以為改資料的插入,這成為頁分裂。頁分裂不僅影響效能,還會影響資料頁空間使用率。當然資料的查詢,當頁利用率很低後還會有頁的合併操作。
索引設計與使用原則
參考文章《資料庫原理研究與優化》
擴充套件:B+樹深入剖析
由於作者講這塊比較淺,這裡我做一個擴充套件。
為什麼要使用B+樹
⾸先需要明確的是,B樹或B+樹要⽐⼆叉樹更適合作為索引儲存,因為B樹中的節點可以儲存多個資料,從⽽就可以減少樹的⾼度,也就減少了提升了查詢效能。那為什麼不選B樹,而選B+樹呢?
主要原因體現在3個⽅⾯:
-
B+樹的磁碟讀寫代價更低
B+樹的內部節點並沒有指向關鍵字具體資訊的指標,因此其內部節點相對B樹更⼩,如果把所有同⼀內部節點的關鍵字存放在同⼀盤塊中,那麼盤塊所能容納的關鍵字數量也越多,⼀次性讀⼊記憶體的需要查詢的關鍵字也就越多,相對IO讀寫次數就降低了。 -
B+樹的查詢效率更加穩定
-
由於⾮終節點並不是最終指向⽂件內容的節點,⽽只是葉⼦節點中關鍵字的索引。所以任何關鍵字的查詢必須⾛⼀條從根節點到葉⼦節點的路。
-
所有關鍵字查詢的路徑⻓度相同,導致每⼀個資料的查詢效率相當。
-
-
由於B+樹的資料都儲存在葉⼦節點中,分⽀節點均為索引,⽅便掃庫,只需要掃⼀遍葉⼦節點即可,但是B樹因為其分⽀節點同樣儲存著資料,我們要找到具體的資料,需要從根節點按序開始掃描,所以B+樹更加適合在區間查詢的情況,所以通常B+樹⽤於資料庫索引。
索引的實現
InnoDB中的索引結構與MyISAM的索引結構有很⼤的不同。
第⼀個重⼤區別是InnoDB的資料⽂件本身就是索引⽂件。在MyISAM中,索引⽂件和資料⽂件是分離的,索引⽂件僅儲存資料記錄的地址。⽽在InnoDB中,表資料⽂件本身就是按B+Tree組織的⼀個索引結構,這棵樹的葉節點data域儲存了完整的資料記錄,這個索引的key是資料表的主鍵,所以InnoDB表資料⽂件本身就是主索引。這種索引叫做聚集索引。
因為InnoDB的資料⽂件本身要按主鍵聚集,所以InnoDB要求表必須有主鍵(MyISAM可以沒有),如果沒有顯式指定,則MySQL系統會⾃動選擇⼀個可以唯⼀標識資料記錄的列作為主鍵,如果不存在這種列,則MySQL⾃動為InnoDB表⽣成⼀個隱含欄位作為主鍵,這個欄位⻓度為6個位元組,型別為⻓整形。
第⼆個與MyISAM索引的不同是,InnoDB的輔助索引data域儲存相應記錄主鍵的值⽽不是地址。換句話說InnoDB的所有輔助索引都引⽤主鍵作為data域。
聚集索引這種實現⽅式使得按主鍵的搜尋⼗分⾼效,但是輔助索引搜尋需要檢索兩遍索引:⾸先檢索輔助索引獲得主鍵,然後⽤主鍵到主索引中檢索獲得記錄。
InnoDB的B+ 樹索引的特點是⾼扇出性,因此⼀般樹的⾼度為2~4層,這樣我們在查詢⼀條記錄時只⽤I/O 24次。當前機械硬碟每秒⾄少100次I/O/s,因此查詢時間只需0.020.04s。
第六章《全域性鎖和表鎖》&第七章《行鎖》
根據加鎖的範圍, 根據加鎖的範圍, MySQL裡面的鎖大致可以分成全域性鎖、表級鎖和行鎖三類 裡面的鎖大致可以分成全域性鎖、表級鎖和行鎖三類 。
全域性鎖
顧名思義,就是對整個庫加鎖,加鎖之後,整個庫變成了只讀。一般只用於對整個庫進行備份的場景。
加全域性鎖備份,是為了保證資料的一致性。但其實但對資料庫進行dump的時候,MySQL會啟動一個事務,來確保拿到一致性檢視,由MVCC的支援,這時候資料依然可以正常更新的。
既然有這機制為什麼還要有全域性鎖的操作,需要注意的是,事務是有儲存引擎實現的,並不是所有的引擎都支援。當你庫中表都是innoDB引擎的時候,就可以這樣備份。所以為什麼建議大家預設使用innoDB引擎。
表級鎖
表級鎖分兩種:一種表鎖(TL),一種後設資料鎖(MDL)
表鎖分為:讀鎖跟寫鎖
當一個執行緒執行lock t1 read,t2 write時,其他執行緒對錶T1只能讀,對T2讀寫都被阻塞。執行緒本身對T1可讀,對T2可讀寫。
一般情況下不建議使用表鎖,除非引擎不支援行鎖。
後設資料鎖(MDL):
MDL鎖不需要顯式加上,當訪問一張表(增刪改查都會加)的時候,會自動加MDL讀鎖,當需要對一張表更改表結構時會自動加MDL寫鎖。
讀鎖之前不互斥,多個執行緒可以同時對同一張表CRUD;
讀鎖與寫鎖,寫鎖與寫鎖之間都是互斥的。當一個執行緒對錶進行CRUD時,另外一個執行緒要對這張表刪除一列,則需要等待第一個執行緒操作結束。或者兩個執行緒同時對一張表進行加欄位時,需要序列。
MDL鎖會在事務提交時釋放,所以在長事務裡,事務不提交,就會一直佔著MDL鎖。
行鎖
行鎖是基於引擎層實現的,並不是所有引擎都支援行鎖,MyISAM是不支援的,innoDB支援行鎖。
但平時我們運算元據庫時,並不需要關心什麼時候加鎖,什麼時候釋放鎖,因為這都是MySQL自動幫我們處理的。例如當我們需要更新id=1這行資料時,就會對這行進行加鎖,當有另外一個執行緒也要更新id=1這行資料時,就需要等待。
在事務中,行鎖的釋放,不是對該行資料執行完就立即釋放的,而是等事務提交才釋放。這就是所謂的兩階段鎖。
那麼為了減少行多帶來的效能問題,當你一個事務中要更新多行對多個行進行加鎖時,把可能有併發操作的哪一行操作放到最後執行,以減少鎖等待時間。當然這還得取決於業務流程。
死鎖
當併發系統多個執行緒迴圈爭奪資源,執行緒之間都在等待其他執行緒釋放資源,就會導致這些執行緒處於無限等待狀態,這稱為死鎖。
以資料庫為例,兩個執行緒,第一個執行緒同時先對id=1再對id=2這行進行更新,第二個執行緒需要先對id=2再對id=1進行更新,當執行緒1執行到要對id=2更新是,由於此時id=2的行鎖被執行緒2持有,便處於等待狀態,當執行緒2執行到id=1進行更新時,此時id=1的行鎖被執行緒1持有,這便造成了死鎖。
顯然我們不可能一直讓這狀態無限持續下去,MySQL可以對鎖等待時間進行設定,通過innoDB_lock_wait_timeout來設定,比如設定60s,那麼時間到了之後,第一個被鎖住的就會超時退出。但有些業務場景,無法等待這麼久的時間的,當也不能設定太短,設定1s,也會對其他正常鎖等待的執行緒,造成誤傷。
除此之外,還可以通過死鎖主動檢測,通過innoDB_deadlock_detect設定on,預設就是on。它會主動檢測發生死鎖的時候,然後快速發現處理,但這本身也有額外負擔的。
當一個新的執行緒加入處於堵塞狀態,就要判斷是否由於自己加入導致了死鎖,當有1000個這樣的執行緒,那麼可想而知這效能消耗,會佔用大量的CPU資源。
所以一般在併發量不高的系統可以這麼用。
筆者鎖在的系統使用的是第一個,設定鎖等待時間。
第八章《事務到底是隔離還是不隔離》
先看個例子:已經k的初始值是1,那麼在事務ABC中分別查到的k值是多少呢?前提是資料庫的隔離級別:可重複讀。start transaction with consistent snapshot執行該命名錶示立即開啟事務。
結果是:A中k=1,B中k=3,Ck=2;你是否答對了呢?
1、A先開啟時了事務,由於隔離級別為可重複讀,所有在該事務內,無論什麼時候讀取k,都是。
2、B開啟了事務,還未執行更新時,C緊接著執行了更新,C的事務時在執行更新語句時自動開啟,在更新結束後自動提交的,所以C更新完,此時k=2。
3、當B執行更新時,由於C也在更新,所以出於行鎖等待C更新完畢,輪到B時,此時K已經是2了,所以B執行更新時,把K更新成了3。所以更新完再次查詢時,查出來的就是。
此時有人可能有疑惑,不是說在可重複隔離級別下,開啟事務後,在事務內查的結果都不會變嗎,B是在C更新之前開啟的時候,那時候K還是1,怎麼後面再查詢就變成了3了呢,這就得引入當前讀(current read)了。
有一條這樣的規則:更新資料都是先讀後寫的,而這個讀,只能讀當前的值。所以當B執行更新時先讀的是K當前的值2,在2的基礎上進行更新的。
MVCC如何實現的
在MySQL中有兩個檢視的概念:
1、就是view,就是用查詢語句定義的虛擬表,通過create view...建立。
2、還有個就是innoDB在實現MVCC用到的一致性檢視,即consistent read view,用於支援讀提交和可重複讀隔離級別的實現。
當一致性檢視指的並不是實際存在的物理結構,只是一個邏輯定義,他定義了在事務執行期間能看到什麼資料。
那麼這個一致性檢視到底是如何實現的?
在邏輯上我們認為,當開啟事務後,就會生成一個靜態的”檢視“,這個“檢視”是基於整個庫的。如果這個“檢視”是真實存在的,假設整個圖有幾百G的資料,要為這個庫在秒級生成對應的靜態檢視,是不是不大可能?
實際上,innoDB中每個事務都有一個唯一的事務ID,transaction id。它在事務開始的時候向事務系統申請的,並且順序嚴格遞增的。
而每行資料也都是有多個版本的。每次事務更新資料的時候,都會生成一個新的資料版本,並且把transaction id賦值給這個資料版本的事務ID,記為row trx _id。同時,舊的資料版本要保留,並且在新的資料版本中,能夠有資訊可以直接拿到它。
也就是說,資料表中的一行記錄,其實可能有多個版本( row ) ,每個版本有自己的row trx _id。
實際上這個每個版本的數值,是根據undo log 推算出來的。
按照可重複讀的定義,在事務啟動的時候,innoDB為這個事務構造了一個陣列,用來儲存事務啟動的瞬間,當前正在活躍的所有事務id。陣列中id最小值為低水位,最大值+1位高水位,這個陣列事務+高水位組成了當前的一致性檢視(read-view)。
所以當一個事務開啟後,一個版本的row trx_id如果比低水位小,那就是說明這是已提交的事務,這個資料是可見的.
如果比高水位高,說明是將來的事務,是不可見的。
如果處於低水位和高水位的,說明row trx_id在陣列中,說明是還未提交的事務,不可見,如果不在,說明是已提交的事務,是可見的。
所以,innoDB利用“所有資料都有多個版本”這個特性,實現了秒級建立”快照“的能力。