重新學習Mysql資料庫7:詳解MyIsam與InnoDB引擎的鎖實現
- 2019 年 11 月 29 日
- 筆記
本文是微信公眾號【Java技術江湖】的《重新學習MySQL資料庫》其中一篇,本文部分內容來源於網路,為了把本文主題講得清晰透徹,也整合了很多我認為不錯的技術部落格內容,引用其中了一些比較好的部落格文章,如有侵權,請聯繫作者。
該系列博文會告訴你如何從入門到進階,從sql基本的使用方法,從MySQL執行引擎再到索引、事務等知識,一步步地學習MySQL相關技術的實現原理,更好地了解如何基於這些知識來優化sql,減少SQL執行時間,通過執行計劃對SQL性能進行分析,再到MySQL的主從複製、主備部署等內容,以便讓你更完整地了解整個MySQL方面的技術體系,形成自己的知識框架。
如果對本系列文章有什麼建議,或者是有什麼疑問的話,也可以關注公眾號【Java技術江湖】聯繫作者,歡迎你參與本系列博文的創作和修訂。
說到鎖機制之前,先來看看Mysql的存儲引擎,畢竟不同的引擎的鎖機制也隨著不同。
三類常見引擎:
MyIsam :不支援事務,不支援外鍵,所以訪問速度快。鎖機制是表鎖,支援全文索引
InnoDB :支援事務、支援外鍵,所以對比MyISAM,InnoDB的處理效率差一些,並要佔更多的磁碟空間保留數據和索引。鎖機制是行鎖,不支援全文索引
Memory:數據是存放在記憶體中的,默認哈希索引,非常適合存儲臨時數據,伺服器關閉後,數據會丟失掉。
如何選擇存儲引擎:
MyISAM:應用是以讀操作和插入操作為主,只有很少的更新和刪除操作,並且對事務的完整性、並發性要求不是很高。
InnoDB:用於事務處理應用程式,支援外鍵,如果應用對事務的完整性有比較高的要求,在並發條件下要求數據的一致性。更新刪除等頻繁(InnoDB可以有效的降低由於刪除和更新導致的鎖定),對於數據準確性要求比較高的,此引擎適合。
Memory:通常用於更新不太頻繁的小表,用以快速得到訪問結果。
Mysql中的鎖
如果熟悉多執行緒,那麼對鎖肯定是有概念的,鎖是電腦協調多個進程或執行緒對某一資源並發訪問的機制。
Mysql中的鎖分為表鎖和行鎖:
顧名思義,表鎖就是鎖住一張表,而行鎖就是鎖住一行。
表鎖的特點:開銷小,不會產生死鎖,發生鎖衝突的概率高,並且並發度低。
行鎖的特點:開銷大,會產生死鎖,發生鎖衝突的概率低,並發度高。
因此MyISAM和Memory引擎採用的是表鎖,而InnoDB存儲引擎採用的是行鎖。
MyISAM的鎖機制:
分為共享讀鎖和獨佔寫鎖。
讀鎖是:當某一進程對某張表進行讀操作時(select),其他執行緒也可以讀,但是不能寫。簡單的理解就是,我讀的時候你不能寫。
寫鎖是:當某一進程對某種表某張表的寫時(insert,update,,delete),其他執行緒不能寫也不能讀。可以理解為,我寫的時候,你不能讀,也不能寫。
因此MyISAM的讀操作和寫操作,以及寫操作之間是串列的!MyISAM在執行讀寫操作的時候會自動給表加相應的鎖(也就是說不用顯示的使用lock table命令),MyISAM總是一次獲得SQL語句所需要的全部鎖,這也是MyISAM不會出現死鎖的原因。
下面分別舉關於寫鎖和讀鎖的例子:
寫鎖:
事務1 |
事務2 |
---|---|
取得firsttest表的寫鎖:mysql> lock table firsttest write;Query OK, 0 rows affected (0.00 sec) |
|
當前事務對查詢、更新和插入操作都可以執行mysql> select * from first_test ;+—-+——+ |
id |
mysql> unlock table;Query OK, 0 rows affected (0.00 sec) |
等待 |
|
mysql> select * from first_test;+—-+——+ |
讀鎖例子如下:
事務1 |
事務2 |
---|---|
獲得表firstread的鎖定mysql> lock table firsttest read;Query OK, 0 rows affected (0.00 sec) |
|
當前事務可以查詢該表記錄:mysql> select * from first_test;+—-+——+ |
id |
但是當前事務不能查詢沒有鎖定的表:mysql> select * from goods;ERROR 1100 (HY000): Table 'goods' was not locked with LOCK TABLES |
其他事務可以查詢或更新未鎖定的表:mysql> select * from goods;+—-+————+——+ |
而且插入更新鎖定的表都會報錯:mysql> insert into firsttest(age) values(15);ERROR 1099 (HY000): Table 'firsttest' was locked with a READ lock and can't be updatedmysql> update firsttest set age=100 where id =1;ERROR 1099 (HY000): Table 'firsttest' was locked with a READ lock and can't be updated |
當更新被鎖定的表時會等待:mysql> update first_test set age=100 where id =1;等待…… |
mysql> unlock table;Query OK, 0 rows affected (0.00 sec) |
mysql> update first_test set age=100 where id =1;Query OK, 1 row affected (38.82 sec)Rows matched: 1 Changed: 1 Warnings: 0 |
並發插入
剛說到Mysql在插入和修改的時候都是串列的,但是MyISAM也支援查詢和插入的並發操作。
MyISAM中有一個系統變數concurrent_insert(默認為1),用以控制並發插入(用戶在表尾插入數據)行為。
當concurrent_insert為0時,不允許並發插入。
當concurrent_insert為1時,如果表中沒有空洞(中間沒有被刪除的行),MyISAM允許一個進程在讀表的同時,另一個進程從表尾插入記錄。
當concurrent_insert為2時,無論MyISAM表中有沒有空洞,都可以在末尾插入記錄
事務1 |
事務2 |
---|---|
mysql> lock table first_test read local;Query OK, 0 rows affected (0.00 sec)–加入local選項是說明,在表滿足並發插入的前提下,允許在末尾插入數據 |
|
當前進程不能進行插入和更新操作mysql> insert into firsttest(age) values(15);ERROR 1099 (HY000): Table 'firsttest' was locked with a READ lock and can't be updatedmysql> update firsttest set age=200 where id =1;ERROR 1099 (HY000): Table 'firsttest' was locked with a READ lock and can't be updated |
其他進程可以進行插入,但是更新會等待:mysql> insert into firsttest(age) values(15);Query OK, 1 row affected (0.00 sec)mysql> update firsttest set age=200 where id =2;等待….. |
當前進程不能不能訪問其他進程插入的數據mysql> select * from first_test;+—-+——+ |
id |
釋放鎖以後皆大歡喜mysql> unlock table;Query OK, 0 rows affected (0.00 sec) |
等待 |
插入的和更新的都出來的:mysql> select * from first_test;+—-+——+ |
id |
需要注意的:
並發插入是解決對同一表中的查詢和插入的鎖爭用。
如果對有空洞的表進行並發插入會產生碎片,所以在空閑時可以利用optimize table命令回收因刪除記錄產生的空洞。
鎖調度
在MyISAM中當一個進程請求某張表的讀鎖,而另一個進程同時也請求寫鎖,Mysql會先讓後者獲得寫鎖。即使讀請求比寫請求先到達鎖等待隊列,寫鎖也會插入到讀鎖之前。
因為Mysql總是認為寫請求一般比讀請求重要,這也就是MyISAM不太適合有大量的讀寫操作的應用的原因,因為大量的寫請求會讓查詢操作很難獲取到讀鎖,有可能永遠阻塞。
處理辦法:
1、指定Insert、update、delete語句的low_priority屬性,降低其優先順序。
2、指定啟動參數low-priority-updates,使得MyISAM默認給讀請求優先的權利。
3、執行命令set lowpriorityupdates=1,使該連接發出的請求降低。
4、指定maxwritelock_count設置一個合適的值,當寫鎖達到這個值後,暫時降低寫請求的優先順序,讓讀請求獲取鎖。
但是上面的處理辦法造成的原因就是當遇到複雜的查詢語句時,寫請求可能很難獲取到鎖,這是一個很糾結的問題,所以我們一般避免使用複雜的查詢語句,如果如法避免,則可以再資料庫空閑階段(深夜)執行。
我們知道mysql在以前,存儲引擎默認是MyISAM,但是隨著對事務和並發的要求越來越高,便引入了InnoDB引擎,它具有支援事務安全等一系列特性。
InnoDB鎖模式
InnoDB實現了兩種類型的行鎖。
共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同的數據集的排他鎖。
排他鎖(X):允許獲得排他鎖的事務更新數據,但是組織其他事務獲得相同數據集的共享鎖和排他鎖。
可以這麼理解:
共享鎖就是我讀的時候,你可以讀,但是不能寫。排他鎖就是我寫的時候,你不能讀也不能寫。其實就是MyISAM的讀鎖和寫鎖,但是針對的對象不同了而已。
除此之外InnoDB還有兩個表鎖:
意向共享鎖(IS):表示事務準備給數據行加入共享鎖,也就是說一個數據行加共享鎖前必須先取得該表的IS鎖
意向排他鎖(IX):類似上面,表示事務準備給數據行加入排他鎖,說明事務在一個數據行加排他鎖前必須先取得該表的IX鎖。
InnoDB行鎖模式兼容列表:

注意:
當一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之如果請求不兼容,則該事務就等待鎖釋放。
意向鎖是InnoDB自動加的,不需要用戶干預。
對於insert、update、delete,InnoDB會自動給涉及的數據加排他鎖(X);對於一般的Select語句,InnoDB不會加任何鎖,事務可以通過以下語句給顯示加共享鎖或排他鎖。
共享鎖:select * from table_name where …..lock in share mode
排他鎖:select * from table_name where …..for update

利用select ….for update加入排他鎖

鎖的實現方式:
InnoDB行鎖是通過給索引項加鎖實現的,如果沒有索引,InnoDB會通過隱藏的聚簇索引來對記錄加鎖。
也就是說:如果不通過索引條件檢索數據,那麼InnoDB將對表中所有數據加鎖,實際效果跟表鎖一樣。
行鎖分為三種情形:
Record lock :對索引項加鎖,即鎖定一條記錄。
Gap lock:對索引項之間的『間隙』、對第一條記錄前的間隙或最後一條記錄後的間隙加鎖,即鎖定一個範圍的記錄,不包含記錄本身
Next-key Lock:鎖定一個範圍的記錄並包含記錄本身(上面兩者的結合)。
注意:InnoDB默認級別是repeatable-read級別,所以下面說的都是在RR級別中的。
之前一直搞不懂Gap Lock和Next-key Lock的區別,直到在網上看到一句話豁然開朗,希望對各位有幫助。
Next-Key Lock是行鎖與間隙鎖的組合,這樣,當InnoDB掃描索引記錄的時候,會首先對選中的索引記錄加上行鎖(Record Lock),再對索引記錄兩邊的間隙加上間隙鎖(Gap Lock)。如果一個間隙被事務T1加了鎖,其它事務是不能在這個間隙插入記錄的。
乾巴巴的說沒意思,我們來看看具體實例:
假設我們有一張表:
+—-+——+
| id | age |
+—-+——+
| 1 | 3 |
| 2 | 6 |
| 3 | 9 |
+—-+——+
表結構如下:
CREATE TABLE test
( id
int(11) NOT NULL AUTOINCREMENT, age
int(11) DEFAULT NULL, PRIMARY KEY ( id
), KEY keyname
( age
) ) ENGINE=InnoDB AUTOINCREMENT=302 DEFAULT CHARSET=gbk ;
這樣我們age段的索引就分為
(negative infinity, 3],
(3,6],
(6,9],
(9,positive infinity);
我們來看一下幾種情況:
1、當事務A執行以下語句:
mysql> select * from fenye where age=6for update ;
不僅使用行鎖鎖住了相應的數據行,同時也在兩邊的區間,(5,6]和(6,9] 都加入了gap鎖。
這樣事務B就無法在這個兩個區間insert進新數據,但是事務B可以在兩個區間外的區間插入數據。
2、當事務A執行
select * from fenye where age=7 for update ;
那麼就會給(6,9]這個區間加鎖,別的事務無法在此區間插入或更新數據。
3、如果查詢的數據不再範圍內,
比如事務A執行 select * from fenye where age=100 for update ;
那麼加鎖區間就是(9,positive infinity)。
小結:
行鎖防止別的事務修改或刪除,GAP鎖防止別的事務新增,行鎖和GAP鎖結合形成的的Next-Key鎖共同解決了RR級別在寫數據時的幻讀問題。
何時在InnoDB中使用表鎖:
InnoDB在絕大部分情況會使用行級鎖,因為事務和行鎖往往是我們選擇InnoDB的原因,但是有些情況我們也考慮使用表級鎖。
1、當事務需要更新大部分數據時,表又比較大,如果使用默認的行鎖,不僅效率低,而且還容易造成其他事務長時間等待和鎖衝突。
2、事務比較複雜,很可能引起死鎖導致回滾。
死鎖:
我們說過MyISAM中是不會產生死鎖的,因為MyISAM總是一次性獲得所需的全部鎖,要麼全部滿足,要麼全部等待。而在InnoDB中,鎖是逐步獲得的,就造成了死鎖的可能。
在上面的例子中我們可以看到,當兩個事務都需要獲得對方持有的鎖才能夠繼續完成事務,導致雙方都在等待,產生死鎖。
發生死鎖後,InnoDB一般都可以檢測到,並使一個事務釋放鎖回退,另一個獲取鎖完成事務。
避免死鎖:
有多種方法可以避免死鎖,這裡只介紹常見的三種:
1、如果不同程式會並發存取多個表,盡量約定以相同的順序訪問表,可以大大降低死鎖機會。
2、在同一個事務中,儘可能做到一次鎖定所需要的所有資源,減少死鎖產生概率;
3、對於非常容易產生死鎖的業務部分,可以嘗試使用升級鎖定顆粒度,通過表級鎖定來減少死鎖產生的概率;