MySQL優化系列(八)--鎖機制超詳細解析(鎖分類、事務併發、引擎併發控制)

Jack__Frost發表於2017-07-04

當一個系統訪問量上來的時候,不只是資料庫效能瓶頸問題了,資料庫資料安全也會浮現,這時候合理使用資料庫鎖機制就顯得異常重要了。

本系列demo下載

(一)MySQL優化筆記(一)–庫與表基本操作以及資料增刪改

(二)MySQL優化筆記(二)–查詢優化(1)(非索引設計)

(三)MySQL優化筆記(二)–查詢優化(2)(外連線、多表聯合查詢以及查詢注意點)

(四) MySQL優化筆記(三)–索引的使用、原理和設計優化

(五) MySQL優化筆記(四)–表的設計與優化(單表、多表)

(六)MySQL優化筆記(五)–資料庫儲存引擎

(七)MySQL優化筆記(六)–儲存過程和儲存函式

(八)MySQL優化筆記(七)–檢視應用詳解

文章結構:

(1)鎖機制概述;
(2)MySQL各種鎖詳解(並針對MyISAM和InnoDB引擎);
(3)專案中鎖的設計方式。

目錄結構:

(1)鎖機制概述

  • 什麼是鎖,以及為什麼使用鎖和鎖的運作?
  • 鎖定機制分類?
    • 按封鎖型別分類
    • 按封鎖的資料粒度分類
      • 行級鎖定
      • 表級鎖定
      • 頁級鎖定
  • 資料庫事務機制。
    • 什麼叫事務?簡稱ACID。
    • 事務引起的併發排程問題
    • 理論上的事務的三級封鎖協議
    • 事務隔離級別
  • 活鎖與死鎖的探究
    • 活鎖
    • 死鎖

(2)MySQL各種鎖詳解(並針對MyISAM和InnoDB引擎)

  • 在這之前先講述下MySQL在共享鎖、排他鎖基礎上的一個鎖擴充–**意向鎖
  • MySQL鎖機制總述
  • MyISAM引擎的鎖機制
    • 測試MyISAM表共享讀鎖
    • 測試MyISAM表獨佔寫鎖
      • 行級鎖定
      • 表級鎖定
      • 頁級鎖定
  • InnoDB引擎的鎖機制
    • 與MyISAM不同,InnoDB有兩大不同點
    • 檢視InnoDB行鎖爭用情況
    • Innodb行鎖模式以及加鎖方法
    • InnoDB檢視鎖語句
    • InnoDB行鎖實現方式與驗證
    • 補充:基於InnoDB對索引加鎖的間隙鎖
    • 補充:InnoDB引擎什麼時候使用表鎖??
    • 事務引擎導致的死鎖

(3)專案中鎖的設計方式

  • InnoDB與MyISAM的不同死鎖
  • 在專案中如何去避免死鎖
  • 如何檢視死鎖

一、MySQL鎖機制概述:

(一)什麼是鎖,以及為什麼使用鎖和鎖的運作?

鎖是計算機協調多個程式或純執行緒併發訪問某一資源的機制。在資料庫中,除傳統的計算資源(CPU、RAM、I/O)的爭用以外,資料也是一種供許多使用者共享的資源。如何保證資料併發訪問的一致性、有效性是所在有資料庫必須解決的一個問題,鎖衝突也是影響資料庫併發訪問效能的一個重要因素。從這個角度來說,鎖對資料庫而言顯得尤其重要,也更加複雜。

防止更新丟失,並不能單靠資料庫事務控制器來解決,需要應用程式對要更新的資料加必要的鎖來解決。

鎖的運作?

事務T在度某個資料物件(如表、記錄等)操作之前,先向系統發出請求,對其加鎖,加鎖後事務T就對資料庫物件有一定的控制,在事務T釋放它的鎖之前,其他事務不能更新此資料物件。

(二)鎖定機制分類?

鎖定機制就是資料庫為了保證資料的一致性而使各種共享資源在被併發訪問訪問變得有序所設計的一種規則。MySQL資料庫由於其自身架構的特點,存在多種資料儲存引擎,每種儲存引擎所針對的應用場景特點都不太一樣,為了滿足各自特定應用場景的需求,每種儲存引擎的鎖定機制都是為各自所面對的特定場景而優化設計,所以各儲存引擎的鎖定機制也有較大區別。

按封鎖型別分類:(資料物件可以是表可以是記錄)

1)排他鎖:(又稱寫鎖,X鎖)

一句總結:會阻塞其他事務讀和寫。

若事務T對資料物件A加上X鎖,則只允許T讀取和修改A,其他任何事務都不能再對加任何型別的鎖,知道T釋放A上的鎖。這就保證了其他事務在T釋放A上的鎖之前不能再讀取和修改A。

2)共享鎖:(又稱讀取,S鎖)

一句總結:會阻塞其他事務修改表資料。

若事務T對資料物件A加上S鎖,則其他事務只能再對A加S鎖,而不能X鎖,直到T釋放A上的鎖。這就保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。

X鎖和S鎖都是載入某一個資料物件上的。也就是資料的粒度。

按封鎖的資料粒度分類如下:

1)行級鎖定(row-level):

一句總結:行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最低,併發度也最高。

**詳細:**行級鎖定最大的特點就是鎖定物件的顆粒度很小,也是目前各大資料庫管理軟體所實現的鎖定顆粒度最小的。由於鎖定顆粒度很小,所以發生鎖定資源爭用的概率也最小,能夠給予應用程式儘可能大的併發處理能力而提高一些需要高併發應用系統的整體效能。

