1. 程式人生 > >Mysql心路歷程:Mysql各種鎖機制(入門篇)

Mysql心路歷程:Mysql各種鎖機制(入門篇)

這一篇文章是本人資料庫的第二篇,也是對資料庫學習的階段性總結。對於資料庫鎖的瞭解,是區分程式設計師,尤其是Java程式設計師,中高階的一個重要標誌。也是日常,我們開發中,經常碰到坑的地方。往往,我們無腦的CURD過程中,其實已經出現問題了,鎖問題,但是我們並沒有發現,你那是沒有被大訪問量衝擊。一旦一朝我們衝擊到了,那損失和鍋,是要自己承擔下來的。不多說,我們接下來就來一步步看看Mysql的鎖機制。

一、Mysql鎖的型別

整體上,Mysql鎖的型別,從全域性的到細節的,包含如下幾個:

  • 全域性鎖
  • 表級鎖
    • 普通的表鎖
    • MDL (元資料鎖)
  • 行鎖
    • 讀鎖(共享鎖)
    • 寫鎖(排他鎖、叉鎖)
  • 間隙鎖
    • Next-key lock

大概上,我們日常生產學習中,所能接觸到的就這幾大種類了(個人的腦容量也就能掌握這麼多了,(⊙﹏⊙)b),接下來,我們一個個的說說。

二、 全域性鎖

顧名思義,全域性鎖就是對整個資料庫例項加鎖。Mysql提供了一個加鎖的語句:Flush tables with read lock (FTWRL)。它能使整個例項上面,只讀,所有的寫和更新,都會被阻塞。全域性鎖的使用經典的使用場景是做全域性的資料備份使用,具體的操作,可能平時我們碰到不多,不過要了解幾點:

  • 每次全域性備份過程中,如果是InnoDB引擎完全可以通過MVCC建立一致性檢視,來保證不受備份中,其他操作的影響,問題是不一定所有的資料引擎都是InnoDB
  • 我們同樣可以通過set global readonly=true 來進行只讀性的設定,但是和FTWRL的區別如下:
    • 有些資料庫把第一種模式用作,設定成備庫的只讀限制,而不是用來做備份的,影響面比較大
    • 異常處理機制不一樣:FTWRL這種機制,如果設定之後,客戶端異常斷開連線了,資料庫會主動釋放全域性的鎖,恢復正常;而set這種方式,客戶端異常斷開了,就不會恢復原狀,資料庫會一直只讀,影響很大。
  • 具體對資料庫的全域性鎖,不僅僅阻塞增刪改操作(DML),對資料庫表的增刪改欄位(DDL)也會被阻塞

三、表級鎖

我們首先要知道的是,每次進行select操作或者DML的時候,對錶加的都是MDL的讀鎖,而進行DDL的時候,對錶加的是MDL的寫鎖,讓我們首先來個印象。接下來來看看普通的表鎖與MDL(元資料鎖meta data lock)的區別。

1、普通的表鎖

普通的表鎖也是分讀鎖與寫鎖,資料庫提供語句操作:lock tables … read/write,使用unlock tables進行釋放鎖。具體注意的點是:加了普通的表鎖之後,對當前加鎖執行緒接下來的資料庫操作,都是有影響的。

舉個例子:如果A執行緒使用語句lock tables t1 read, t2 write; 這個語句,那麼,其他執行緒寫t1和讀寫t2都會被阻塞;同時執行緒A再進行unlock之前,也只能讀t1和讀寫t2,連寫t1都是不被允許的。自然也不能訪問其他的表

在沒有出現行鎖之前,都是通過表鎖進行併發控制的,上面例子可見,影響面還是太大,限制太嚴格了。

2、元資料鎖(MDL)

MDL不需要主動加鎖,每當我們訪問一個數據表的時候,會自動被加上,作用是防止在我們進行表的操作的時候,進行了表結構的變更。再5.5這個版本中被引入了Mysql中:

  • 當對一個表進行增刪改查的時候,加MDL的讀鎖
  • 當進行一個表的結構變更的時候,加MDL的寫鎖

讀寫鎖的MDL之間的互斥關係是:

  • 讀鎖與讀鎖不互斥
  • 讀鎖與寫鎖互斥
  • 寫鎖與寫鎖互斥

具體有個經典的例子:經常發生的是,我們給一個表加了個一個欄位或者幾個欄位,很小心了,但是加的過程中,直接整個表掛了,接下來的操作都失敗了或者不返回。接下來我們就看看具體的操作過程:

sessionAsessionBsessionCsessionD
begin;
select * from t limit1
select * from t limit1
alter table t add f int (block)
select * from t limit1 (block)

可見,我們sessionC操作之後,由於sessionA是沒有結束事務的,我們MDL會隨著事務的開啟而加鎖,事務的結束而釋放鎖,所以,sessionA這時候保持住了MDL的讀鎖。然後sessionC想要獲取MDL的寫的時候,由於讀寫互斥,sessionC就被阻塞了。接下來的語句,也都執行不了了,因為接下倆的語句要申請MDL的讀鎖,而有寫鎖已經在阻塞狀態,讀鎖又要排隊等這個寫鎖執行釋放,那接下來的現象可想而知。

我們如何安全的對一個表進行加欄位的操作呢:

  • 在information_schema庫裡面的innodb_tx表中,可以查到當前正在執行的事務,如果是一個長事務,我們可以先考慮kill掉這個事務
  • 如果是頻繁訪問的斷事務比較多的情況,我們可以使用alter table tablename wait N add col這種型別的操作,如果拿不到MDL寫鎖,一段時間會釋放阻塞,不長期影響資料庫。

四、行鎖

