關於MySQL的鎖機制詳解

鎖概述

  MySQL的鎖機制,就是數據庫爲了保證數據的一致性而設計的面對併發場景的一種規則。html

  最顯著的特色是不一樣的存儲引擎支持不一樣的鎖機制,InnoDB支持行鎖和表鎖,MyISAM支持表鎖。mysql

  表鎖就是把整張表鎖起來,特色是加鎖快,開銷小,不會出現死鎖,鎖粒度大,發生鎖衝突的機率高,併發相對較低。
  行鎖就是以行爲單位把數據鎖起來,特色是加鎖慢,開銷大,會出現死鎖,鎖粒度小,發生鎖衝突的機率低,併發度也相對錶鎖較高。linux

MyISAM鎖

MyISAM的鎖調度

  在MyISAM引擎中,讀鎖和寫鎖是互斥的,讀寫操做是串行的,鎖設計方案以下:sql

  對於寫操做:若是表上沒有鎖,則在上面加一把寫鎖,不然,把請求放到寫鎖隊列中。
  對於讀操做:若是表上沒有鎖,則在上面加一把讀鎖,不然,把請求方到讀鎖隊列中。數據庫

  這是什麼意思呢?併發

  意思就是說MyISAM在執行查詢語句前,會自動給涉及的全部表加讀鎖,在執行更新語句(增刪改操做)前,會自動給涉及的表加寫鎖,這個過程並不須要用戶干預。ide

  當一個鎖被釋放時,鎖定權會先被寫鎖隊列中的線程獲得,當寫鎖隊列中的請求都跑完後,才輪到讀鎖隊列中的請求。(即便讀請求先到鎖等待隊列中,寫請求後到,寫請求也會插入到讀請求以前!這就是MySQL認爲寫請求通常比讀請求重要)測試

  這就意味着,若是一個表上有不少更新操做,那麼select語句將等待直到別的更新都結束後才能查到東西。這也就是爲何MyISAM表不適合大量更新操做應用的緣由,由於大量更新操做可能致使查詢操做很難得到讀鎖,從而長久阻塞,導致程序響應超時。spa

也許你須要顯式加鎖

  表鎖語句有以下三條(MyISAM和InnoDB都同樣):線程

  LOCK TABLES tb_name READ; 加讀鎖,其餘會話可讀,但不能更新。
  LOCK TABLES tb_name WRITE; 加寫錯,其餘會話不可讀,不可寫。
  UNLOCK TABLES; 釋放鎖

當有連續多表更新的時候,可能會出現頻繁的表鎖競爭,更新數據的速度反而會降低,而且更新這個表的時候另外一個表的數據可能被別的線程更新了(MyISAM是沒有事務的),這個時候,咱們就須要鎖住多張表,再進行更新。

這裏示例,同時上鎖更新兩個表,給id爲1的用戶餘額加1:

  LOCK TABLES tb_1 WRITE,tb_2 WRITE;
  UPDATE tb_1 SET balance=balance+1 WHERE user_id=1;
  UPDATE tb_2 SET balance=balance+1 WHERE user_id=1;
  UNLOCK TABLES;

特別注意:顯式加鎖的時候,必須同時取得全部涉及表的鎖,而且,只能訪問顯式加鎖的這些表,不能訪問未加鎖的表。

(MyISAM的內容就這一章,接下來的章節都是InnDB的了,特此說明哈。)

InnoDB鎖類型

共享鎖(S鎖、讀鎖)

  SELECT * FROM tb_name LOCK IN SHARE MODE;

  一個事務獲取了一個數據行的讀鎖,容許其餘事務也來獲取讀鎖,可是不容許其餘事務來獲取寫鎖。也就是說,我上了讀鎖以後,其餘事務也能夠來讀,可是不能增刪改。

排他鎖(X鎖、寫鎖)

  SELECT * FROM tb_name FOR UPDATE;

  一個事務獲取了一個數據行的寫鎖,其餘事務就不能再跑來獲取任何鎖了,全部請求都會被阻塞,直到當前的寫鎖被釋放。

意向鎖與MDL鎖

  意向共享鎖(IS):事務在給一個數據行加共享鎖以前必須先取得該表的IS鎖。
  意向排他鎖(IX):事務在給一個數據行加共享鎖以前必須先取得該表的IX鎖。
  MDL鎖:在事務中,InnoDB會給涉及的全部表加上一個MDL鎖,其餘事務就不能夠執行任何DDL語句的操做。(親測只要在事務中,無論是查詢語句仍是更新語句,涉及到的表都會被加上MDL鎖)

  這三種鎖,是InnoDB內部使用的鎖,是自動實現的,不須要用戶干預。

幾種行鎖技術

記錄鎖(record lock)

  這是一個索引記錄鎖,它是創建在索引記錄上的鎖(主鍵和惟一索引都算),不少時候,鎖定一條數據,因爲無索引,每每會致使整個表被鎖住,創建合適的索引能夠防止掃描整個表。

  如:開兩個會話,兩個事務,而且都不commit,該表有主鍵,兩個會話修改同一條數據,第一個會話update執行後,第二個會話的update是沒法執行成功的,會進入等待狀態,可是若是update別的數據行就能夠成功。

  再例如:開兩個會話,兩個事務,而且都不commit,而且該表無主鍵無索引,那麼第二個會話無論改什麼都會進入等待狀態。由於無索引的話,整個表的數據都被第一個會話鎖定了。

間隙鎖(gap lock)

  MySQL默認隔離級別是可重複讀,這個隔離級別爲了不幻讀現象,引入了這個間隙鎖,對索引項之間的間隙上鎖。

  示例:

  (會話1)
  START TRANSACTION;
  SELECT * FROM tb_name WHERE id>10 LOCK IN SHARE MODE;
  (會話2)
  START TRANSACTION;
  INSERT INTO tb_name(id,name) VLUES(11,"張三")

  結果怎樣?會話2會進入執行等待狀態,直至會話1的鎖釋放或者鎖超時。

next-key鎖(記錄所和間隙鎖的組合)

  當InnoDB掃描索引記錄時,會先對選中的索引記錄加上記錄鎖(record Lock),再對索引記錄兩遍的間隙加上間隙鎖(gap lock)。

  仍是以間隙鎖的例子說,假如表中沒有id=10的這行數據,會話2添加的id該爲10,會成功嗎?

  答案是不會,由於它不止鎖了id>10的間隙,連id=10也一塊兒鎖了。

表鎖

  在InnoDB中絕大部分都應該使用行鎖,由於事務和行鎖每每是咱們選擇InnoDB表的理由,可是在個別特殊事務中,也能夠考慮使用表鎖。

  狀況1:事務須要更新大部分或者所有數據,表又比較大,若是使用默認的行鎖,不只這個事務執行效率低,並且可能形成其餘事務長時間鎖等待和鎖衝突,這種狀況下能夠考慮使用表鎖來提升事務的執行速度。

  狀況2:事務涉及多個表,比較複雜,極可能引發死鎖,形成大量事務回滾,這種狀況也能夠考慮一次性鎖定事務涉及的表,從而避免死鎖,減小數據庫因事務回滾帶來的開銷。

  固然,這兩種狀況不能太多,不然就應該從業務和程序設計上進行拆分處理,而不是由數據庫來承擔這個事情。

  例子以下:

  LOCK TABLES tb_name WRITE;
  UNLOCK TABLES;

注意:在事務中鎖表時,在事務結束前不要釋放鎖,由於unlock tables會隱含提交事務,因此正確的作法是結束事務後再釋放鎖。

鎖等待和死鎖

  鎖等待是指一個事務過程當中產生的鎖,其餘事務須要等待上一個事務釋放它的鎖,才能佔用該資源,若是該事務一直不釋放,就須要繼續等待下去,直到超過了鎖等待時間,會報一個超時錯誤。

  查看鎖等待容許時間:

  SHOW VARIABLES LIKE "innodb_lock_wait_timeout"

  死鎖是指兩個或兩個以上的進程在執行過程當中,因爭奪資源而形成的一種互相等待的現象,就是所謂的死循環。

  典型的實驗過程就是兩個事務併發,互相修改本身的一條數據,緊接着又修改對方的鎖定的那條數據,都要等待對方的鎖,死鎖就產生了。

  出現死鎖的問題並不可怕,解決死鎖一般有以下辦法:

  1.不要把無關的操做放到事務裏,小事務發生衝突的機率較低。
  2.若是不一樣的程序會併發存取多個表,應儘可能約定以相同的順序來訪問表,這樣事務就會造成定義良好的查詢而且沒有死鎖。
  3.儘可能按照索引去查數據,範圍查找增長了鎖衝突的可能性。
  4.對於很是容易產生死鎖的業務部分,能夠嘗試升級鎖粒度,經過表鎖定來減小死鎖產生的機率。

鎖監控

