前言
本文摘自數據庫兩大神器【索引和鎖】
聲明:如果沒有說明具體的數據庫和存儲引擎,默認指的是MySQL中的InnoDB存儲引擎
索引
在之前,我對索引有以下的認知:
- 索引可以加快數據庫的檢索速度
- 表經常進行INSERT/UPDATE/DELETE操作就不要建立索引了,換言之:索引會降低插入、刪除、修改等維護任務的速度。
- 索引需要占物理和數據空間。
- 了解過索引的最左匹配原則
- 知道索引的分類:聚集索引和非聚集索引
- Mysql支持Hash索引和B+樹索引兩種
看起來好像啥都知道,but,但面試讓你說的時候可能就GG了:
- 使用索引為什么可以加快數據庫的檢索速度啊?
- 為什么說索引會降低插入、刪除、修改等維護任務的速度?
- 索引的最左匹配原則指的是什么?
- Hash索引和B+樹索引有什么區別?主流的使用哪一個比較多?InnoDB存儲都支持嗎?
- 聚集索引和非聚集索引有什么區別?
- ........
索引的基礎知識
首先Mysql的基本存儲結構是頁(記錄都存在頁里邊)
可以得出以下結論:
1. 各個數據頁可以組成一個雙向鏈表
2. 而每個數據頁中的記錄又可以組成一個單向鏈表
3. 每個數據頁都會為記錄生成一個頁目錄,通過主鍵查找某條記錄時可以在頁目錄中使用二分法快速定位到對應的槽,然后遍歷該槽對應分組中的記錄找到指定的記錄
4. 以其他列(非主鍵)作為搜索條件:只能從最小記錄開始依次遍歷單鏈表中的每條記錄。
所以說,如果我們寫select * from user where username = 'Java3y' 這樣沒有進行任何優化的sql語句,默認會這樣做:
- 遍歷雙向鏈表,定位到所在的頁
- 從所在的頁內中查找相應的記錄,因為不是根據主鍵查詢,因而只能遍歷
很明顯,在數據量很大的情況下這樣查找會很慢!
索引提高檢索速度
通過上面可知,加快查找速度勢在必行,那么,索引做了些什么可以讓我們查詢加快速度呢?其實就是將無序的數據變成有序(相對)
例如:要找到id為8的記錄簡要步驟如下
很明顯的是:
- 沒有用索引我們是需要遍歷雙向鏈表來定位對應的頁,現在通過**“目錄”**就可以很快地定位到對應的頁上了!
- 其實底層結構就是B+樹,B+樹作為樹的一種實現,能夠讓我們很快地查找出對應的記錄。
【參考資料】:Mysql索引
索引降低增刪改的速度
平衡樹:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。
B+樹是平衡樹的一種,是不會退化成鏈表的,樹的高度都是相對比較低的(基本符合均衡的結構)【這樣一來我們檢索的時間復雜度就是O(logn)】!從上一節的圖我們也可以
看見,建立索引實際上就是建立一顆B+樹。
- B+樹是一顆平衡樹,如果我們對這顆樹增刪改的話,那肯定會破壞它的原有結構。
- 要維持平衡樹,就必須做額外的工作。正因為這些額外的工作開銷,導致索引會降低增刪改的速度
【參考資料】:B+樹刪除和修改
哈希索引
除了B+樹之外,還有一種常見的是哈希索引。哈希索引就是采用一定的哈希算法,把鍵值換算成新的哈希值,檢索時不需要類似B+樹那樣從根節點到葉子節點逐級查找,只
需一次哈希算法即可立刻定位到相應的位置,速度非常快。但是hash索引有好幾個局限:
- 哈希索引也沒辦法利用索引完成排序
- 不支持最左匹配原則
- 在有大量重復鍵值情況下,哈希索引的效率也是極低的---->哈希碰撞問題。
- 不支持范圍查詢
【參考資料】:B+樹索引和哈希索引的區別
InnoDB支持哈希索引嘛?
主流的還是使用B+樹索引比較多,對於哈希索引,InnoDB是自適應哈希索引的(hash索引的創建由InnoDB存儲引擎引擎自動優化創建,用戶干預不了)!
聚集和非聚集索引
- 聚集索引就是以主鍵創建的索引
- 非聚集索引就是以非主鍵創建的索引
聚集索引有2個特點:
1. 使用記錄主鍵值的大小進行記錄和頁的排序,這包括三個方面的含義
- 頁內的記錄是按照主鍵的大小順序排成一個單向鏈表。
- 各個存放用戶記錄的頁也是根據頁中記錄的主鍵大小順序排成一個雙向鏈表。
- 各個存放目錄項的頁也是根據頁中記錄的主鍵大小順序排成一個雙向鏈表。
2. B+樹的葉子節點存儲的是完整的用戶記錄,就是指這個記錄中存儲了所有列的值。
我們把具有這兩種特性的B+
樹稱為聚集索引,所有完整的用戶記錄都存放在這個聚集索引的葉子節點處。這種聚集索引並不需要我們在MySQL語句中顯式的去創建,
InnoDB存儲引擎會自動的為我們創建聚簇索引。另外有趣的一點是,在InnoDB存儲引擎中,聚集索引就是數據的存儲方式(所有的用戶記錄都存儲在了葉子節點),也
就是所謂的索引即數據。
非聚集索引也叫做二級索引
上邊介紹的聚簇索引只能在搜索條件是主鍵值時才能發揮作用,因為B+樹中的數據都是按照主鍵進行排序的。那如果我們想以別的列作為搜索條件該怎么辦呢?難道
只能從頭到尾沿着鏈表依次遍歷記錄么?我們可以多建幾棵B+樹,不同的B+樹中的數據采用不同的排序規則。比方說我們用c2列的大小作為數據頁、頁中記錄的排序規則,
再建一棵B+樹
- 其中,B+樹的葉子節點存儲的並不是完整的用戶記錄,而只是c2列+主鍵這兩個列的值。
- 索引項記錄中不再是主鍵+頁號的搭配,而變成了c2列+頁號的搭配。
過程如下:首先根據c2列的值,定位到非聚集索引中的某一個葉子節點,然后根據該葉子節點中的主鍵去聚簇索引中再查找一遍完整的用戶記錄,稱之為回表
索引優化
在執行SQL時,MySQL只能使用一個索引,會從多個單列索引中選擇一個限制最為嚴格的索引,根據B+樹的性質,很容易理解各種常見的MySQL索引優化思路
1. 優先使用自增key作為主鍵
假設用4B的自增key作為索引,則m可達到512,層高僅有3。參考這里,使用自增的key有兩個好處:
1. 自增key一般為int等整數型,key比較緊湊,這樣m可以非常大,而且索引占用空間小。最極端的例子,如果使用50B的varchar(包括長度),那么m = 4 * 1024 /
54m = 75.85 ~= 76,深度最大log(76/2)(10^7) = 4.43 ~= 5 ,再加上cache缺失、字符串比較的成本,時間成本增加較大。同時,key由4B增長到50B,整棵索引樹
的空間占用增長也是極為恐怖的(如果二級索引使用主鍵定位數據行,則空間增長更加嚴重)
2. 自增的性質使得新數據行的插入請求必然落到索引樹的最右側,發生節點分裂的頻率較低,理想情況下,索引樹可以達到“滿”的狀態。索引樹滿,一方面層高更低,一
方面刪除節點時發生節點合並的頻率也較低
優化案例:
曾使用varchar(100)的列做過主鍵,存儲containerId,過了3、4天100G的數據庫就滿了,之后增加了自增列作為主鍵,containerId作為unique的二級索引,時間、
空間優化效果相當顯著。
2. 最左前綴匹配
- 索引可以簡單如一個列(a),也可以復雜如多個列(a, b, c, d),即聯合索引。如果是聯合索引,那么key也由多個列組成,同時,索引只能用於查找key是否存在(相等)
- 遇到范圍查詢(>、<、between、like左匹配)等就不能進一步匹配了,后續退化為線性查找,也就是此時不會通過索引來查詢
- =、in自動優化順序,mysql會自動優化這些條件的順序
3. 索引列不能參與計算
有索引列參與計算的查詢條件對索引不友好(甚至無法使用索引)。原因很簡單,如何在節點中查找到對應key?如果線性掃描,則每次都需要重新計算,成本太高;如果二分
查找,則需要確定大小關系
4. 能擴展就不要新建索引
如果已有索引(a),想建立索引(a, b),盡量選擇修改索引(a)為索引(a, b)。MySQL可以直接在索引a的B+樹上,經過分裂、合並等修改為索引(a, b)
5. 不需要建立前綴有包含關系的索引
如果已有索引(a, b),則不需要再建立索引(a),但是如果有必要,則仍然需考慮建立索引(b)。
6. 選擇區分度高的列作索引
這很容易理解。如,用性別作索引,那么索引僅能將1000w行數據划分為兩部分(如500w男,500w女),索引幾乎無效。區分度的公式是count(distinct <col>) /
count(*),表示字段不重復的比例,比例越大區分度越好。唯一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前的區分度趨近於0
鎖
鎖的相關知識又跟存儲引擎,索引,事務的隔離級別都是關聯的
數據庫鎖知識
不少人在開發的時候,應該很少會注意到這些鎖的問題,也很少會給程序加鎖(除了庫存這些對數量准確性要求極高的情況下),即使我們不會這些鎖知識,我們的程序在一
般情況下還是可以跑得好好的。因為這些鎖數據庫隱式幫我們加了,只會在某些特定的場景下才需要手動加鎖。
- 對於UPDATE、DELETE、INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X)
- MyISAM在執行查詢語句SELECT前,會自動給涉及的所有表加讀鎖,在執行增、刪、改操作前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預
表鎖
首先要明確的是,用戶很少手動加表鎖
首先,從鎖的粒度,我們可以分成兩大類:
- 表鎖
- 開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖沖突概率高,並發度最低
- 行鎖
- 開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖沖突的概率低,並發度高
不同的存儲引擎支持的鎖粒度是不一樣的:InnoDB行鎖和表鎖都支持、MyISAM只支持表鎖!InnoDB只有通過索引條件檢索數據才使用行級鎖,否則,InnoDB使用表
鎖也就是說,InnoDB的行鎖是基於索引的!
表鎖下又分為兩種模式:
- 表讀鎖(Table Read Lock)
- 表寫鎖(Table Write Lock)
- 從下圖可以清晰看到,在表讀鎖和表寫鎖的環境下:讀讀不阻塞,讀寫阻塞,寫寫阻塞!
- 讀讀不阻塞:當前用戶在讀數據,其他的用戶也在讀數據,不會加鎖
- 讀寫阻塞:當前用戶在讀數據,其他的用戶不能修改當前用戶讀的數據,會加鎖!
- 寫寫阻塞:當前用戶在修改數據,其他的用戶不能修改當前用戶正在修改的數據,會加鎖!
從上面已經看到了:讀鎖和寫鎖是互斥的,讀寫操作是串行。
- 如果某個進程想要獲取讀鎖,同時另外一個進程想要獲取寫鎖。在mysql中,寫鎖是優先於讀鎖的!
- 寫鎖和讀鎖優先級的問題是可以通過參數調節的:max_write_lock_count和low-priority-updates
行鎖
InnoDB和MyISAM有兩個本質的區別:InnoDB支持行鎖、InnoDB支持事務
InnoDB實現了以下兩種類型的行鎖:
- 共享鎖(S鎖、讀鎖):允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。即多個客戶可以同時讀取同一個資源,但不允許其他客戶修改。
- 排他鎖(X鎖、寫鎖):允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的讀鎖和寫鎖。寫鎖是排他的,寫鎖會阻塞其他的寫鎖和讀鎖。
另外,為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖:
- 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
- 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
- 意向鎖也是數據庫隱式幫我們做了,不需要程序員關心!
MVCC
MVCC(Multi-Version Concurrency Control)多版本並發控制,可以簡單地認為:MVCC就是行級鎖的一個變種(升級版)。在表鎖中我們讀寫是阻塞的,基於提升並發性能
的考慮,MVCC一般讀寫是不阻塞的(很多情況下避免了加鎖的操作)。可以簡單的理解為:對數據庫的任何修改的提交都不會直接覆蓋之前的數據,而是產生一個新的版本與
老版本共存,使得讀取時可以完全不加鎖。
事務的隔離級別
事務的隔離級別就是通過鎖的機制來實現,鎖的應用最終導致不同事務的隔離級別,只不過隱藏了加鎖細節,事務的隔離級別有4種:
- Read uncommitted:會出現臟讀,不可重復讀,幻讀
- Read committed:會出現不可重復讀,幻讀
- Repeatable read:會出現幻讀(Mysql默認的隔離級別,但是Repeatable read配合gap鎖不會出現幻讀!)
- Serializable:串行,避免以上的情況
Read uncommitted:出現的現象--->臟讀:一個事務讀取到另外一個事務未提交的數據,例子:A向B轉賬,A執行了轉賬語句,但A還沒有提交事務,B讀取數據,發現
自己賬戶錢變多了!B跟A說,我已經收到錢了。A回滾事務【rollback】,等B再查看賬戶的錢時,發現錢並沒有多...
出現臟讀的本質就是因為操作(修改)完該數據就立馬釋放掉鎖,導致讀的數據就變成了無用的或者是錯誤的數據
Read committed:出現的現象--->不可重復讀:一個事務讀取到另外一個事務已經提交的數據,也就是說一個事務可以看到其他事務所做的修改,例如:A查詢數據庫得到
數據,B去修改數據庫的數據,導致A多次查詢數據庫的結果都不一樣【危害:A每次查詢的結果都是受B的影響的,那么A查詢出來的信息就沒有意思了】
Repeatable read:避免不可重復讀是事務級別的快照!每次讀取的都是當前事務的版本,即使被修改了,也只會讀取當前事務版本的數據
如果還是不太清楚,我們來看看InnoDB的MVCC是怎么樣的吧《高性能MySQL》
至於虛讀(幻讀):是指在一個事務內讀取到了別的事務插入的數據,導致前后讀取不一致。和不可重復讀類似,但虛讀(幻讀)會讀到其他事務的插入的數據,導致前后讀取不
一致,幻讀的重點在於新增或者刪除 (數據條數變化),不可重復讀的重點是修改,幻讀和不可重復的區別?
樂觀鎖和悲觀鎖
無論是Read committed還是Repeatable read隔離級別,都是為了解決讀寫沖突的問題,現在考慮一個問題:有一張數據庫表USER,只有id、name字段,現在有2個請求
同時候操作表A,過程如下:(模擬更新丟失,雖然不是很恰當)
1. 操作1查詢出name="zhangsan"
2. 操作2也查詢出name="zhangsan"
3. 操作1把name字段數據修改成lisi並提交
4. 操作2把name字段數據修改為wangwu並提交
那么操作1的更新丟失啦,即一個事務的更新覆蓋了其它事務的更新結果,解決上述更新丟失的方式有如下3種:
- 使用Serializable隔離級別,事務是串行執行的!
- 樂觀鎖
- 悲觀鎖
悲觀鎖
我們使用悲觀鎖的話其實很簡單(手動加行鎖就行了):select * from xxxx for update,在select 語句后邊加了for update 相當於加了排它鎖(寫鎖),加了寫鎖以后,其
他事務就不能對它修改了!需要等待當前事務修改完之后才可以修改.也就是說,如果操作1使用select ... for update,操作2就無法對該條記錄修改了,即可避免更新丟失。
樂觀鎖
樂觀鎖不是數據庫層面上的鎖,需要用戶手動去加的鎖。一般我們在數據庫表中添加一個版本字段version來實現,例如操作1和操作2在更新User表的時,執行語句如下:
update A set Name=lisi,version=version+1 where ID=#{id} and version=#{version},此時即可避免更新丟失。
【參考資料】:什么是樂觀鎖和悲觀鎖
間隙鎖GAP
間隙鎖只會在Repeatable read隔離級別下使用
當我們用范圍條件檢索數據而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合范圍條件的已有數據記錄的索引項加鎖;對於鍵值在條件范圍內但並不存在
的記錄,叫做“間隙(GAP)”。InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖。例子:假如emp表中只有101條記錄,其empid的值分別是1,2,...,100,101
select * from emp where empid > 100 for update;
上面是一個范圍查詢,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大於101(這些記錄並不存在)的“間隙”加鎖
InnoDB使用間隙鎖的目的有2個:
- 為了防止幻讀(上面也說了,Repeatable read隔離級別下再通過GAP鎖即可避免了幻讀)
- 滿足恢復和復制的需要:MySQL的恢復機制要求在一個事務未提交前,其他並發事務不能插入滿足其鎖定條件的任何記錄,也就是不允許出現幻讀
死鎖
並發的問題就少不了死鎖,在MySQL中同樣會存在死鎖的問題
【參考資料】:MySQL死鎖問題分析
【參考資料】: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