MySQL深入研究--學習總結(2)

有夢想的老王 發表於 2021-03-04

前言

接上文,繼續學習後續章節。

第四章&第五章《深入淺出索引》

這兩章節主要介紹的索引結構及其如何合理建立索引,但是我覺得講的比較簡單。

總結回顧下吧,其實在我之前的文章《資料庫原理研究與優化》對索引這塊已有詳細的介紹了,並對如何合理建立和使用索引給出了建議。

索引的常見模型:雜湊表,有序陣列和搜尋樹。

雜湊表:典型的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資源。

所以一般在併發量不高的系統可以這麼用。

筆者鎖在的系統使用的是第一個,設定鎖等待時間。

第八章《事務到底是隔離還是不隔離》

MySQL深入研究--學習總結(2)

先看個例子:已經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)。

MySQL深入研究--學習總結(2)

所以當一個事務開啟後,一個版本的row trx_id如果比低水位小,那就是說明這是已提交的事務,這個資料是可見的.

如果比高水位高,說明是將來的事務,是不可見的。

如果處於低水位和高水位的,說明row trx_id在陣列中,說明是還未提交的事務,不可見,如果不在,說明是已提交的事務,是可見的。

所以,innoDB利用“所有資料都有多個版本”這個特性,實現了秒級建立”快照“的能力。