表鎖監控

  獲取表鎖爭用狀況:

    SHOW STATUS LIKE "table%"

  查了不少資料,確實是這個獲取方法,可是我本身沒測出來它的用處,試了兩臺數據庫都不行,很奇怪。

  查詢哪些表正在被鎖定:

    SHOW OPEN TABLES WHERE In_use > 0;

  這個命令監控的是被表鎖鎖住的表,親測若是用行鎖,這個命令是沒有反應的,真的得本身動手實踐才能發現真相。

行鎖監控

  獲取行鎖爭用狀況:

  SHOW STATUS LIKE "innodb_row_lock%"

  下面介紹幾張表,能夠幫助咱們監控當前的事務並分析可能存在的鎖問題。

  select * from information_schema.innodb_trx;

  主要字段以下:

  trx_id:惟一的事務id號
  trx_state:當前事務的狀態,lock wait鎖等待狀態,running執行中狀態。
  trx_started:事務開始時間
  trx_wait_started:事務開始等待時間
  trx_mysql_thread_id:線程id
  trx_query:事務運行的SQL語句

  持有鎖的對象:

  select * from information_schema.innodb_locks;  

  鎖等待的對象:

  select * from information_schema.innodb_lock_waits;

解密

爲何鎖一行數據,速度就變得這麼慢?

  實驗內容:兩個會話兩個事務,會話1鎖,會話2改,目標是不一樣的行數據。

  會話1的where條件必須是索引,才能鎖住這一行,不然就會鎖住整張表的數據,讓會話2上不了鎖。

  會話2的where條件也必須是索引,才能鎖住這一行,不然會試圖去鎖整張表的數據,而整張表的數據已經有一行被會話1鎖了,因此會話2鎖不上。

爲何我要鎖一行,MySQL給我鎖全表?

  即便在條件中使用了索引,可是是否使用索引來檢索數據是由MySQL經過判斷不一樣執行計劃的代價來決定的,若是MySQL認爲全表掃描效率更高,好比對一些很小的表,它就不會使用索引,這種狀況下InnoDB也會對全表記錄上鎖(申明一點,行鎖不會升級成表鎖,它其實是把全部行都上了鎖)。

事務中混合使用存儲引擎會怎樣?

  MySQL的服務層無論理事務,事務是由下層的存儲引擎實現的(表鎖是由MySQL的服務層實現的),因此在同一個事務中,使用多種存儲引擎的表是有風險的。

  好比在事務中同時操做innodb和myisam的表,正常提交不會有問題,可是若是要回滾,myisam的表是不會被回滾的。

  所以,在一個事務中,最好不要使用不一樣存儲引擎的表。

先開事務再鎖表?仍是鎖了表再開事務?

  答案是先開事務再鎖表,由於START TRANSACTION語句會隱含了UNLOCK TABLES,一開事務就等於釋放了以前的表鎖。

我就是開一個事務執行SQL,算不算上鎖?

  InnoDB採用的是兩階段鎖定協議。

  在事務執行過程當中,隨時均可以執行鎖定,鎖只有在commit或者rollback的時候纔會釋放(這裏說的是行鎖哈^_^,表鎖是不在存儲引擎這層的),而且全部的鎖是在同一時刻釋放。

  innodb會根據隔離級別在須要的時候自動加鎖,優先走隔離級別的規則,而後纔是行鎖,若是數據確實隔離了,那麼是不會上鎖的(不信小夥伴們能夠親測,開事務改數據會自動上鎖,可是開事務查數據不會上鎖)。

  顯式加鎖語句是LOCK IN SHARE MODE 和 FOR UPDATE了。

(隔離級別的內容請往這裏跳:https://www.linuxidc.com/Linux/2018-11/155273.htm

怎麼測試它到底有沒有上鎖呢?

  兩種辦法:

  第一種,在事務中使用顯式加鎖語句,不在事務中使用你是感受不到它上了鎖的。

  第二種,關閉自動提交模式 

  SET autocommit=0

  關閉以後就能夠不開事務直接顯式上鎖,直到你執行commit或者rollback它纔會釋放鎖。

  這其實就證實了一個不少人都不知道的事情:每一條SQL都是一個事務。只不過都是自動提交的,因此人們感受不到事務的存在而已,當關閉了自動提交後,就必須手動提交事務纔可讓SQL生效。

  查詢自動提交是否開啓:

  SHOW VARIABLES LIKE "autocommit"

(這裏有一個我還沒弄明白的問題:我只能肯定每一個更新語句是開了事務的,但我不知道每個查詢是否是開了事務,沒辦法去證實,也沒想出來該怎麼去證實,有知道的小夥伴能夠交流一下哦)

文章同步發佈: https://www.geek-share.com/detail/2752692802.html

參考文章:

相關文章
相關標籤/搜索