Java編程——資料庫兩大神器:索引和鎖

前言

只有光頭才能變強

索引和鎖在數據庫中可以說是非常重要的知識點了,在面試中也會經常會被問到的。

一、索引

在之前,我對索引有以下的認知:

  • 索引可以加快數據庫的檢索速度
  • 經常進行INSERT/UPDATE/DELETE操作就不要建立索引了,換言之:索引會降低插入、刪除、修改等維護任務的速度。
  • 索引需要佔物理和數據空間
  • 瞭解過索引的最左匹配原則
  • 知道索引的分類:聚集索引和非聚集索引
  • Mysql支持Hash索引和B+樹索引兩種

看起來好像啥都知道,但面試讓你說的時候可能就GG了:

  • 使用索引為什麼可以加快數據庫的檢索速度啊?
  • 為什麼說索引會降低插入、刪除、修改等維護任務的速度。
  • 索引的最左匹配原則指的是什麼?
  • Hash索引和B+樹索引有什麼區別?主流的使用哪一個比較多?InnoDB存儲都支持嗎?
  • 聚集索引和非聚集索引有什麼區別?
  • ........

1.1聊聊索引的基礎知識

首先Mysql的基本存儲結構是(記錄都存在頁裡邊):

Java編程——數據庫兩大神器:索引和鎖

Java編程——數據庫兩大神器:索引和鎖

  • 各個數據頁可以組成一個雙向鏈表
  • 每個數據頁中的記錄又可以組成一個單向鏈表
  • 每個數據頁都會為存儲在它裡邊兒的記錄生成一個頁目錄,在通過主鍵查找某條記錄的時候可以在頁目錄中使用二分法快速定位到對應的槽,然後再遍歷該槽對應分組中的記錄即可快速找到指定的記錄
  • 其他列(非主鍵)作為搜索條件:只能從最小記錄開始依次遍歷單鏈表中的每條記錄

所以說,如果我們寫select * from user where username = 'Java3y'這樣沒有進行任何優化的sql語句,默認會這樣做:

  • 定位到記錄所在的頁
  • 需要遍歷雙向鏈表,找到所在的頁
  • 從所在的頁內中查找相應的記錄
  • 由於不是根據主鍵查詢,只能遍歷所在頁的單鏈表了

很明顯,在數據量很大的情況下這樣查找會很慢

1.2索引提高檢索速度

索引做了些什麼可以讓我們查詢加快速度呢?

其實就是將無序的數據變成有序(相對)

Java編程——數據庫兩大神器:索引和鎖

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

Java編程——數據庫兩大神器:索引和鎖

很明顯的是:沒有用索引我們是需要遍歷雙向鏈表來定位對應的頁,現在通過**“目錄”**就可以很快地定位到對應的頁上了!

其實底層結構就是B+樹,B+樹作為樹的一種實現,能夠讓我們

很快地查找出對應的記錄。

  • Mysql索引

1.3索引降低增刪改的速度

B+樹是平衡樹的一種。

平衡樹:它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹。

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

Java編程——數據庫兩大神器:索引和鎖

B+樹是平衡樹的一種,是不會退化成鏈表的,樹的高度都是相對比較低的(基本符合矮矮胖胖(均衡)的結構)【這樣一來我們檢索的時間複雜度就是O(logn)】!從上一節的圖我們也可以看見,建立索引實際上就是建立一顆B+樹。

  • B+樹是一顆平衡樹,如果我們對這顆樹增刪改的話,那肯定會破壞它的原有結構
  • 要維持平衡樹,就必須做額外的工作。正因為這些額外的工作開銷,導致索引會降低增刪改的速度

B+樹刪除和修改具體可參考:

  • www.cnblogs.com/wade-luffy/…

1.4哈希索引

除了B+樹之外,還有一種常見的是哈希索引。

哈希索引就是採用一定的哈希算法,把鍵值換算成新的哈希值,檢索時不需要類似B+樹那樣從根節點到葉子節點逐級查找,只需一次哈希算法即可立刻定位到相應的位置,速度非常快

  • 本質上就是把鍵值換算成新的哈希值,根據這個哈希值來定位
Java編程——數據庫兩大神器:索引和鎖

看起來哈希索引很牛逼啊,但其實哈希索引有好幾個侷限(根據他本質的原理可得):

  • 哈希索引也沒辦法利用索引完成排序
  • 不支持最左匹配原則
  • 在有大量重複鍵值情況下,哈希索引的效率也是極低的---->哈希碰撞問題。
  • 不支持範圍查詢
  • www.cnblogs.com/zengkefu/p/…---hash索引和b+tree索引

1.5InnoDB支持哈希索引嗎?

主流的還是使用B+樹索引比較多,對於哈希索引,InnoDB是自適應哈希索引的(hash索引的創建由InnoDB存儲引擎引擎自動優化創建,我們干預不了)!

Java編程——數據庫兩大神器:索引和鎖

  • blog.csdn.net/doctor_who2…

1.6聚集和非聚集索引

簡單概括:

  • 聚集索引就是以主鍵創建的索引
  • 非聚集索引就是以
    非主鍵創建的索引

區別:

  • 聚集索引在葉子節點存儲的是表中的數據
  • 非聚集索引在葉子節點存儲的是主鍵和索引列
  • 使用非聚集索引查詢出數據時,拿到葉子上的主鍵再去查到想要查找的數據。(拿到主鍵再查找這個過程叫做回表)

非聚集索引也叫做二級索引,不用糾結那麼多名詞,將其等價就行了~

非聚集索引在建立的時候也未必是單列的,可以多個列來創建索引。

  • 此時就涉及到了哪個列會走索引,哪個列不走索引的問題了(最左匹配原則-->後面有說)
  • 創建多個單列(非聚集)索引的時候,會生成多個索引樹(所以過多創建索引會佔用磁盤空間)
Java編程——數據庫兩大神器:索引和鎖

在創建多列索引中也涉及到了一種特殊的索引-->覆蓋索引

  • 我們前面知道了,如果不是聚集索引,葉子節點存儲的是主鍵+列值
  • 最終還是要“回表”,也就是要通過主鍵查找一次。這樣就會比較慢
  • 覆蓋索引就是把要查詢出的列和索引是對應的,不做回表操作!

比如說:

  • 現在我創建了索引(username,age),在查詢數據的時候:select username , age from user where username = 'Java3y' and age = 20。
  • 很明顯地知道,我們上邊的查詢是走索引的,並且,要查詢出的列在葉子節點都存在!所以,就不用回表了~
  • 所以,能使用覆蓋索引就儘量使用吧~

1.7索引最左匹配原則

最左匹配原則

  • 索引可以簡單如一個列(a),也可以複雜如多個列(a, b, c, d),即
    聯合索引
  • 如果是聯合索引,那麼key也由多個列組成,同時,索引只能用於查找key是否存在(相等),遇到範圍查詢(>、不能進一步匹配了,後續退化為線性查找。
  • 因此,列的排列順序決定了可命中索引的列數

例子:

  • 如有索引(a, b, c, d),查詢條件a = 1 and b = 2 and c > 3 and d = 4,則會在每個節點依次命中a、b、c,無法命中d。(很簡單:索引命中只能是相等的情況,不能是範圍匹配)

1.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等順序都是可以的,MySQL會自動優化為a = 1 and b = 2 and c > 3 and d < 4,依次命中a、b、c。

1.9索引總結

索引在數據庫中是一個非常重要的知識點!上面談的其實就是索引最基本的東西,要創建出好的索引要顧及到很多的方面:

  • 1,最左前綴匹配原則。這是非常重要、非常重要、非常重要(重要的事情說三遍)的原則,MySQL會一直向右匹配直到遇到範圍查詢(>,
  • 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只能使用一個索引,會從多個單列索引中選擇一個限制最為嚴格的索引。
  • zhuanlan.zhihu.com/p/23624390--簡單理解索引
  • blog.csdn.net/mysteryhaoh…-- MySQL學習之——索引(普通索引、唯一索引、全文索引、索引匹配原則、索引命中等)
  • monkeysayhi.github.io/2018/03/06/…---淺談MySQL的B樹索引與索引優化

二、鎖

Java編程——數據庫兩大神器:索引和鎖

在mysql中的鎖看起來是很複雜的,因為有一大堆的東西和名詞:排它鎖,共享鎖,表鎖,頁鎖,間隙鎖,意向排它鎖,意向共享鎖,行鎖,讀鎖,寫鎖,樂觀鎖,悲觀鎖,死鎖。這些名詞有的博客又直接寫鎖的英文的簡寫--->X鎖,S鎖,IS鎖,IX鎖,MMVC...

鎖的相關知識又跟存儲引擎,索引,事務的隔離級別都是關聯的....

這就給初學數據庫鎖的人帶來不少的麻煩~~~於是我下面就簡單整理一下數據庫鎖的知識點,希望大家看完會有所幫助。

2.1為什麼需要學習數據庫鎖知識

不少人在開發的時候,應該很少會注意到這些鎖的問題,也很少會給程序加鎖(除了庫存這些對數量準確性要求極高的情況下)

一般也就聽過常說的樂觀鎖和悲觀鎖,瞭解過基本的含義之後就沒了~~~

定心丸:即使我們不會這些鎖知識,我們的程序在一般情況下還是可以跑得好好的。因為這些鎖數據庫隱式幫我們加了

  • 對於UPDATE、DELETE、INSERT語句,InnoDB自動給涉及數據集加排他鎖(X)
  • MyISAM在執行查詢語句SELECT前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要用戶干預

只會在某些特定的場景下才需要手動加鎖,學習數據庫鎖知識就是為了:

  • 能讓我們在特定的場景下派得上用場
  • 更好把控自己寫的程序
  • 在跟別人聊數據庫技術的時候可以搭上幾句話
  • 構建自己的知識庫體系!在面試的時候不虛

2.2表鎖簡單介紹

首先,從鎖的粒度,我們可以分成兩大類:

  • 表鎖開銷小,加鎖快;不會出現死鎖;鎖定力度大,發生鎖衝突概率高,併發度最低
  • 行鎖開銷大,加鎖慢;會出現死鎖;鎖定粒度小,發生鎖衝突的概率低,併發度高

不同的存儲引擎支持的鎖粒度是不一樣的:

  • InnoDB行鎖和表鎖都支持
  • MyISAM只支持表鎖

InnoDB只有通過索引條件檢索數據才使用行級鎖,否則,InnoDB將使用表鎖

  • 也就是說,InnoDB的行鎖是基於索引的

表鎖下又分為兩種模式

  • 表讀鎖(Table Read Lock)
  • 表寫鎖(Table Write Lock)
  • 從下圖可以清晰看到,在表讀鎖和表寫鎖的環境下:讀讀不阻塞,讀寫阻塞,寫寫阻塞
  • 讀讀不阻塞:當前用戶在讀數據,其他的用戶也在讀數據,不會加鎖
  • 讀寫阻塞:當前用戶在讀數據,其他的用戶不能修改當前用戶讀的數據,會加鎖!
  • 寫寫阻塞:當前用戶在修改數據,其他的用戶不能修改當前用戶正在修改的數據,會加鎖!
Java編程——數據庫兩大神器:索引和鎖

從上面已經看到了:讀鎖和寫鎖是互斥的,讀寫操作是串行

  • 如果某個進程想要獲取讀鎖,同時另外一個進程想要獲取寫鎖。在mysql裡邊,寫鎖是優先於讀鎖的
  • 寫鎖和讀鎖優先級的問題是可以通過參數調節的:max_write_lock_count和low-priority-updates

值得注意的是:

The LOCAL modifier enables nonconflicting INSERT statements (concurrent inserts) by other sessions to execute while the lock is held. (See Section 8.11.3, “Concurrent Inserts”.) However, READ LOCAL cannot be used if you are going to manipulate the database using processes external to the server while you hold the lock. For InnoDB tables, READ LOCAL is the same as READ

  • MyISAM可以支持查詢和插入操作的併發進行。可以通過系統變量concurrent_insert來指定哪種模式,在MyISAM中它默認是:如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),MyISAM允許在一個進程讀表的同時,另一個進程從表尾插入記錄。
  • 但是InnoDB存儲引擎是不支持的
  • dev.mysql.com/doc/refman/…--官方手冊
  • ourmysql.com/archives/56…---幾個參數說明

2.3樂觀鎖和悲觀鎖

無論是Read committed還是Repeatable read隔離級別,都是為了解決讀寫衝突的問題。

單純在Repeatable read隔離級別下我們來考慮一個問題:

Java編程——數據庫兩大神器:索引和鎖

此時,用戶李四的操作就丟失掉了:

  • 丟失更新:一個事務的更新覆蓋了其它事務的更新結果

(ps:暫時沒有想到比較好的例子來說明更新丟失的問題,雖然上面的例子也是更新丟失,但一定程度上是可接受的..不知道有沒有人能想到不可接受的更新丟失例子呢...)

解決的方法:

  • 使用Serializable隔離級別,事務是串行執行的!
  • 樂觀鎖
  • 悲觀鎖
  • 樂觀鎖是一種思想,具體實現是,表中有一個版本字段,第一次讀的時候,獲取到這個字段。處理完業務邏輯開始更新的時候,需要再次查看該字段的值是否和第一次的一樣。如果一樣更新,反之拒絕。之所以叫樂觀,因為這個模式沒有從數據庫加鎖,等到更新的時候再判斷是否可以更新。
  • 悲觀鎖是數據庫層面加鎖,都會阻塞去等待鎖。

2.3.1悲觀鎖

所以,按照上面的例子。我們使用悲觀鎖的話其實很簡單(手動加行鎖就行了):

  • select * from xxxx for update

在select 語句後邊加了 for update相當於加了排它鎖(寫鎖),加了寫鎖以後,其他的事務就不能對它修改了!需要等待當前事務修改完之後才可以修改.

  • 也就是說,如果張三使用select ... for update,李四就無法對該條記錄修改了~

2.3.2樂觀鎖

樂觀鎖不是數據庫層面上的鎖,是需要自己手動去加的鎖。一般我們添加一個版本字段來實現:

具體過程是這樣的:

張三select * from table --->會查詢出記錄出來,同時會有一個version字段

Java編程——數據庫兩大神器:索引和鎖

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

Java編程——數據庫兩大神器:索引和鎖

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

此時數據庫記錄如下:

Java編程——數據庫兩大神器:索引和鎖

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

Java編程——數據庫兩大神器:索引和鎖

  • zhuanlan.zhihu.com/p/31537871---什麼是悲觀鎖和樂觀鎖
  • www.zhihu.com/question/27…---樂觀鎖和 MVCC 的區別?

2.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的恢復機制要求:在一個事務未提交前,其他併發事務不能插入滿足其鎖定條件的任何記錄,也就是不允許出現幻讀

2.5死鎖

併發的問題就少不了死鎖,在MySQL中同樣會存在死鎖的問題。

但一般來說MySQL通過回滾幫我們解決了不少死鎖的問題了,但死鎖是無法完全避免的,可以通過以下的經驗參考,來儘可能少遇到死鎖:

  • 1)以固定的順序訪問表和行。比如對兩個job批量更新的情形,簡單方法是對id列表先排序,後執行,這樣就避免了交叉等待鎖的情形;將兩個事務的sql順序調整為一致,也能避免死鎖。
  • 2)大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。
  • 3)在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖概率。
  • 4)降低隔離級別。如果業務允許,將隔離級別調低也是較好的選擇,比如將隔離級別從RR調整為RC,可以避免掉很多因為gap鎖造成的死鎖。
  • 5)為表添加合理的索引。可以看到如果不走索引將會為表的每一行記錄添加上鎖,死鎖的概率大大增大。
  • hedengcheng.com/?p=771#_Toc…
  • www.cnblogs.com/LBSer/p/518…

2.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字段來實現。
  • 悲觀鎖用的就是數據庫的行鎖,認為數據庫會發生併發衝突,直接上來就把數據鎖住,其他事務不能修改,直至提交了當前事務
  • zhuanlan.zhihu.com/p/29150809--Mysql鎖總結
  • blog.csdn.net/mysteryhaoh…--MySQL學習之——鎖(行鎖、表鎖、頁鎖、樂觀鎖、悲觀鎖等)
  • segmentfault.com/a/119000001…--MySQL InnoDB引擎鎖的總結

三、總結

本文主要介紹了數據庫中的兩個比較重要的知識點:索引和鎖。他倆可以說息息相關的,鎖會涉及到很多關於索引的知識~

Java編程——數據庫兩大神器:索引和鎖


分享到:


相關文章: