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


前言

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

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

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

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

索引的常見模型:哈希表,有序數組和搜索樹。

哈希表:典型的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利用“所有數據都有多個版本”這個特性,實現了秒級創建”快照“的能力。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM