Mysql共享鎖、排他鎖、悲觀鎖、樂觀鎖


一、相關名詞

  |--表級鎖(鎖定整個表)

  |--頁級鎖(鎖定一頁)

  |--行級鎖(鎖定一行)

  |--共享鎖(S鎖,MyISAM 叫做讀鎖)

  |--排他鎖(X鎖,MyISAM 叫做寫鎖)

  |--間隙鎖(NEXT-KEY鎖)

  |--悲觀鎖(抽象性,不真實存在這個鎖)

  |--樂觀鎖(抽象性,不真實存在這個鎖)

 

二、InnoDB與MyISAM

  Mysql 在5.5之前默認使用 MyISAM 存儲引擎,之后使用 InnoDB 。查看當前存儲引擎:

show variables like '%storage_engine%';

  MyISAM 操作數據都是使用的表鎖,你更新一條記錄就要鎖整個表,導致性能較低,並發不高。當然同時它也不會存在死鎖問題。

  而 InnoDB 與 MyISAM 的最大不同有兩點:一是 InnoDB 支持事務;二是 InnoDB 采用了行級鎖。也就是你需要修改哪行,就可以只鎖定哪行。

  在 Mysql 中,行級鎖並不是直接鎖記錄,而是鎖索引。索引分為主鍵索引和非主鍵索引兩種,如果一條sql 語句操作了主鍵索引,Mysql 就會鎖定這條主鍵索引;如果一條語句操作了非主鍵索引,MySQL會先鎖定該非主鍵索引,再鎖定相關的主鍵索引。

  InnoDB 行鎖是通過給索引項加鎖實現的,如果沒有索引,InnoDB 會通過隱藏的聚簇索引來對記錄加鎖。也就是說:如果不通過索引條件檢索數據,那么InnoDB將對表中所有數據加鎖,實際效果跟表鎖一樣。因為沒有了索引,找到某一條記錄就得掃描全表,要掃描全表,就得鎖定表。

 

三、共享鎖與排他鎖

 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 運行,准備加更新鎖,但發現已經有一個更新鎖在那兒了,只好等。當后來有 user3、user4...需要查詢 table 表中的數據時,並不會因為 T1 的 select 在執行就被阻塞,照樣能查詢,相比起例6,這提高了效率。

 

后面還有意向鎖和計划鎖:

  計划鎖,和程序員關系不大,就沒去了解。
  意向鎖(innodb特有)分意向共享鎖和意向排他鎖。
  意向共享鎖:表示事務獲取行共享鎖時,必須先得獲取該表的意向共享鎖;
  意向排他鎖:表示事務獲取行排他鎖時,必須先得獲取該表的意向排他鎖;
 我們知道,如果要對整個表加鎖,需保證該表內目前不存在任何鎖。

  因此,如果需要對整個表加鎖,那么就可以根據:檢查意向鎖是否被占用,來知道表內目前是否存在共享鎖或排他鎖了。而不需要再一行行地去檢查每一行是否被加鎖。

 

四、樂觀鎖與悲觀鎖

  首先說明,樂觀鎖和悲觀鎖都是針對讀(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為1、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讀到的結果是不一樣的,有了間隙鎖,讀的就是一樣的了

  (2)防止數據誤刪/改

   這個作用比較重要,假設以下場景:

  這種情況下,如果沒有間隙鎖,會出現的問題是:id為2的記錄,剛加進去,就被刪除了,這種情況有時候對業務,是致命性的打擊。加了間隙鎖之后,由於insert語句要等待事務A執行完之后釋放鎖,避免了這種情況

 4.使用間隙鎖的隱患
 最大的隱患就是性能問題

  前面提到,假設這時候如果有id=2的記錄insert進來了,是要等到這個事務結束以后才會執行的,假設是這種場景

  這種情況,對插入的性能就有很大影響了,必須等到事務結束才能進行插入,性能大打折扣

  更有甚者,如果間隙鎖出現死鎖的情況下,會更隱晦,更難定位

 

怎樣避免死鎖   

  1、以固定的順序訪問表和行。比如兩個更新數據的事務,事務A 更新數據的順序 為1,2;事務B更新數據的順序為2,1。這樣更可能會造成死鎖。

  2、大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。

  3、在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖概率。

  4、降低隔離級別。如果業務允許,將隔離級別調低也是較好的選擇,比如將隔離級別從RR調整為RC,可以避免掉很多因為gap鎖造成的死鎖。

  5、為表添加合理的索引。可以看到如果不走索引將會為表的每一行記錄添加上鎖,死鎖的概率大大增大。

 

 資料出處:

      https://blog.csdn.net/mweibiao/article/details/81672315?utm_source=blogxgwz8

      https://blog.csdn.net/localhost01/article/details/78720727


免責聲明!

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



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