**缺陷:**由於鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發生死鎖。

2)表級鎖定(table-level):

一句總結:表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最高,併發度最低。

**詳細:**和行級鎖定相反,表級別的鎖定是MySQL各儲存引擎中最大顆粒度的鎖定機制。該鎖定機制最大的特點是實現邏輯非常簡單,帶來的系統負面影響最小。所以獲取鎖和釋放鎖的速度很快。由於表級鎖一次會將整個表鎖定,所以可以很好的避免困擾我們的死鎖問題。

**缺陷:**鎖定顆粒度大所帶來最大的負面影響就是出現鎖定資源爭用的概率也會最高,致使併發度大打折扣。

3)頁級鎖定(page-level):(MySQL特有)

一句總結:頁級鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,併發度一般。

**詳細:**頁級鎖定是MySQL中比較獨特的一種鎖定級別,在其他資料庫管理軟體中也並不是太常見。頁級鎖定的特點是鎖定顆粒度介於行級鎖定與表級鎖之間,所以獲取鎖定所需要的資源開銷,以及所能提供的併發處理能力也同樣是介於上面二者之間。

**缺陷:**頁級鎖定和行級鎖定一樣,會發生死鎖。

從這裡我們應該引申去思考行鎖更多的缺點:(因為我們執行sql主要依賴行鎖來提高併發度)
1- 比表級鎖、頁級鎖消耗更多記憶體
2- 如果你在大部分資料上經常進行GROUP BY操作或者必須經常掃描整個表,比其它鎖定明顯慢很多。
3- 更容易發生死鎖。

其次,我們應該思考什麼情況下用表鎖、行鎖

(因為我們主要使用引擎預設是這兩個,MyISAM是表級鎖;InnoDb是行級鎖,當然也支援表級鎖)

(三)資料庫事務機制(這個是資料庫核心,本系列多篇文章提到並重復)(為什麼提及事務?因為事務中有封鎖機制)

1)什麼叫事務?簡稱ACID。是恢復和併發控制的基本單位。

A 事務的原子性(Atomicity):
指一個事務要麼全部執行,要麼不執行.也就是說一個事務不可能只執行了一半就停止了.比如你從取款機取錢,這個事務可以分成兩個步驟:1劃卡,2出錢.不可能劃了卡,而錢卻沒出來.這兩步必須同時完成.要麼就不完成.

C 事務的一致性(Consistency):
指事務的執行並不改變資料庫中資料的一致性.例如,完整性約束了a+b=10,一個事務改變了a,那麼b也應該隨之改變.

I 獨立性(Isolation):
事務的獨立性也有稱作隔離性,是指兩個以上的事務不會出現交錯執行的狀態.因為這樣可能會導致資料不一致.

D 永續性(Durability):
事務的永續性是指事務執行成功以後,該事務所對資料庫所作的更改便是持久的儲存在資料庫之中,不會無緣無故的回滾.

2)事務引起的併發排程問題:(這些會另開一篇給予例項)首先明確讀資料要以最新的為準。

下面這些是併發操作破壞了事務的隔離性導致的。

例子根結點:原本有31張票,甲售貨員讀取票數為事務一,乙售貨員讀取票數為事務二。甲售貨員賣出一張票為事務三。乙售貨員賣出一張票為事務四。

1.髒讀(dirty read):A事務讀取B事務尚未提交的更改資料,並在這個資料基礎上操作。如果B事務回滾,那麼A事務讀到的資料根本不是合法的,稱為髒讀。在oracle中,由於有version控制,不會出現髒讀。

例子:事務三中售出一張票,修改了資料庫31變30,可是事務還沒提交,事務一讀了修改了的資料(30),但是事務三被中斷撤銷了,比如存錢修改資料庫後,在返回資料給客戶時出現異常,那麼事務三就是不成功的,資料會變回31。可是事務一讀了一個不正確的資料。

2.不可重複讀(unrepeatable read):A事務讀取了B事務已經提交的更改資料。比如A事務第一次讀取資料,然後B事務更改該資料並提交,A事務再次讀取資料,兩次讀取的資料不一樣。

例子:事務一讀取後,事務三對31張票修改,可是事務一中,有再次讀這張票的SQL語句,那麼事務一得到的跟第一次不同的值(31張票就可能變成30張了)。

3.幻讀(phantom read):A事務讀取了B事務已經提交的新增資料。注意和不可重複讀的區別,這裡是新增與刪除,不可重複讀是更改。這兩種情況對策是不一樣的,對於不可重複讀,只需要採取行級鎖防止該記錄資料被更改或刪除,然而對於幻讀必須加表級鎖,防止在這個表中新增一條資料。

**例子:**事務一按一定條件讀取31條記錄,但是後面還有一些步驟需要再次讀取此資料校驗,所以沒提交事務,可這個時候事務三(31條記錄變30條記錄)執行成功,賣出了票。那當事務一再次讀的時候,就讀到了30。這樣讀到的資料就不是最新的了。

4.丟失更新:A事務撤銷時,把已提交的B事務的資料覆蓋掉。

5.覆蓋更新:A事務提交時,把已提交的B事務的資料覆蓋掉。

4和5的例子:事務一和事務二同時讀入同一資料–16張,事務一執行賣一張A-1=15,將15返回;事務二執行賣一張A-1=15,將15返回;這樣一來就會覆蓋了事務一對資料庫的修改。

