1、相關名詞mysql
|--表級鎖(鎖定整個表)程序員
|--頁級鎖(鎖定一頁)sql
|--行級鎖(鎖定一行)數據庫
|--共享鎖(S鎖,MyISAM 叫作讀鎖)併發
|--排他鎖(X鎖,MyISAM 叫作寫鎖)性能
|--間隙鎖(NEXT-KEY鎖)spa
|--悲觀鎖(抽象性,不真實存在這個鎖).net
|--樂觀鎖(抽象性,不真實存在這個鎖)線程
2、InnoDB與MyISAM設計
Mysql 在5.5以前默認使用 MyISAM 存儲引擎,以後使用 InnoDB 。查看當前存儲引擎:
show variables like '%storage_engine%';
MyISAM 操做數據都是使用的表鎖,你更新一條記錄就要鎖整個表,致使性能較低,併發不高。固然同時它也不會存在死鎖問題。
而 InnoDB 與 MyISAM 的最大不一樣有兩點:一是 InnoDB 支持事務;二是 InnoDB 採用了行級鎖。也就是你須要修改哪行,就能夠只鎖定哪行。
在 Mysql 中,行級鎖並非直接鎖記錄,而是鎖索引。索引分爲主鍵索引和非主鍵索引兩種,若是一條sql 語句操做了主鍵索引,Mysql 就會鎖定這條主鍵索引;若是一條語句操做了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。
InnoDB 行鎖是經過給索引項加鎖實現的,若是沒有索引,InnoDB 會經過隱藏的聚簇索引來對記錄加鎖。也就是說:若是不經過索引條件檢索數據,那麼InnoDB將對錶中全部數據加鎖,實際效果跟表鎖同樣。由於沒有了索引,找到某一條記錄就得掃描全表,要掃描全表,就得鎖定表。
3、共享鎖與排他鎖
1.首先說明:數據庫的增刪改操做默認都會加排他鎖,而查詢不會加任何鎖。
mysql InnoDB引擎默認的修改數據語句,update,delete,insert都會自動給涉及到的數據加上排他鎖,select語句默認不會加任何鎖類型,若是加排他鎖可使用select ...for update語句,加共享鎖可使用select ... lock in share mode語句。因此加過排他鎖的數據行在其餘事務種是不能修改數據的,也不能經過for update和lock in share mode鎖的方式查詢數據,但能夠直接經過select ...from...查詢數據,由於普通查詢沒有任何鎖機制。
|--共享鎖:對某一資源加共享鎖,自身能夠讀該資源,其餘人也能夠讀該資源(也能夠再繼續加共享鎖,即 共享鎖可多個共存),但沒法修改。要想修改就必須等全部共享鎖都釋放完以後。語法爲:
select * from table lock in share mode
|--排他鎖:對某一資源加排他鎖,自身能夠進行增刪改查,其餘人沒法進行任何操做。語法爲:
select * from table for update
2.下面援引例子說明 (援自:http://blog.csdn.net/samjustin1/article/details/52210125):
這裏用T1表明一個數據庫執行請求,T2表明另外一個請求,也能夠理解爲T1爲一個線程,T2 爲另外一個線程。
例1:------------------------------------------------------------------------------------------
T1: select * from table lock in share mode(假設查詢會花很長時間,下面的例子也都這麼假設)
T2: update table set column1='hello'
過程:
T1運行(並加共享鎖)
T2運行
if T1還沒執行完
T2等......
else 鎖被釋放
T2執行
end if
T2 之因此要等,是由於 T2 在執行 update 前,試圖對 table 表加一個排他鎖,而數據庫規定同一資源上不能同時共存共享鎖和排他鎖。因此 T2 必須等 T1 執行完,釋放了共享鎖,才能加上排他鎖,而後才能開始執行 update 語句。
例2:------------------------------------------------------------------------------------------
T1: select * from table lock in share mode
T2: select * from table lock in share mode
這裏T2不用等待T1執行完,而是能夠立刻執行。
分析:
T1運行,則 table 被加鎖,好比叫lockA,T2運行,再對 table 加一個共享鎖,好比叫lockB,兩個鎖是能夠同時存在於同一資源上的(好比同一個表上)。這被稱爲共享鎖與共享鎖兼容。這意味着共享鎖不阻止其它人同時讀資源,但阻止其它人修改資源。
例3:------------------------------------------------------------------------------------------
T1: select * from table lock in share mode
T2: select * from table lock in share mode
T3: update table set column1='hello'
T2 不用等 T1 運行完就能運行,T3 卻要等 T1 和 T2 都運行完才能運行。由於 T3 必須等 T1 和 T2 的共享鎖所有釋放才能進行加排他鎖而後執行 update 操做。
例4 (死鎖的發生):------------------------------------------------------------------------------------------
T1:begin tran
select * from table lock in share mode
update table set column1='hello'
T2:begin tran
select * from table lock in share mode
update table set column1='world'
假設 T1 和 T2 同時達到 select,T1 對 table 加共享鎖,T2 也對 table 加共享鎖,當 T1 的 select 執行完,準備執行 update 時,根據鎖機制,T1 的共享鎖須要升級到排他鎖才能執行接下來的 update。在升級排他鎖前,必須等 table 上的其它共享鎖(T2)釋放,同理,T2 也在等 T1 的共享鎖釋放。因而死鎖產生了。
例5:------------------------------------------------------------------------------------------
T1:begin tran
update table set column1='hello' where id=10
T2:begin tran
update table set column1='world' where id=20
這種語句雖然最爲常見,不少人以爲它有機會產生死鎖,但實際上要看狀況
|--若是id是主鍵(默認有主鍵索引),那麼T1會一會兒找到該條記錄(id=10的記錄),而後對該條記錄加排他鎖,T2,一樣,一會兒經過索引定位到記錄,而後對id=20的記錄加排他鎖,這樣T1和T2各更新各的,互不影響。T2也不須要等。
|--若是id是普通的一列,沒有索引。那麼當T1對id=10這一行加排他鎖後,T2爲了找到id=20,須要對全表掃描。但由於T1已經爲一條記錄加了排他鎖,致使T2的全表掃描進行不下去(實際上是由於T1加了排他鎖,數據庫默認會爲該表加意向鎖,T2要掃描全表,就得等該意向鎖釋放,也就是T1執行完成),就致使T2等待。
死鎖怎麼解決呢?一種辦法是,以下:
例6:------------------------------------------------------------------------------------------
T1:begin tran
select * from table for update
update table set column1='hello'
T2:begin tran
select * from table for update
update table set column1='world'
這樣,當 T1 的 select 執行時,直接對錶加上了排他鎖,T2 在執行 select 時,就須要等 T1 事物徹底執行完才能執行。排除了死鎖發生。但當第三個 user 過來想執行一個查詢語句時,也由於排他鎖的存在而不得不等待,第四個、第五個 user 也會所以而等待。在大併發狀況下,讓你們等待顯得性能就太友好了。
因此,有些數據庫這裏引入了更新鎖(如Mssql,注意:Mysql不存在更新鎖)。
例7:------------------------------------------------------------------------------------------
T1:begin tran
select * from table [加更新鎖操做]
update table set column1='hello'
T2:begin tran
select * from table [加更新鎖操做]
update table set column1='world'
更新鎖其實就能夠當作排他鎖的一種變形,只是它也容許其餘人讀(而且還容許加共享鎖)。但不容許其餘操做,除非我釋放了更新鎖。T1 執行 select,加更新鎖。T2 運行,準備加更新鎖,但發現已經有一個更新鎖在那兒了,只好等。當後來有 user三、user4...須要查詢 table 表中的數據時,並不會由於 T1 的 select 在執行就被阻塞,照樣能查詢,相比起例6,這提升了效率。
後面還有意向鎖和計劃鎖:
計劃鎖,和程序員關係不大,就沒去了解。
意向鎖(innodb特有)分意向共享鎖和意向排他鎖。
意向共享鎖:表示事務獲取行共享鎖時,必須先得獲取該表的意向共享鎖;
意向排他鎖:表示事務獲取行排他鎖時,必須先得獲取該表的意向排他鎖;
咱們知道,若是要對整個表加鎖,需保證該表內目前不存在任何鎖。
所以,若是須要對整個表加鎖,那麼就能夠根據:檢查意向鎖是否被佔用,來知道表內目前是否存在共享鎖或排他鎖了。而不須要再一行行地去檢查每一行是否被加鎖。
4、樂觀鎖與悲觀鎖
首先說明,樂觀鎖和悲觀鎖都是針對讀(select)來講的。
案例:
某商品,用戶購買後庫存數應-1,而某兩個或多個用戶同時購買,此時三個執行程序均同時讀得庫存爲「n」,以後進行了一些操做,最後將均執行update table set 庫存數=n-1,那麼,很顯然這是錯誤的。
解決:
使用悲觀鎖(其實說白了也就是排他鎖)
|-- 程序A在查詢庫存數時使用排他鎖(select * from table where id=10 for update)
|-- 而後進行後續的操做,包括更新庫存數,最後提交事務。
|-- 程序B在查詢庫存數時,若是A還未釋放排他鎖,它將等待……
|-- 程序C同B……
使用樂觀鎖(靠表設計和代碼來實現)
|-- 通常是在該商品表添加version版本字段或者timestamp時間戳字段
|-- 程序A查詢後,執行更新變成了:
update table set num=num-1 where id=10 and version=23
這樣,保證了修改的數據是和它查詢出來的數據是一致的(其餘執行程序確定未進行修改)。固然,若是更新失敗,表示在更新操做以前,有其餘執行程序已經更新了該庫存數,那麼就能夠嘗試重試來保證更新成功。爲了儘量避免更新失敗,能夠合理調整重試次數(阿里巴巴開發手冊規定重試次數不低於三次)。
總結:對於以上,能夠看得出來樂觀鎖和悲觀鎖的區別:
悲觀鎖實際使用了排他鎖來實現(select **** for update)。文章開頭說到,innodb加行鎖的前提是:必須是經過索引條件來檢索數據,不然會切換爲表鎖。
所以,悲觀鎖在未經過索引條件檢索數據時,會鎖定整張表。致使其餘程序不容許「加鎖的查詢操做」,影響吞吐。故若是在查詢居多的狀況下,推薦使用樂觀鎖。
「加鎖的查詢操做」:加過排他鎖的數據行在其餘事務中是不能修改的,也不能經過for update或lock in share mode的加鎖方式查詢,但能夠直接經過select ...from...查詢數據,由於普通查詢沒有任何鎖機制。
樂觀鎖更新有可能會失敗,甚至是更新幾回都失敗,這是有風險的。因此若是寫入居多,對吞吐要求不高,可以使用悲觀鎖。
也就是一句話:讀用樂觀鎖,寫用悲觀鎖。
間隙鎖
1.什麼叫間隙鎖
當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對於鍵值在條件範圍內但不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖(NEXT-KEY)鎖。
2.間隙鎖的產生
上面的文字很抽象,如今舉個栗子,介紹間隙鎖是怎麼產生的:
假設有如下表t_student:(其中id爲PK,name爲非惟一索引)
這個時候咱們發出一條這樣的加鎖sql語句:
select id,name from t_student where id > 0 and id < 5 for update;
這時候,咱們命中的數據爲如下着色部分:
細心的朋友可能就會發現,這裏缺乏了條id爲2的記錄,咱們的重點就在這裏。
select ... for update這條語句,是會對數據記錄加鎖的,這裏由於命中了索引,加的是行鎖。從數據記錄來看,這裏排它鎖鎖住數據是id爲一、3和4的這3條數據。
可是,看看前面咱們的介紹——對於鍵值在條件範圍內但不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖。
好了,咱們這裏,鍵值在條件範圍可是不存在的記錄,就是id爲2的記錄,這裏會對id爲2數據加上間隙鎖。假設這時候若是有id=2的記錄insert進來了,是要等到這個事務結束之後纔會執行的
3.間隙鎖的做用
總的來講,有2個做用:防止幻讀和防止數據誤刪/改
(1)防止幻讀
關於幻讀的概念能夠參考這篇文章 https://blog.csdn.net/mweibiao/article/details/80805031 ,這裏就很少作解釋了
假設有下面場景
若是沒有間隙鎖,事務A在T1和T4讀到的結果是不同的,有了間隙鎖,讀的就是同樣的了
這個做用比較重要,假設如下場景:
這種狀況下,若是沒有間隙鎖,會出現的問題是:id爲2的記錄,剛加進去,就被刪除了,這種狀況有時候對業務,是致命性的打擊。加了間隙鎖以後,因爲insert語句要等待事務A執行完以後釋放鎖,避免了這種狀況
4.使用間隙鎖的隱患
最大的隱患就是性能問題
前面提到,假設這時候若是有id=2的記錄insert進來了,是要等到這個事務結束之後纔會執行的,假設是這種場景
這種狀況,對插入的性能就有很大影響了,必須等到事務結束才能進行插入,性能大打折扣
更有甚者,若是間隙鎖出現死鎖的狀況下,會更隱晦,更難定位
怎樣避免死鎖
一、以固定的順序訪問表和行。好比兩個更新數據的事務,事務A 更新數據的順序 爲1,2;事務B更新數據的順序爲2,1。這樣更可能會形成死鎖。
二、大事務拆小。大事務更傾向於死鎖,若是業務容許,將大事務拆小。
三、在同一個事務中,儘量作到一次鎖定所須要的全部資源,減小死鎖機率。
四、下降隔離級別。若是業務容許,將隔離級別調低也是較好的選擇,好比將隔離級別從RR調整爲RC,能夠避免掉不少由於gap鎖形成的死鎖。
五、爲表添加合理的索引。能夠看到若是不走索引將會爲表的每一行記錄添加上鎖,死鎖的機率大大增大。
資料出處:
https://blog.csdn.net/mweibiao/article/details/81672315?utm_source=blogxgwz8
https://blog.csdn.net/localhost01/article/details/78720727