資料庫兩大必備神器:索引和鎖底層原理是什麼!
一、索引
在之前,我對索引有以下的認知:
索引可以加快資料庫的檢索速度;
表經常進行INSERT/UPDATE/DELETE操作就不要建立索引了,換言之:索引會降低插入、刪除、修改等維護任務的速度;
索引需要佔物理和資料空間;
瞭解過索引的最左匹配原則;
知道索引的分類:聚集索引和非聚集索引;
Mysql支援Hash索引和B+樹索引兩種;
看起來好像啥都知道,但面試讓你說的時候可能就GG了:
使用索引為什麼可以加快資料庫的檢索速度啊?
為什麼說索引會降低插入、刪除、修改等維護任務的速度;
索引的最左匹配原則指的是什麼?
Hash索引和B+樹索引有什麼區別?主流的使用哪一個比較多?InnoDB儲存都支援嗎?
聚集索引和非聚集索引有什麼區別?
........
1、聊聊索引的基礎知識
首先Mysql的基本儲存結構是頁(記錄都存在頁裡邊):


各個資料頁可以組成一個雙向連結串列;
而每個資料頁中的記錄又可以組成一個單向連結串列;
每個資料頁都會為儲存在它裡邊兒的記錄生成一個頁目錄,在通過主鍵查詢某條記錄的時候可以在頁目錄中使用二分法快速定位到對應的槽,然後再遍歷該槽對應分組中的記錄即可快速找到指定的記錄;
以其他列(非主鍵)作為搜尋條件:只能從最小記錄開始依次遍歷單鏈表中的每條記錄。
所以說,如果我們寫select * from user where username = 'Java3y'這樣沒有進行任何優化的sql語句,預設會這樣做:
定位到記錄所在的頁
需要遍歷雙向連結串列,找到所在的頁
從所在的頁內中查詢相應的記錄
由於不是根據主鍵查詢,只能遍歷所在頁的單鏈表了
很明顯,在資料量很大的情況下這樣查詢會很慢!
2、索引提高檢索速度
索引做了些什麼可以讓我們查詢加快速度呢?
其實就是將無序的資料變成有序(相對):

要找到id為8的記錄簡要步驟:

很明顯的是:沒有用索引我們是需要遍歷雙向連結串列來定位對應的頁,現在通過"目錄"就可以很快地定位到對應的頁上了!
其實底層結構就是B+樹,B+樹作為樹的一種實現,能夠讓我們很快地查找出對應的記錄。
3、索引降低增刪改的速度

如果一棵普通的樹在極端的情況下,是能退化成連結串列的(樹的優點就不復存在了)

B+樹是平衡樹的一種,是不會退化成連結串列的,樹的高度都是相對比較低的(基本符合矮矮胖胖(均衡)的結構)【這樣一來我們檢索的時間複雜度就是O(logn)】!從上一節的圖我們也可以看見,建立索引實際上就是建立一顆B+樹。
B+樹是一顆平衡樹,如果我們對這顆樹增刪改的話,那肯定會破壞它的原有結構;
要維持平衡樹,就必須做額外的工作。正因為這些額外的工作開銷,導致索引會降低增刪改的速度;
4、雜湊索引
除了B+樹之外,還有一種常見的是雜湊索引。
雜湊索引就是採用一定的雜湊演算法,把鍵值換算成新的雜湊值,檢索時不需要類似B+樹那樣從根節點到葉子節點逐級查詢,只需一次雜湊演算法即可立刻定位到相應的位置,速度非常快。
本質上就是把鍵值換算成新的雜湊值,根據這個雜湊值來定位。

看起來雜湊索引很牛逼啊,但其實雜湊索引有好幾個侷限(根據他本質的原理可得):
雜湊索引也沒辦法利用索引完成排序;
不支援最左匹配原則;
在有大量重複鍵值情況下,雜湊索引的效率也是極低的---->雜湊碰撞問題;
不支援範圍查詢;
5、InnoDB支援雜湊索引嗎?
主流的還是使用B+樹索引比較多,對於雜湊索引,InnoDB是自適應雜湊索引的(hash索引的建立由InnoDB儲存引擎引擎自動優化建立,我們干預不了)!

6、聚集和非聚集索引
簡單概括:
聚集索引就是以主鍵建立的索引;
非聚集索引就是以非主鍵建立的索引;
區別:
聚集索引在葉子節點儲存的是表中的資料;
非聚集索引在葉子節點儲存的是主鍵和索引列;
使用非聚集索引查詢出資料時,拿到葉子上的主鍵再去查到想要查詢的資料。(拿到主鍵再查詢這個過程叫做回表)
非聚集索引也叫做二級索引,不用糾結那麼多名詞,將其等價就行了~
非聚集索引在建立的時候也未必是單列的,可以多個列來建立索引。
此時就涉及到了哪個列會走索引,哪個列不走索引的問題了(最左匹配原則-->後面有說)
建立多個單列(非聚集)索引的時候,會生成多個索引樹(所以過多建立索引會佔用磁碟空間)

在建立多列索引中也涉及到了一種特殊的索引-->覆蓋索引
我們前面知道了,如果不是聚集索引,葉子節點儲存的是主鍵+列值
最終還是要“回表”,也就是要通過主鍵再查詢一次。這樣就會比較慢
覆蓋索引就是把要查詢出的列和索引是對應的,不做回表操作!
比如說:
現在我建立了索引(username,age),在查詢資料的時候:select username , age from user where username = 'Java3y' and age = 20。
很明顯地知道,我們上邊的查詢是走索引的,並且,要查詢出的列在葉子節點都存在!所以,就不用回表了~
所以,能使用覆蓋索引就儘量使用吧~
7、索引最左匹配原則
最左匹配原則:
索引可以簡單如一個列(a),也可以複雜如多個列(a, b, c, d),即聯合索引。
如果是聯合索引,那麼key也由多個列組成,同時,索引只能用於查詢key是否存在(相等),遇到範圍查詢(>、<、between、like左匹配)等就不能進一步匹配了,後續退化為線性查詢。
因此,列的排列順序決定了可命中索引的列數。
例子:
如有索引(a, b, c, d),查詢條件a = 1 and b = 2 and c > 3 and d = 4,則會在每個節點依次命中a、b、c,無法命中d。(很簡單:索引命中只能是相等的情況,不能是範圍匹配)
8、=、in自動優化順序
不需要考慮=、in等的順序,mysql會自動優化這些條件的順序,以匹配儘可能多的索引列。
例子:
如有索引(a, b, c, d),查詢條件c > 3 and b = 2 and a = 1 and d < 4與a = 1 and c > 3 and b = 2 and d < 4等順序都是可以的,SQL/">MySQL會自動優化為a = 1 and b = 2 and c > 3 and d < 4,依次命中a、b、c。
9、索引總結
索引在資料庫中是一個非常重要的知識點!上面談的其實就是索引最基本的東西,要創建出好的索引要顧及到很多的方面:
1,最左字首匹配原則。這是非常重要、非常重要、非常重要(重要的事情說三遍)的原則,MySQL會一直向右匹配直到遇到範圍查詢(>,<,BETWEEN,LIKE)就停止匹配。
3,儘量選擇區分度高的列作為索引,區分度的公式是 COUNT(DISTINCT col) / COUNT(*)。表示欄位不重複的比率,比率越大我們掃描的記錄數就越少。
4,索引列不能參與計算,儘量保持列“乾淨”。比如,FROM_UNIXTIME(create_time) = '2016-06-06' 就不能使用索引,原因很簡單,B+樹中儲存的都是資料表中的欄位值,但是進行檢索時,需要把所有元素都應用函式才能比較,顯然這樣的代價太大。所以語句要寫成 : create_time = UNIX_TIMESTAMP('2016-06-06')。
5,儘可能的擴充套件索引,不要新建立索引。比如表中已經有了a的索引,現在要加(a,b)的索引,那麼只需要修改原來的索引即可。
6,單個多列組合索引和多個單列索引的檢索查詢效果不同,因為在執行SQL時,MySQL只能使用一個索引,會從多個單列索引中選擇一個限制最為嚴格的索引。
二、鎖

在mysql中的鎖看起來是很複雜的,因為有一大堆的東西和名詞:排它鎖,共享鎖,表鎖,頁鎖,間隙鎖,意向排它鎖,意向共享鎖,行鎖,讀鎖,寫鎖,樂觀鎖,悲觀鎖,死鎖。這些名詞有的部落格又直接寫鎖的英文的簡寫--->X鎖,S鎖,IS鎖,IX鎖,MMVC...
鎖的相關知識又跟儲存引擎,索引,事務的隔離級別都是關聯的....
這就給初學資料庫鎖的人帶來不少的麻煩~~~於是我下面就簡單整理一下資料庫鎖的知識點,希望大家看完會有所幫助。
1、為什麼需要學習資料庫鎖知識
不少人在開發的時候,應該很少會注意到這些鎖的問題,也很少會給程式加鎖(除了庫存這些對數量準確性要求極高的情況下)
一般也就聽過常說的樂觀鎖和悲觀鎖,瞭解過基本的含義之後就沒了~~~
定心丸:即使我們不會這些鎖知識,我們的程式在一般情況下還是可以跑得好好的。因為這些鎖資料庫隱式幫我們加了:
對於UPDATE、DELETE、INSERT語句,InnoDB會自動給涉及資料集加排他鎖(X);
MyISAM在執行查詢語句SELECT前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要使用者干預;
只會在某些特定的場景下才需要手動加鎖,學習資料庫鎖知識就是為了:
能讓我們在特定的場景下派得上用場
更好把控自己寫的程式
在跟別人聊資料庫技術的時候可以搭上幾句話
構建自己的知識庫體系!在面試的時候不虛
2、表鎖簡單介紹
首先,從鎖的粒度,我們可以分成兩大類:
表鎖開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突概率高,併發度最低;
行鎖開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖衝突的概率低,併發度高;
不同的儲存引擎支援的鎖粒度是不一樣的:
InnoDB行鎖和表鎖都支援!
MyISAM只支援表鎖!
InnoDB只有通過索引條件檢索資料才使用行級鎖,否則,InnoDB將使用表鎖
也就是說,InnoDB的行鎖是基於索引的!
表鎖下又分為兩種模式:
表讀鎖(Table Read Lock)
表寫鎖(Table Write Lock)
從下圖可以清晰看到,在表讀鎖和表寫鎖的環境下:讀讀不阻塞,讀寫阻塞,寫寫阻塞!
讀讀不阻塞:當前使用者在讀資料,其他的使用者也在讀資料,不會加鎖
讀寫阻塞:當前使用者在讀資料,其他的使用者不能修改當前使用者讀的資料,會加鎖!
寫寫阻塞:當前使用者在修改資料,其他的使用者不能修改當前使用者正在修改的資料,會加鎖!

從上面已經看到了:讀鎖和寫鎖是互斥的,讀寫操作是序列。
如果某個程序想要獲取讀鎖,同時另外一個程序想要獲取寫鎖。在mysql裡邊,寫鎖是優先於讀鎖的!
寫鎖和讀鎖優先順序的問題是可以通過引數調節的:max_write_lock_count和low-priority-updates
值得注意的是:

MyISAM可以支援查詢和插入操作的併發進行。可以通過系統變數concurrent_insert來指定哪種模式,在MyISAM中它預設是:如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個程序讀表的同時,另一個程序從表尾插入記錄。
但是InnoDB儲存引擎是不支援的!
3、樂觀鎖和悲觀鎖
無論是Read committed還是Repeatable read隔離級別,都是為了解決讀寫衝突的問題。
單純在Repeatable read隔離級別下我們來考慮一個問題:

此時,使用者李四的操作就丟失掉了:
丟失更新:一個事務的更新覆蓋了其它事務的更新結果。
(ps:暫時沒有想到比較好的例子來說明更新丟失的問題,雖然上面的例子也是更新丟失,但一定程度上是可接受的..不知道有沒有人能想到不可接受的更新丟失例子呢...)
解決的方法:
使用Serializable隔離級別,事務是序列執行的!
樂觀鎖
悲觀鎖
樂觀鎖是一種思想,具體實現是,表中有一個版本欄位,第一次讀的時候,獲取到這個欄位。處理完業務邏輯開始更新的時候,需要再次檢視該欄位的值是否和第一次的一樣。如果一樣更新,反之拒絕。之所以叫樂觀,因為這個模式沒有從資料庫加鎖,等到更新的時候再判斷是否可以更新。
悲觀鎖是資料庫層面加鎖,都會阻塞去等待鎖。
3.1、悲觀鎖
所以,按照上面的例子。我們使用悲觀鎖的話其實很簡單(手動加行鎖就行了):
select * from xxxx for update
在select 語句後邊加了 for update相當於加了排它鎖(寫鎖),加了寫鎖以後,其他的事務就不能對它修改了!需要等待當前事務修改完之後才可以修改.
也就是說,如果張三使用select ... for update,李四就無法對該條記錄修改了~
3.2、樂觀鎖
樂觀鎖不是資料庫層面上的鎖,是需要自己手動去加的鎖。一般我們新增一個版本欄位來實現:
具體過程是這樣的:
張三select * from table --->會查詢出記錄出來,同時會有一個version欄位

李四select * from table --->會查詢出記錄出來,同時會有一個version欄位

李四對這條記錄做修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version},判斷之前查詢到的version與現在的資料的version進行比較,同時會更新version欄位
此時資料庫記錄如下:

張三也對這條記錄修改:update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version},但失敗了!因為當前資料庫中的版本跟查詢出來的版本不一致!

4、間隙鎖GAP
當我們用範圍條件檢索資料而不是相等條件檢索資料,並請求共享或排他鎖時,InnoDB會給符合範圍條件的已有資料記錄的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫做“間隙(GAP)”。InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖。
值得注意的是:間隙鎖只會在Repeatable read隔離級別下使用~
例子:假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101
Select * from emp where empid > 100 for update;
上面是一個範圍查詢,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的“間隙”加鎖。
InnoDB使用間隙鎖的目的有兩個:
為了防止幻讀(上面也說了,Repeatable read隔離級別下再通過GAP鎖即可避免了幻讀)
滿足恢復和複製的需要MySQL的恢復機制要求:在一個事務未提交前,其他併發事務不能插入滿足其鎖定條件的任何記錄,也就是不允許出現幻讀
5、死鎖
併發的問題就少不了死鎖,在MySQL中同樣會存在死鎖的問題。
但一般來說MySQL通過回滾幫我們解決了不少死鎖的問題了,但死鎖是無法完全避免的,可以通過以下的經驗參考,來儘可能少遇到死鎖:
1)以固定的順序訪問表和行。比如對兩個job批量更新的情形,簡單方法是對id列表先排序,後執行,這樣就避免了交叉等待鎖的情形;將兩個事務的sql順序調整為一致,也能避免死鎖。
2)大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。
3)在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖概率。
4)降低隔離級別。如果業務允許,將隔離級別調低也是較好的選擇,比如將隔離級別從RR調整為RC,可以避免掉很多因為gap鎖造成的死鎖。
5)為表新增合理的索引。可以看到如果不走索引將會為表的每一行記錄新增上鎖,死鎖的概率大大增大。
6、鎖總結
上面說了一大堆關於MySQL資料庫鎖的東西,現在來簡單總結一下。
表鎖其實我們程式設計師是很少關心它的:
在MyISAM儲存引擎中,當執行SQL語句的時候是自動加的。
在InnoDB儲存引擎中,如果沒有使用索引,表鎖也是自動加的。
現在我們大多數使用MySQL都是使用InnoDB,InnoDB支援行鎖:
共享鎖--讀鎖--S鎖
排它鎖--寫鎖--X鎖
在預設的情況下,select是不加任何行鎖的~事務可以通過以下語句顯示給記錄集加共享鎖或排他鎖。
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
InnoDB基於行鎖還實現了MVCC多版本併發控制,MVCC在隔離級別下的Read committed和Repeatable read下工作。MVCC能夠實現讀寫不阻塞!
InnoDB實現的Repeatable read隔離級別配合GAP間隙鎖已經避免了幻讀!
樂觀鎖其實是一種思想,正如其名:認為不會鎖定的情況下去更新資料,如果發現不對勁,才不更新(回滾)。在資料庫中往往新增一個version欄位來實現。
悲觀鎖用的就是資料庫的行鎖,認為資料庫會發生併發衝突,直接上來就把資料鎖住,其他事務不能修改,直至提交了當前事務
三、總結
本文主要介紹了資料庫中的兩個比較重要的知識點:索引和鎖。他倆可以說息息相關的,鎖會涉及到很多關於索引的知識~
歡迎工作一到五年的Java工程師朋友們加入Java填坑之路:860113481
群內提供免費的Java架構學習資料(裡面有高可用、高併發、高效能及分散式、Jvm效能調優、Spring原始碼,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!