有朋友反饋說,還是區分不了不可重複讀與幻讀。
在這裡重新補充:
不可重複讀重在修改,就是你剛讀過的一個資料,再讀一次又一不一樣了。
而幻讀重在記錄的增刪,就是你第一次讀的資料量,第二次讀又不一樣了。
所以要解決:
解決不可重複讀:這個資料只有在修改事務完全提交後才可讀取資料。
解決幻讀:符合這個條件下的資料,在操作事務完成處理之前,其他事務不可新增新資料、或者刪除資料。

3)理論上的事務的三級封鎖協議:

1.一級封鎖協議:事務T中如果對資料R有寫操作,必須在這個事務中對R的第一次讀操作前對它加X鎖,直到事務結束才釋放。事務結束包括正常結束(COMMIT)和非正常結束(ROLLBACK)。

2.二級封鎖協議:一級封鎖協議加上事務T在讀取資料R之前必須先對其加S鎖,讀完後方可釋放S鎖。

3.三級封鎖協議 :一級封鎖協議加上事務T在讀取資料R之前必須先對其加S鎖,直到事務結束才釋放。

三級鎖操作一個比一個厲害(滿足高階鎖則一定滿足低階鎖)。但有個非常致命的地方,一級鎖協議就要在第一次讀加x鎖,直到事務結束。幾乎就要在整個事務加寫鎖了,效率非常低。三級封鎖協議只是一個理論上的東西,實際資料庫常用另一套方法來解決事務併發問題。

4)事務隔離級別:

這個是mysql用意向鎖(另一種機制,一會講解)來解決事務併發問題,為了區別封鎖協議,弄了一個新概念隔離性級別:包括Read Uncommitted、Read Committed、Repeatable Read、Serializable。mysql 一般預設Repeatable Read。

1.讀未提交(Read Uncommited,RU)

一句總結:讀取資料一致性在最低階別,只能保證不讀物理上損壞的資料,會髒讀,會不可重複讀,會幻讀。

這種隔離級別下,事務間完全不隔離,會產生髒讀,可以讀取未提交的記錄,實際情況下不會使用。

2.讀提交(Read commited,RC)

一句總結:讀取資料一致性在語句級別,不會髒讀,會不可重複讀,會幻讀。

僅能讀取到已提交的記錄,這種隔離級別下,會存在幻讀現象,所謂幻讀是指在同一個事務中,多次執行同一個查詢,返回的記錄不完全相同的現象。幻讀產生的根本原因是,在RC隔離級別下,每條語句都會讀取已提交事務的更新,若兩次查詢之間有其他事務提交,則會導致兩次查詢結果不一致。雖然如此,讀提交隔離級別在生產環境中使用很廣泛。

3.可重複讀(Repeatable Read, RR)

一句總結:讀取資料一致性在事務級別,不會髒讀,不會不可重複讀,會幻讀。

可重複讀隔離級別解決了不可重複讀的問題,但依然沒有解決幻讀的問題。不可重複讀重點在修改,即讀取過的資料,兩次讀的值不一樣;而幻讀則側重於記錄數目變化【插入和刪除】。

4.序列化(Serializable)

一句總結:讀取資料一致性在最高階別,事務級別,不會髒讀,不會不可重複讀,不會幻讀。

在序列化隔離模式下,消除了髒讀,幻象,但事務併發度急劇下降,事務的隔離級別與事務的併發度成反比,隔離級別越高,事務的併發度越低。實際生產環境下,dba會在併發和滿足業務需求之間作權衡,選擇合適的隔離級別。

這樣就解釋了為什麼僅靠事務就能解決丟失修改是錯誤的了。

隔離級別/會不會/併發排程問題 髒讀 不可重複讀 幻讀
讀未提交
讀已提交 不會
可重複讀 不會 不會
序列化 不會 不會 不會

(四)活鎖與死鎖的探究:

採用封鎖的方法可以有效防止資料的不一致性,單封鎖本身會引起麻煩,就是死鎖和活鎖。

1)活鎖:

定義:

如果事務T1封鎖了資料物件R後,事務T2也請求封鎖R,於是T2等待,接著T3也請求封鎖R。當T1釋放了載入R上的鎖後,系統首先批准T3的請求,T2只能繼續等待。接著T4也請求封鎖R,T3釋放R上的鎖後,系統又批轉了T4的請求。這樣的一直迴圈下去,事務T2就只能永遠等待了,這樣情況叫活鎖。

解決方法:

採用先來先服務的佇列策略。佇列式申請。

2)死鎖:

定義:

當兩個事務分別鎖定了兩個單獨的物件,這時每一個事務都要求在另一個事務鎖定的物件上獲得一個鎖,因此每一個事務都必須等待另一個事務釋放佔有的鎖。這就發生了死鎖了。

例子:兩個事務,事務都有兩個操作。當同時發生時,事務A鎖定first表,事務B鎖定second表,導致了死鎖。

在這裡插入圖片描述

解決方法:

理論上預防死鎖的發生就是要破壞產生死鎖的條件。

  1. 一次封鎖法。
    一次封鎖法要求每個事務必須一次將所有要使用的資料全部加鎖,否則就不能繼續執行。

此方法存在的問題:(一)一次將以後要用到的全部資料加鎖,加大封鎖範圍,降低系統的併發度。(二)資料庫中資料是不斷變化的,原來不要求封鎖的資料,在執行過程中可能會變成封鎖物件,所以很難事先精確確定每個事務要封鎖的資料物件,為此只能擴大封鎖範圍,將事務在執行過程中可能要封鎖的資料物件全部加鎖,這就更降低了併發度。

  1. 順序封鎖法:
    預先對資料物件規定一個封鎖熟悉怒,所有事務都按這個順序實行封鎖。如:在B樹結構的索引中,規定封鎖的順序必須從根結點開始,然後是下一級的子女結點,逐級封鎖。

此方法存在的問題:(一)資料庫系統中封鎖的資料物件極多,隨著資料的插入、刪除等操作而不斷變化,要維護這樣的資源的封鎖順序很難,成本高。(二)事務的封鎖請求可隨著事務的執行而動態地決定,很難事先確定每一個事務要封鎖哪些物件,因此很難按規定的順序去加鎖。比如:規定資料物件的封鎖順序:A、B、C、D、E。事務T3起初要求封鎖資料物件B、C、E,但當它封鎖了B、C後,才發現需要封鎖A。

以上就是策略就是作業系統中廣為採用的預防死鎖的策略,但並不適合資料庫。所以資料庫系統一般採用診斷並解除死鎖的方法。

死鎖的診斷與解除:

資料庫系統中診斷死鎖的方法與作業系統類似,一般是用超時法或事務等待圖法。

  1. 超時法:
    指的是如果一個事務的等待時間超過了規定的時限,就認為傳送死鎖。
    不足:(一)有可能誤判死鎖,事務因為其他原因使等待時機超過時限。(二)時限若設定得太長,死鎖發生後不能及時發現。
  2. 等待圖法:
    指的是用事務等待圖動態反應所有事務的等待情況。
    事務等待圖是一個有向圖G=(T,U),其中T為結點的集合,每個結點表示正在執行的事務。U為邊的集合,每條邊表示事務等待的情況。若T1等待T2,則T1、T2之間劃一條有向邊,從T1指向T2。事務等待圖動態地反映了所有事務的等待情況。併發控制子系統週期性地檢測事務等待圖,如果發現圖中存在迴路,則表示系統中出現了死鎖。

以上就是死鎖的診斷與解除了**。而且DBMS併發控制子系統一旦檢測到系統中存在死鎖,就會設法解除。通常是選擇一個處理死鎖代價最小的事務,將其撤銷,釋放此事務持有的所有的鎖,使其他事務能繼續執行下去。(而且要對撤銷的事務所執行的資料修改操作進行恢復)

死鎖真例項子呈現:

//測試表
CREATE TABLE `tb3` (
	`id` SMALLINT(5) UNSIGNED NOT NULL AUTO_INCREMENT,
	`username` VARCHAR(30) NOT NULL,
	PRIMARY KEY (`id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
AUTO_INCREMENT=5
;
//測試資料,就這樣插幾條
insert into tb3(username) values('fuzhu');
測試開始:
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from tb3 where id=3 for update
    -> ;
+----+----------+
| id | username |
+----+----------+
|  3 | Rose     |
+----+----------+
1 row in set (0.00 sec)
//再用一執行緒去訪問(wins、Linux再開個視窗)
mysql> delete from tb3 where id =2;		//行鎖沒鎖住第二行
Query OK, 1 row affected (0.00 sec)
mysql> delete from tb3 where id =3;		//因為前面有查詢修改事務鎖住了這一行
//然後出現死鎖,最終出現以下資訊:
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

最後匯出死鎖日誌:

mysql> show engine innodb status\G;
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
170618 20:55:42 INNODB MONITOR OUTPUT	//INNODB引擎監控
=====================================
Per second averages calculated from the last 46 seconds
-----------------
BACKGROUND THREAD
-----------------
srv_master_thread loops: 19 1_second, 19 sleeps, 1 10_second, 10 background, 10
flush
srv_master_thread log flush and writes: 19
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 4, signal count 4
Mutex spin waits 1, rounds 19, OS waits 0
RW-shared spins 4, rounds 120, OS waits 4
RW-excl spins 0, rounds 0, OS waits 0
Spin rounds per wait: 19.00 mutex, 30.00 RW-shared, 0.00 RW-excl
------------
TRANSACTIONS			//事務資訊
------------
Trx id counter 8450C
Purge done for trx's n:o < 8432A undo n:o < 0
History list length 1942
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 8450A, not started
MySQL thread id 2, OS thread handle 0x15e4, query id 272 localhost 127.0.0.1 roo
t
---TRANSACTION 8450B, ACTIVE 35 sec starting index read  //事務ID=8450E,活躍了35s  
mysql tables in use 1, locked 1			//表有一個在使用
LOCK WAIT 3 lock struct(s), heap size 376, 2 row lock(s), undo log entries 1		//3個鎖,2個行鎖,1個undo log  
MySQL thread id 3, OS thread handle 0x2130, query id 278 localhost 127.0.0.1 roo			//該事務的執行緒ID=3
t updating
delete from tb3 where id =3				//這是當前事務執行的SQL,死鎖的sql
------- TRX HAS BEEN WAITING 32 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 0 page no 312 n bits 72 index `PRIMARY` of table `test`.`t			//上面SQL等待的鎖資訊
b3` trx id 8450B lock_mode X locks rec but not gap waiting
Record lock, heap no 4 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
 0: len 2; hex 0003; asc   ;;
 1: len 6; hex 000000000908; asc       ;;
 2: len 7; hex 890000013b0110; asc     ;  ;;
 3: len 4; hex 526f7365; asc Rose;;

------------------
---TRANSACTION 84509, ACTIVE 101 sec		//事務ID=84509,活躍了101s  
2 lock struct(s), heap size 376, 1 row lock(s)	//2個鎖,1個行鎖,1個undo log  
MySQL thread id 1, OS thread handle 0x2358, query id 279 localhost 127.0.0.1 roo			///該事務的執行緒ID=1
t
show engine innodb status		//這是當前事務執行的SQL,查詢日誌
--------
FILE I/O				//IO流輸出日誌
--------
I/O thread 0 state: wait Windows aio (insert buffer thread)
I/O thread 1 state: wait Windows aio (log thread)
I/O thread 2 state: wait Windows aio (read thread)
I/O thread 3 state: wait Windows aio (read thread)
I/O thread 4 state: wait Windows aio (read thread)
I/O thread 5 state: wait Windows aio (read thread)
I/O thread 6 state: wait Windows aio (write thread)
I/O thread 7 state: wait Windows aio (write thread)
I/O thread 8 state: wait Windows aio (write thread)
I/O thread 9 state: wait Windows aio (write thread)
Pending normal aio reads: 0 [0, 0, 0, 0] , aio writes: 0 [0, 0, 0, 0] ,
 ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
489 OS file reads, 12 OS	q file writes, 11 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 0.11 writes/s, 0.09 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX	//插入緩衝區和自適應雜湊索引
-------------------------------------
Ibuf: size 1, free list len 0, seg size 2, 0 merges
merged operations:
 insert 0, delete mark 0, delete 0
discarded operations:
 insert 0, delete mark 0, delete 0
Hash table size 222149, node heap has 1 buffer(s)
0.00 hash searches/s, 0.04 non-hash searches/s
---
LOG
---
Log sequence number 1718594032
Log flushed up to   1718594032
Last checkpoint at  1718594032
0 pending log writes, 0 pending chkp writes
12 log i/o's done, 0.04 log i/o's/second
----------------------
BUFFER POOL AND MEMORY		//快取池與記憶體
----------------------
Total memory allocated 114835456; in additional pool allocated 0
Dictionary memory allocated 267680
Buffer pool size   6848
Free buffers       6369
Database pages     478
Old database pages 0
Modified db pages  0
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 478, created 0, written 3
0.00 reads/s, 0.00 creates/s, 0.04 writes/s
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s

LRU len: 478, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
1 read views open inside InnoDB
Main thread id 3220, state: waiting for server activity
Number of rows inserted 0, updated 0, deleted 1, read 30
0.00 inserts/s, 0.00 updates/s, 0.02 deletes/s, 0.02 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

1 row in set (0.00 sec)

ERROR:
No query specified

以上就是鎖機制的基本知識了。


二、MySQL各種鎖詳解(並針對MyISAM和InnoDB引擎):

部分參考此文章

(1)在這之前先講述下MySQL在共享鎖、排他鎖基礎上的一個鎖擴充–意向鎖。

(InnoDB特有,此外在索引加上中,InnoDB還有一種鎖叫間隙鎖。一會補充。)
封鎖粒度結合封裝型別又是一層設計。也就是說表鎖中使用又可分為共享鎖和排他鎖。同理行鎖。(MySQL意向鎖基於這個問題的出現而設計)

但是我們要思考一個問題:(表級鎖和行級鎖之間的衝突)

事務A鎖住了表中的一行,讓這一行只能讀,不能寫。之後,事務B申請整個表的寫鎖。如果事務B申請成功,那麼理論上它就能修改表中的任意一行,這與A持有的行鎖是衝突的。資料庫需要避免這種衝突,就是說要讓B的申請被阻塞,直到A釋放了行鎖。資料庫要怎麼判斷這個衝突呢?

普通認為兩步:step1:判斷表是否已被其他事務用表鎖鎖表。step2:判斷表中的每一行是否已被行鎖鎖住。但是這樣的方法效率很低,因為要遍歷整個表。

所以解決方案是:意向鎖。

在意向鎖存在的情況下,事務A必須先申請表的意向共享鎖,成功後再申請一行的行鎖。

在意向鎖存在的情況下,兩步驟為:step1:判斷表是否已被其他事務用表鎖鎖表。step2:發現表上有意向共享鎖,說明表中有些行被共享行鎖鎖住了,因此,事務B申請表的寫鎖會被阻塞。

注意:

申請意向鎖的動作是資料庫完成的,就是說,事務A申請一行的行鎖的時候,資料庫會自動先開始申請表的意向鎖,不需要我們程式設計師使用程式碼來申請。

**意向鎖目的:

**解決表級鎖和行級鎖之間的衝突

意向鎖是一種表級鎖,鎖的粒度是整張表。結合共享與排他鎖使用,分為意向共享鎖(IS)和意向排他鎖(IX)。意向鎖為了方便檢測表級鎖和行級鎖之間的衝突,故在給一行記錄加鎖前,首先給該表加意向鎖。也就是同時加意向鎖和行級鎖。

(2)剛剛MySQL鎖機制總述:

MySQL中不同的儲存引擎支援不同的鎖機制。比如MyISAM和MEMORY儲存引擎採用的表級鎖,BDB採用的是頁面鎖,也支援表級鎖,InnoDB儲存引擎既支援行級鎖,也支援表級鎖,預設情況下采用行級鎖。

三類: 行級、表級、頁級

僅從鎖的角度來說:表級鎖更適合於以查詢為主,只有少量按索引條件更新資料的應用,如Web應用;而行級鎖則更適合於有大量按索引條件併發更新少量不同資料,同時又有併發查詢的應用,如一些線上事務處理(OLTP)系統。

(3)MyISAM引擎的鎖機制:

MyISAM只有表鎖,其中又分為讀鎖和寫鎖。

前面得知:mysql的表鎖有兩種模式:表共享讀鎖(table read lock)和表獨佔寫鎖(table write lock)。(意向鎖是解決行鎖與表鎖衝突,不在此引擎中)。

所以對於MyISAM引擎的鎖相容用一個常規圖描述:

表閱讀姿勢:先確定當前鎖模式,思考另一使用者請求,就去看請求鎖模式,思考是否相容。

請求鎖模式/是否相容/當前鎖模式 NONE 讀鎖 寫鎖
讀鎖
寫鎖

(一)MyISAM表的讀操作,不會阻塞其他使用者對同一個表的讀請求,但會阻塞對同一個表的寫請求。

(二)MyISAM表的寫操作,會阻塞其他使用者對同一個表的讀和寫操作。

(三)MyISAM表的讀、寫操作之間、以及寫操作之間是序列的。

當一個執行緒獲得對一個表的寫鎖後,只有持有鎖執行緒可以對錶進行更新操作。其他執行緒的讀、寫操作都會等待,直到鎖被釋放為止。

MyISAM引擎的鎖表演示講述:

首先明確:1. MySQL認為寫請求一般比讀請求重要。 2. MyISAM在執行查詢語句(SELECT)前,會自動給涉及的所有表加讀鎖,在執行更新操作(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,這個過程並不需要使用者干預,因此使用者一般不需要直接用LOCK TABLE命令給MyISAM表顯式加鎖。

檢視MyISAM表級鎖的爭用狀態:

這裡寫圖片描述
接著我們開始演示MyISAM引擎鎖表:

(一)測試MyISAM表共享讀鎖

資料準備

CREATE TABLE `tb3` (
	`id` SMALLINT(5) UNSIGNED NOT NULL AUTO_INCREMENT,
	`username` VARCHAR(30) NOT NULL,
	PRIMARY KEY (`id`)
)
COLLATE='latin1_swedish_ci'
ENGINE=MyISAM
AUTO_INCREMENT=6
;
//測試資料,就這樣插幾條
insert into tb3(username) values('fuzhu');

測試語句。開啟兩個MySQL訪問視窗去訪問。
其中一個:
這裡寫圖片描述
另外一個:在前一個訪問後,就立刻去訪問。
在這裡插入圖片描述

(二)測試MyISAM表獨佔寫鎖

兩個視窗,一個開啟寫的事務,這樣的話在這個事務提交以前,其他事務都不可以修改與讀取這個表了。

在這裡插入圖片描述

直到這個事務提交,另一個事務才能讀取成功。且看時間停了多久

在這裡插入圖片描述

(三)另外,MyISAM的鎖排程就是讓我們更好地去讓MySQL適應市場的需求,解決剛剛首要明確的問題。

  1. 通過指定啟動引數low-priority-updates,使MyISAM引擎預設給予讀請求以優先的權利。
  2. 通過執行命令SET LOW_PRIORITY_UPDATES=1,使該連線發出的更新請求優先順序降低。
  3. 通過指定INSERT、UPDATE、DELETE語句的LOW_PRIORITY屬性,降低該語句的優先順序。

雖然上面方式都挺極端的。但是MySQL也提供了一種折中的辦法來調節讀寫衝突,即給系統引數max_write_lock_count設定一個合適的值,當一個表的讀鎖達到這個值後,MySQL變暫時將寫請求的優先順序降低,給讀程式一定獲得鎖的機會。

(四)MyISAM併發插入問題:

MyISAM儲存引擎有一個系統變數,concurrent_insert,專門用來控制併發插入行為的,值可以為0,1,2.

concurrent_insert為0時候,不允許插入

concurrent_insert為1時候,如果mysql沒有空洞(中間沒有被刪除的行),myISAM執行一個程式讀表的時候,另一個程式從表尾插入記錄,這也是mysql預設設定。

concurrent_insert為2時候,無論MyISAM表中有沒有空洞,都允許在表尾並行的插入。

(4)InnoDB引擎的鎖機制:

(一)與MyISAM不同,InnoDB有兩大不同點:

1)支援事務

2)採用行級鎖

(二)檢視InnoDB行鎖爭用情況:

這裡寫圖片描述

(三)Innodb行鎖模式以及加鎖方法:

一共三類:共享鎖,排他鎖,意向鎖。其中意向鎖分為意向共享鎖和意向排他鎖。詳情請見前文。

表閱讀姿勢:先確定當前鎖模式,思考另一使用者請求,就去看請求鎖模式,思考是否相容。
在這裡插入圖片描述

注意:

如果一個事務請求的鎖模式與當前的鎖模式相容,innodb就將請求的鎖授予該事務;反之,如果兩者不相容,該事務就要等待鎖釋放。意向鎖是Innodb自動加的,不需要使用者干預。

對於UPDATE、DELETE、INSERT語句,Innodb會自動給涉及的資料集加排他鎖(X);對於普通SELECT語句,Innodb不會加任何鎖。

(四)InnoDB檢視鎖語句:

//顯示共享鎖(S) :
 SELECT * FROM table_name WHERE .... LOCK IN SHARE MODE
 //顯示排他鎖(X):
 SELECT * FROM table_name WHERE .... FOR UPDATE.

使用select … in share mode獲取共享鎖,主要用在需要資料依存關係時,確認某行記錄是否存在,並確保沒有人對這個記錄進行update或者delete。

(五)InnoDB行鎖實現方式與驗證:(可能會遇到所有事務併發問題–InnoDB是事務引擎)

InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,後者是通過再資料塊中,對相應資料行加鎖來實現的。InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索資料,innoDB才使用行級鎖,否則InnoDB將使用表鎖,在實際開發中應當注意。

驗證一:行鎖是針對索引的

  1. 資料表準備:注意是沒有索引的表!沒有索引!沒有索引!唯一索引也是一種索引,不能用!
CREATE TABLE `tb0` (
	`id` SMALLINT(5) UNSIGNED NOT NULL,
	`id2` SMALLINT(5) UNSIGNED NOT NULL
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
;
//插兩條記錄
insert into tb0 values(1,1),(2,2);
  1. 可以看到有1和2的session視窗去訪問。
    第一個視窗(使用者)去訪問。先設定事務提交方式,再進行去查詢修改操作。沒有索引情況下,一個使用者訪問,可事務還沒提交,第二個使用者就不能訪問。可見,行鎖是針對索引的!!!
    這裡寫圖片描述
  2. 提交事務後。
    可見第二個視窗(使用者)鎖等待超時。
    這裡寫圖片描述

驗證二:行鎖真正的作用範圍

  1. 資料準備:表基於第一個驗證去改造(加索引–普通索引,索引值可出現多次。),測試狀態也是:set autocommi = 0; 事務手動提交。

在innodb中,不同的索引的考量是不同的。具體見:

CREATE TABLE `tb7` (
	`id` SMALLINT(5) UNSIGNED NOT NULL,
	`id2` SMALLINT(5) UNSIGNED NOT NULL,
	INDEX id (id)
)
COLLATE='latin1_swedish_ci'
ENGINE=InnoDB
;
insert into tb7 values(1,1),(2,2),(1,3);

//並且新增索引:
ALTER TABLE tb0 ADD INDEX id1(id);

在這裡插入圖片描述

  1. 還是兩個視窗(使用者)同時訪問:結果是第一個使用者訪問不再鎖表,而是鎖行。
    這裡寫圖片描述

  2. 還是兩個視窗(使用者)同時訪問。使用的是普通索引,所以索引值可多個。也就是我故意插入的1和3記錄。那麼當確定訪問第一條記錄,就會把索引為1指向的給鎖定,也就是鎖定了1和3記錄,因為他們的索引值都是1。
    這裡寫圖片描述

最終會導致第二個視窗(使用者)訪問超時。

這裡寫圖片描述
因為Mysql行鎖是針對索引加的鎖,不是針對記錄加的鎖,索引雖然訪問不同的記錄,但是他們的索引相同,是會出現衝突的,在設計資料庫時候需要注意這一點。上面只有將欄位id2,也新增上索引才能解決衝突問題。這也是mysql效率低的一個原因。

(六)補充:基於InnoDB對索引加鎖的間隙鎖

定義:

當我們用範圍條件而不是相等條件檢索資料,並請求共享或排他鎖時,InnoDB會給符合條件的已有資料的索引項加鎖;對於鍵值在條件範圍內但並不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。

舉例來說

假如user表中只有101條記錄,其userid 的值分別是1,2,…,100,101,下面的SQL:SELECT * FROM user WHERE userid > 100 FOR UPDATE

上面是一個範圍條件的檢索,InnoDB不僅會對符合條件的userid 值為101的記錄加鎖,也會對userid 大於101(這些記錄並不存在)的“間隙”加鎖。

目的:

一方面是為了防止幻讀,以滿足相關隔離級別的要求,對於上面的例子,要是不使用間隙鎖,如果其他事務插入了userid 大於100的任何記錄,那麼本事務如果再次執行上述語句,就會發生幻讀;另一方面,是為了滿足其恢復和複製的需要。有關其恢復和複製對機制的影響,以及不同隔離級別下InnoDB使用間隙鎖的情況。

實際開發:

可見,在使用範圍條件檢索並鎖定記錄時,InnoDB這種加鎖機制會阻塞符合條件範圍內鍵值的併發插入,這往往會造成嚴重的鎖等待。因此,在實際開發中,尤其是併發插入比較多的應用,我們要儘量優化業務邏輯,儘量使用相等條件來訪問更新資料,避免使用範圍條件。

補充對行鎖之間隙鎖的思考、LBCC到MVCC的設計思考

我們也應該從這裡開始深入:思考InnoDB的行鎖演算法。

InnoDB是一個支援行鎖的儲存引擎,鎖的型別有:共享鎖(S)、排他鎖(X)、意向共享(IS)、意向排他(IX)。
也就是我們的LBCC,基於鎖的併發控制。
InnoDB有三種行鎖的演算法:
1)Record Lock:單個行記錄上的鎖。
2)Gap Lock:間隙鎖,鎖定一個範圍,但不包括記錄本身。GAP鎖的目的,是為了防止同一事務的兩次當前讀,出現幻讀的情況。
3)Next-Key Lock:1+2,鎖定一個範圍,並且鎖定記錄本身。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。

而上面所說的間隙鎖就是行鎖的演算法之一。
但是 我們要想到LBCC這樣的加鎖訪問,其實並不算是真正的併發,或者說它只能實現併發的讀,因為它最終實現的是讀寫序列化,這樣就大大降低了資料庫的讀寫效能。
基於這樣,MySQL在後續設計中引入MVCC這一基於多版本的併發控制協議。主要是解決讀寫的衝突!!!
MVCC只工作在RC與RR級別下,當然最主要還是為RR級別設計。(MVCC的部落格解析太多了,我就不多說了)。
然後我們要思考一個問題,MVCC解決讀寫衝突,那寫寫衝突如何解決?
那就是一個樂觀鎖的設計了。(這個部落格也很多,也不多說了)

(七)補充:InnoDB引擎什麼時候使用表鎖??

對於InnoDB表,在絕大部分情況下都應該使用行級鎖,因為事務和行鎖往往是我們之所以選擇InnoDB表的理由。但在個另特殊事務中,也可以考慮使用表級鎖。

1. 第一種情況是:

事務需要更新大部分或全部資料,表又比較大,如果使用預設的行鎖,不僅這個事務執行效率低,而且可能造成其他事務長時間鎖等待和鎖衝突,這種情況下可以考慮使用表鎖來提高該事務的執行速度。

2. 第二種情況是:

事務涉及多個表,比較複雜,很可能引起死鎖,造成大量事務回滾。這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖、減少資料庫因事務回滾帶來的開銷。

在InnoDB下 ,使用表鎖要注意以下兩點。
(1)使用LOCK TALBES雖然可以給InnoDB加表級鎖,但必須說明的是,表鎖不是由InnoDB儲存引擎層管理的,而是由其上一層MySQL Server負責的,僅當autocommit=0、innodb_table_lock=1(預設設定)時,InnoDB層才能知道MySQL加的表鎖,MySQL Server才能感知InnoDB加的行鎖,這種情況下,InnoDB才能自動識別涉及表級鎖的死鎖;否則,InnoDB將無法自動檢測並處理這種死鎖。
(2)在用LOCAK TABLES對InnoDB鎖時要注意,要將AUTOCOMMIT設為0,否則MySQL不會給表加鎖;事務結束前,不要用UNLOCAK TABLES釋放表鎖,因為UNLOCK TABLES會隱含地提交事務;COMMIT或ROLLBACK產不能釋放用LOCAK TABLES加的表級鎖,必須用UNLOCK TABLES釋放表鎖,正確的方式見如下語句。

如果需要寫表t1並從表t讀,可以按如下做:

mysql> SET AUTOCOMMIT=0;
mysql> LOCAK TABLES t1 WRITE, t2 READ, ...;

	[do something with tables t1 and here];
	
mysql> COMMIT;
mysql> UNLOCK TABLES;

三、專案中鎖的設計方式

1)兩個引擎的死鎖對比

MyISAM表鎖是無死鎖的,這是因為MyISAM總是一次性獲得所需的全部鎖,要麼全部滿足,要麼等待,因此不會出現死鎖。但是在InnoDB中,除單個SQL組成的事務外,鎖是逐步獲得的,這就決定了InnoDB發生死鎖是可能的。

發生死鎖後,InnoDB一般都能自動檢測到,並使一個事務釋放鎖並退回,另一個事務獲得鎖,繼續完成事務。但在涉及外部鎖,或涉及鎖的情況下,InnoDB並不能完全自動檢測到死鎖,這需要通過設定鎖等待超時引數innodb_lock_wait_timeout來解決。需要說明的是,這個引數並不是只用來解決死鎖問題,在併發訪問比較高的情況下,如果大量事務因無法立即獲取所需的鎖而掛起,會佔用大量計算機資源,造成嚴重效能問題,甚至拖垮資料庫。我們通過設定合適的鎖等待超時閾值,可以避免這種情況發生。

通常來說,死鎖都是應用設計的問題,通過調整業務流程、資料庫物件設計、事務大小、以及訪問資料庫的SQL語句,絕大部分都可以避免。

2)下面就通過例項來介紹幾種死鎖的常用避免和解決方法。

1)在應用中,如果不同的程式會併發存取多個表,應儘量約定以相同的順序為訪問表,這樣可以大大降低產生死鎖的機會。如果兩個session訪問兩個表的順序不同,發生死鎖的機會就非常高!但如果以相同的順序來訪問,死鎖就可能避免。
2)在程式以批量方式處理資料的時候,如果事先對資料排序,保證每個執行緒按固定的順序來處理記錄,也可以大大降低死鎖的可能。
3)在事務中,如果要更新記錄,應該直接申請足夠級別的鎖,即排他鎖,而不應該先申請共享鎖,更新時再申請排他鎖,甚至死鎖。
4)在REPEATEABLE-READ隔離級別下,如果兩個執行緒同時對相同條件記錄用SELECT…ROR UPDATE加排他鎖,在沒有符合該記錄情況下,兩個執行緒都會加鎖成功。程式發現記錄尚不存在,就試圖插入一條新記錄,如果兩個執行緒都這麼做,就會出現死鎖。這種情況下,將隔離級別改成READ COMMITTED,就可以避免問題。
5)當隔離級別為READ COMMITED時,如果兩個執行緒都先執行SELECT…FOR UPDATE,判斷是否存在符合條件的記錄,如果沒有,就插入記錄。此時,只有一個執行緒能插入成功,另一個執行緒會出現鎖等待,當第1個執行緒提交後,第2個執行緒會因主鍵重出錯,但雖然這個執行緒出錯了,卻會獲得一個排他鎖!這時如果有第3個執行緒又來申請排他鎖,也會出現死鎖。對於這種情況,可以直接做插入操作,然後再捕獲主鍵重異常,或者在遇到主鍵重錯誤時,總是執行ROLLBACK釋放獲得的排他鎖。

儘管通過上面的設計和優化等措施,可以大減少死鎖,但死鎖很難完全避免。因此,在程式設計中總是捕獲並處理死鎖異常是一個很好的程式設計習慣。

3)如何檢視死鎖

如果出現死鎖,可以用SHOW INNODB STATUS命令來確定最後一個死鎖產生的原因和改進措施。


原始碼:DEMO下載戳此處

MySQL優化系列(八)–鎖機制超詳細解析(鎖分類、事務併發、引擎併發控制)講完了

又是一篇MySQL優化筆記,這是積累的必經一步,我會繼續出這個系列文章,分享經驗給大家。歡迎在下面指出錯誤,共同學習!!你的點贊是對我最好的支援!!

更多內容,可以訪問JackFrost的部落格

相關文章