這個過程比較複雜,首先,我們來看看,Mysql加行鎖,是使用兩階段加鎖策略的,我們看看什麼叫做兩階段加鎖:

兩階段鎖協議,整個事務分為兩個階段,前一個階段為加鎖,後一個階段為解鎖。在加鎖階段,事務只能加鎖,也可以操作資料,但不能解鎖,直到事務釋放第一個鎖,就進入解鎖階段,此過程中事務只能解鎖,也可以操作資料,不能再加鎖。兩階段鎖協議使得事務具有較高的併發度,因為解鎖不必發生在事務結尾。它的不足是沒有解決死鎖的問題,因為它在加鎖階段沒有順序要求。如兩個事務分別申請了A, B鎖,接著又申請對方的鎖,此時進入死鎖狀態。

1、什麼時候加行鎖

正常,我們select語句時候,是不會新增行鎖的,只會加上MDL的讀鎖,即使這條語句是全表掃描,也不會加行鎖,只不過全表掃描,查詢較慢罷了,並不會因為鎖的問題而對其他操作進行阻塞。下面是我總結的一些加行鎖的場景:

  • select * from t where id = 1 in share model 對主鍵為1的這一行,加行鎖,共享鎖
  • select * from t where id = 1 for update 對主鍵為1的這一行,加行鎖,排它鎖,叉鎖
  • update t set col1 = 1 where id =1 對主鍵為1的這一行加行鎖,排它鎖,叉鎖
  • update t set col1 = 1 where col2 =1 如果col2沒有索引,那麼是加普通表鎖;如果col2是非唯一索引,對所有col2為1的行,加行鎖;如果col2是唯一索引,對col2為1的這一行,加行鎖。行鎖都是排它鎖

p.s.:當然上面所有所列取的操作,都是首先加了MDL的讀鎖的

2、加行鎖的影響

行鎖,之所以存在,就是提高併發度的。取代以前,我們要整表進行加鎖,而引起同一時刻,只能有一個執行緒對資料表進行增刪改的操作,下面我們看一個具體的資料庫操作:

事務A事務B
begin;
update t set k = k+1 where id = 1;
update t set k = k+2 where id = 2;
begin;
update t set k = k+3 where id = 1;
commit;
  • 事務B的update會被阻塞,因為id為1的這行行鎖被事務A所持有
  • begin的時候,沒有任何行鎖被持有,只有當具體操作進行是,依次請求MDL的讀鎖,這一行的排它行鎖
  • 所有,當前事務持有的行鎖,語句執行完都不會釋放,知道commit之後才釋放

所以,按照這種邏輯,越是併發度高的資料表,越要靠事務的後面寫,因為持有行鎖時間短,影響併發度的時間越短。

3、這裡我們引出死鎖

首先我們看接下來的這個模擬操作:

事務A事務B
begin;
update t set k = k+1 where id = 1;begin;
update t set k = k+3 where id = 2;
update t set k = k+2 where id = 2;
update t set k = k+3 where id = 1;

這就是一個經典的死鎖場景,我們來分析下:

  • 事務A的update t set k = k+1 where id = 1;獲取了id為1這一行的行鎖(排它鎖)

  • 事務B的update t set k = k+3 where id = 2;獲取了id為2這一行的行鎖

  • 事務A的update t set k = k+2 where id = 2;要獲取id為2的行鎖,而獲取不到,阻塞

  • 事務B的update t set k = k+3 where id = 1;要獲取id為1的行鎖,獲取不到,阻塞

對於這種,Mysql有兩種機制進行處理:

  • innodb_lock_wait_timeout可以通過這個引數,進行設定鎖等待時間,超過這個時間,阻塞的程序釋放所有持有的鎖,回滾。
  • innodb_deadlock_detect通過設定為on,能主動監測死鎖,通過回滾死鎖聯調中的一個事物,來解決死鎖

第一種情況雖然能控制,死鎖,但是時間不好設定,例如我們設定一個10s,如果一個執行緒被鎖住,要等待10s才能進行回滾,併發度自然不高,如果我設定低了,1s,那麼一個正常等待的,並非死鎖,也會被回滾。如此一來得不償失。下面重點說說主動死鎖檢測

4、主動死鎖檢測

每當吧innodb_deadlock_detect設定成on,MySQL會主動檢測死鎖:

  • 一個執行緒加入
  • 即將被等待其他執行緒的鎖而堵住
  • 判斷當前執行緒持有的鎖,是否堵住了其他系統中正在執行的執行緒
  • 如果是,將回滾當前執行緒的事務

看起來很好,然後會有代價:每次對比是否當前執行緒堵住了其他執行緒這一步,會對比所有系統正在執行的執行緒,時間複雜度是O(n)。當前執行的執行緒數少不成問題,如果是1000個正在執行的執行緒,那麼這就是100w次的對比,這個過程極度消耗CPU資源。結果可能檢測出沒有死鎖,然後會發現:最終CPU飈的老高,然而執行的條數沒幾個!解決辦法有下面幾個:

  • 臨時關掉innodb_deadlock_detect,但是這樣會有很多超時,不實用
  • 控制資料庫的併發度:可以從中介軟體這層控制(Java這裡),或者有能力的從資料庫這一層進行控制
  • 業務欄位拆分:例如我們將一行記錄拆分成多行,讓一行的併發度下降(例如併發扣減id為1這一行的金額,我們可以拆分成id為100,id為200,id為300這三行的金額,最後要查詢的時候,將這三行相加)

五、結束

下面一篇文章,會重點講間隙鎖,這個內容最為複雜,涉及到了所謂資料庫解決幻讀的機制問題,特別單獨抽