MySQL存儲引擎 -- MyISAM(表鎖定) 與 InnoDB(行鎖定) 鎖定機制
前言
為了保證數據的一致完整性,任何一個數據庫都存在鎖定機制。鎖定機制的優劣直接應想到一個數據庫系統的並發處理能力和性能,所以鎖定機制的實現也就成為了各種數據庫的核心技術之一。本章將對MySQL中兩種使用最為頻繁的存儲引擎MyISAM(表鎖定)和Innodb(行鎖定)各自的鎖定機制進行較為詳細的分析。
MySQL鎖定機制簡介
數據庫鎖定機制簡單來說就是數據庫為了保證數據的一致性而使各種共享資源在被並發訪問訪問變得有序所設計的一種規則。對於任何一種數據庫來說都需要有相應的鎖定機制,所以MySQL自然也不能例外。MySQL數據庫由於其自身架構的特點,存在多種數據存儲引擎,每種存儲引擎所針對的應用場景特點都不太一樣,為了滿足各自特定應用場景的需求,每種存儲引擎的鎖定機制都是為各自所面對的特定場景而優化設計,所以各存儲引擎的鎖定機制也有較大區別。
總的來說,MySQL各存儲引擎使用了兩種類型(級別)的鎖定機制:表鎖定,行鎖定。下面我們先分析一下MySQL這兩種鎖定的特點和各自的優劣所在。
表級鎖定(table-level)
和行級鎖定相反,表級別的鎖定是MySQL各存儲引擎中最大顆粒度的鎖定機制。該鎖定機制最大的特點是實現邏輯非常簡單,帶來的系統負面影響最小。所以獲取鎖和釋放鎖的速度很快。由於表級鎖一次會將整個表鎖定,所以可以很好的避免困擾我們的死鎖問題。
當然,鎖定顆粒度大所帶來最大的負面影響就是出現鎖定資源爭用的概率也會最高,致使並大度大打折扣。
行級鎖定(row-level)
行級鎖定最大的特點就是鎖定對象的顆粒度很小,也是目前各大數據庫管理軟件所實現的鎖定顆粒度最小的。由於鎖定顆粒度很小,所以發生鎖定資源爭用的概率也最小,能夠給予應用程序盡可能大的並發處理能力而提高一些需要高並發應用系統的整體性能。
雖然能夠在並發處理能力上面有較大的優勢,但是行級鎖定也因此帶來了不少弊端。由於鎖定資源的顆粒度很小,所以每次獲取鎖和釋放鎖需要做的事情也更多,帶來的消耗自然也就更大了。此外,行級鎖定也最容易發生死鎖。
一、表級鎖定
MySQL的表級鎖定主要分為兩種類型,一種是讀鎖定,另一種是寫鎖定。在MySQL中,主要通過四個隊列來維護這兩種鎖定:兩個存放當前正在鎖定中的讀和寫鎖定信息,另外兩個存放等待中的讀寫鎖定信息,如下:
Current read-lock queue (lock->read)
Pending read-lock queue (lock->read_wait)
Current write-lock queue (lock->write)
Pending write-lock queue (lock->write_wait)
當前持有讀鎖的所有線程的相關信息都能夠在Currentread-lockqueue中找到,隊列中的信息按照獲取到鎖的時間依序存放。而正在等待鎖定資源的信息則存放在Pendingread-lockqueue裏面,另外兩個存放寫鎖信息的隊列也按照上面相同規則來存放信息。
雖然對於我們這些使用者來說MySQL展現出來的鎖定(表鎖定)只有讀鎖定和寫鎖定這兩種類型,但是在MySQL內部實現中卻有多達11種鎖定類型,由系統中一個枚舉量(thr_lock_type)定義,各值描述如下:
鎖定類型 |
說明 |
IGNORE |
當發生鎖請求的時候內部交互使用,在鎖定結構和隊列中並不會有任何信息存儲 |
UNLOCK |
釋放鎖定請求的交互用所類型 |
READ |
普通讀鎖定 |
WRITE |
普通寫鎖定 |
READ_WITH_SHARED_LOCKS |
在Innodb中使用到,由如下方式產生如:SELECT...LOCKINSHAREMODE |
READ_HIGH_PRIORITY |
高優先級讀鎖定 |
READ_NO_INSERT |
不允許ConcurentInsert的鎖定 |
WRITE_ALLOW_WRITE |
這個類型實際上就是當由存儲引擎自行處理鎖定的時候,mysqld允許其他的線程再獲取讀或者寫鎖定,因為即使資源沖突,存儲引擎自己也會知道怎麽來處理 |
WRITE_ALLOW_READ |
這種鎖定發生在對表做DDL(ALTERTABLE...)的時候,MySQL可以允許其他線程獲取讀鎖定,因為MySQL是通過重建整個表然後再RENAME而實現的該功能,所在整個過程原表仍然可以提供讀服務 |
WRITE_CONCURRENT_INSERT |
正在進行ConcurentInsert時候所使用的鎖定方式,該鎖定進行的時候,除了READ_NO_INSERT之外的其他任何讀鎖定請求都不會被阻塞 |
WRITE_DELAYED |
在使用INSERTDELAYED時候的鎖定類型 |
WRITE_LOW_PRIORITY |
顯示聲明的低級別鎖定方式,通過設置LOW_PRIORITY_UPDAT=1而產生 |
WRITE_ONLY |
當在操作過程中某個鎖定異常中斷之後系統內部需要進行CLOSETABLE操作,在這個過程中出現的鎖定類型就是WRITE_ONLY |
讀鎖定
一個新的客戶端請求在申請獲取讀鎖定資源的時候,需要滿足兩個條件:
1、請求鎖定的資源當前沒有被寫鎖定;
2、寫鎖定等待隊列(Pendingwrite-lockqueue)中沒有更高優先級的寫鎖定等待;
如果滿足了上面兩個條件之後,該請求會被立即通過,並將相關的信息存入Currentread-lockqueue中,而如果上面兩個條件中任何一個沒有滿足,都會被迫進入等待隊列Pendingread-lockqueue中等待資源的釋放。
寫鎖定
當客戶端請求寫鎖定的時候,MySQL首先檢查在Currentwrite-lockqueue是否已經有鎖定相同資源的信息存在。
如果Currentwrite-lockqueue沒有,則再檢查Pendingwrite-lockqueue,如果在Pendingwrite-lockqueue中找到了,自己也需要進入等待隊列並暫停自身線程等待鎖定資源。反之,如果Pendingwrite-lockqueue為空,則再檢測Currentread-lockqueue,如果有鎖定存在,則同樣需要進入Pendingwrite-lockqueue等待。當然,也可能遇到以下這兩種特殊情況:
1. 請求鎖定的類型為WRITE_DELAYED;
2. 請求鎖定的類型為WRITE_CONCURRENT_INSERT或者是TL_WRITE_ALLOW_WRITE,同時Currentreadlock是READ_NO_INSERT的鎖定類型。
當遇到這兩種特殊情況的時候,寫鎖定會立即獲得而進入Current write-lock queue 中。
如果剛開始第一次檢測就Currentwrite-lockqueue中已經存在了鎖定相同資源的寫鎖定存在,那麽就只能進入等待隊列等待相應資源鎖定的釋放了。
讀請求和寫等待隊列中的寫鎖請求的優先級規則主要為以下規則決定:
1. 除了READ_HIGH_PRIORITY的讀鎖定之外,Pendingwrite-lockqueue中的WRITE寫鎖定能夠阻塞所有其他的讀鎖定;
2. READ_HIGH_PRIORITY讀鎖定的請求能夠阻塞所有Pendingwrite-lockqueue中的寫鎖定;
3. 除了WRITE寫鎖定之外,Pendingwrite-lockqueue中的其他任何寫鎖定都比讀鎖定的優先級低。
寫鎖定出現在Currentwrite-lockqueue之後,會阻塞除了以下情況下的所有其他鎖定的請求:
1. 在某些存儲引擎的允許下,可以允許一個WRITE_CONCURRENT_INSERT寫鎖定請求
2. 寫鎖定為WRITE_ALLOW_WRITE的時候,允許除了WRITE_ONLY之外的所有讀和寫鎖定請求
3. 寫鎖定為WRITE_ALLOW_READ的時候,允許除了READ_NO_INSERT之外的所有讀鎖定請求
4. 寫鎖定為WRITE_DELAYED的時候,允許除了READ_NO_INSERT之外的所有讀鎖定請求
5. 寫鎖定為WRITE_CONCURRENT_INSERT的時候,允許除了READ_NO_INSERT之外的所有讀鎖定請求
由於MyISAM存儲引擎使用的鎖定機制完全是由MySQL提供的表級鎖定實現,所以下面我們將以MyISAM存儲引擎作為示例存儲引擎,來實例演示表級鎖定的一些基本特性。由於,為了讓示例更加直觀,我將使用顯示給表加鎖來演示:RITE_ALLOW_READ 類型的寫鎖定。
Session a |
Session b |
|
行鎖定基本演示 |
||
1 |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> update test_innodb_lock set b = ‘b1‘ where a = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 更新,但是不提交 |
||
2 |
mysql> update test_innodb_lock set b = ‘b1‘ where a = 1; 被阻塞,等待 |
|
3 |
mysql> commit; Query OK, 0 rows affected (0.05 sec) 提交 |
|
4 |
mysql> update test_innodb_lock set b = ‘b1‘ where a = 1; Query OK, 0 rows affected (36.14 sec) Rows matched: 1 Changed: 0 Warnings: 0 解除阻塞,更新正常進行 |
|
無索引升級為表鎖演示 |
||
5 |
mysql> update test_innodb_lock set b = ‘2‘ where b = 2000; Query OK, 1 row affected (0.02 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
6 |
mysql> update test_innodb_lock set b = ‘3‘ where b = 3000; 被阻塞,等待 |
|
7 |
mysql> commit; Query OK, 0 rows affected (0.10 sec)提交 |
|
8 |
mysql> update test_innodb_lock set b = ‘3‘ where b = 3000; Query OK, 1 row affected (1 min 3.41 sec) Rows matched: 1 Changed: 1 Warnings: 0 阻塞解除,完成更新 |
|
間隙鎖帶來的插入問題演示 |
||
9 |
mysql> select * from test_innodb_lock; +------+------+ | a | b |+------+------+ | 1 | b2 | | 3 | 3 | | 4 | 4000 | | 5 | 5000 | | 6 | 6000 | | 7 | 7000 | | 8 | 8000 | | 9 | 9000 | | 1 | b1 | +------+------+ 9 rows in set (0.00 sec) mysql> update test_innodb_lock set b = a * 100 where a < 4 and a > 1; Query OK, 1 row affected (0.02 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
10 |
mysql> insert into test_innodb_lock values(2,‘200‘); 被阻塞,等待 |
|
11 |
mysql> commit; Query OK, 0 rows affected (0.02 sec) |
|
12 |
mysql> insert into test_innodb_lock values(2,‘200‘); Query OK, 1 row affected (38.68 sec) 阻塞解除,完成插入 |
|
使用共同索引不同數據的阻塞示例 |
||
13 |
mysql> update test_innodb_lock set b = ‘bbbbb‘ where a = 1 and b =‘b2‘; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
14 |
mysql> update test_innodb_lock set b = ‘bbbbb‘ where a = 1 and b = ‘b1‘; 被阻塞 |
|
15 |
mysql> commit; Query OK, 0 rows affected (0.02 sec) |
|
16 |
mysql> update test_innodb_lock set b = ‘bbbbb‘ where a = 1 and b = ‘b1‘; Query OK, 1 row affected (42.89 sec) Rows matched: 1 Changed: 1 Warnings: 0 session 提交事務,阻塞去除,更新完成 |
|
死鎖示例 |
||
17 |
mysql> update t1 set id = 110 where id = 11; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 |
|
18 |
mysql> update t2 set id = 210 where id = 21; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
19 |
mysql>update t2 set id=2100 where id=21; 等待sessionb釋放資源,被阻塞 |
|
20 |
mysql>update t1 set id=1100 where id=11; Query OK,0 rows affected (0.39sec) Rows matched: 0 Changed: 0 Warnings:0 等待sessiona釋放資源,被阻塞 |
|
兩個 session 互相等等待對方的資源釋放之後才能釋放自己的資源,造成了死鎖。
|
三、行級鎖定
行級鎖定不是MySQL自己實現的鎖定方式,而是由其他存儲引擎自己所實現的,如廣為大家所知的Innodb存儲引擎,以及MySQL的分布式存儲引擎NDBCluster等都是實現了行級鎖定。
Innodb 鎖定模式及實現機制
Innodb的行級鎖定同樣分為兩種類型,【共享鎖】和【排他鎖】,而在鎖定機制的實現過程中為了讓行級鎖定和表級鎖定共存,Innodb也同樣使用了意向鎖(表級鎖定)的概念,也就有了意向共享鎖和意向排他鎖這兩種。
當一個事務需要給自己需要的某個資源加鎖的時候,如果遇到一個共享鎖正鎖定著自己需要的資源的時候,自己可以再加一個共享鎖,不過不能加排他鎖。但是,如果遇到自己需要鎖定的資源已經被一個排他鎖占有之後,則只能等待該鎖定釋放資源之後自己才能獲取鎖定資源並添加自己的鎖定。而意向鎖的作用就是當一個事務在需要獲取資源鎖定的時候,如果遇到自己需要的資源已經被排他鎖占用的時候,該事務可以需要鎖定行的表上面添加一個合適的意向鎖。如果自己需要一個共享鎖,那麽就在表上面添加一個意向共享鎖。而如果自己需要的是某行(或者某些行)上面添加一個排他鎖的話,則先在表上面添加一個意向排他鎖。意向共享鎖可以同時並存多個,但是意向排他鎖同時只能有一個存在。所以,可以說Innodb的鎖定模式實際上可以分為四種:共享鎖(S),排他鎖(X),意向共享鎖(IS)和意向排他鎖(IX),我們可以通過以下表格來總結上面這四種所的共存邏輯關系:
共享鎖(S) |
排他鎖(X) |
意向共享鎖(IS) |
意向排他鎖(IX) |
|
共享鎖(S) |
兼容 |
沖突 |
兼容 |
沖突 |
排他鎖(X) |
沖突 |
沖突 |
沖突 |
沖突 |
意向共享鎖(IS) |
兼容 |
沖突 |
兼容 |
兼容 |
意向排他鎖(IX) |
沖突 |
沖突 |
兼容 |
兼容 |
Innodb的鎖定則是通過在指向數據記錄的第一個索引鍵之前和最後一個索引鍵之後的空域空間上標記鎖定信息而實現的。Innodb的這種鎖定實現方式被稱為“NEXT-KEYlocking”(間隙鎖),因為Query執行過程中通過過範圍查找的話,他會鎖定整個範圍內所有的索引鍵值,即使這個鍵值並不存在。
間隙鎖有一個比較致命的弱點,就是當鎖定一個範圍鍵值之後,即使某些不存在的鍵值也會被無辜的鎖定,而造成在鎖定的時候無法插入鎖定鍵值範圍內的任何數據。在某些場景下這可能會對性能造成很大的危害。而Innodb給出的解釋是為了組織幻讀的出現,所以他們選擇的間隙鎖來實現鎖定。
除了間隙鎖給Innodb帶來性能的負面影響之外,通過索引實現鎖定的方式還存在其他幾個較大的性能隱患:
當Query無法利用索引的時候,Innodb會放棄使用行級別鎖定而改用表級別的鎖定,造成並發性能的降低;
當Quuery使用的索引並不包含所有過濾條件的時候,數據檢索使用到的索引鍵所只想的數據可能有部分並不屬於該Query的結果集的行列,但是也會被鎖定,因為間隙鎖鎖定的是一個範圍,而不是具體的索引鍵;
當Query在使用索引定位數據的時候,如果使用的索引鍵一樣但訪問的數據行不同的時候(索引只是過濾條件的一部分),一樣會被鎖定。
Innodb中監測死鎖
在Innodb的事務管理和鎖定機制中,有專門檢測死鎖的機制,會在系統中產生死鎖之後的很短時間內就檢測到該死鎖的存在。當Innodb檢測到系統中產生了死鎖之後,Innodb會通過相應的判斷來選這產生死鎖的兩個事務中較小的事務來回滾,而讓另外一個較大的事務成功完成。那Innodb是以什麽來為標準判定事務的大小的呢?MySQL官方手冊中也提到了這個問題,實際上在Innodb發現死鎖之後,會計算出兩個事務各自插入、更新或者刪除的數據量來判定兩個事務的大小。也就是說哪個事務所改變的記錄條數越多,在死鎖中就越不會被回滾掉。但是有一點需要註意的就是,當產生死鎖的場景中涉及到不止Innodb存儲引擎的時候,Innodb是沒辦法檢測到該死鎖的,這時候就只能通過鎖定超時限制來解決該死鎖了。
我們已經介紹過,行級鎖定肯定會帶來死鎖問題,Innodb也不可能例外。至於死鎖的產生過程我們就不在這裏詳細描述了,在後面的鎖定示例中會通過一個實際的例子為大家展示死鎖的產生過程。
Innodb 鎖定機制示例
代碼如下:
mysql> create table test_innodb_lock (a int(11),b varchar(16)) engine=innodb;
Query OK, 0 rows affected (0.02 sec)
mysql> create index test_innodb_a_ind on test_innodb_lock(a);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> create index test_innodb_lock_b_ind on test_innodb_lock(b);
Query OK, 11 rows affected (0.01 sec)
Records: 11 Duplicates: 0 Warnings: 0
時刻 |
Session a |
Session b |
行鎖定基本演示 |
||
1 |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) |
mysql> update test_innodb_lock set b = ‘b1‘ where a = 1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 更新,但是不提交 |
||
2 |
mysql> update test_innodb_lock set b = ‘b1‘ where a = 1; 被阻塞,等待 |
|
3 |
mysql> commit; Query OK, 0 rows affected (0.05 sec) 提交 |
|
4 |
mysql> update test_innodb_lock set b = ‘b1‘ where a = 1; Query OK, 0 rows affected (36.14 sec) Rows matched: 1 Changed: 0 Warnings: 0 解除阻塞,更新正常進行 |
|
無索引升級為表鎖演示 |
||
5 |
mysql> update test_innodb_lock set b = ‘2‘ where b = 2000; Query OK, 1 row affected (0.02 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
mysql> update test_innodb_lock set b = ‘3‘ where b = 3000; 被阻塞,等待 |
6 |
||
7 |
mysql> commit; Query OK, 0 rows affected (0.10 sec) |
|
8 |
mysql> update test_innodb_lock set b = ‘3‘ where b = 3000; Query OK, 1 row affected (1 min 3.41 sec) Rows matched: 1 Changed: 1 Warnings: 0 阻塞解除,完成更新 |
|
間隙鎖帶來的插入問題演示 |
||
9 |
mysql> select * from test_innodb_lock; +------+------+ | a | b |+------+------+ | 1 | b2 | | 3 | 3 | | 4 | 4000 | | 5 | 5000 | | 6 | 6000 | | 7 | 7000 | | 8 | 8000 | | 9 | 9000 | | 1 | b1 | +------+------+ 9 rows in set (0.00 sec) mysql> update test_innodb_lock set b = a * 100 where a < 4 and a > 1; Query OK, 1 row affected (0.02 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
10 |
mysql> insert into test_innodb_lock values(2,‘200‘); 被阻塞,等待 |
|
11 |
mysql> commit; Query OK, 0 rows affected (0.02 sec) |
|
12 |
mysql> insert into test_innodb_lock values(2,‘200‘); Query OK, 1 row affected (38.68 sec) 阻塞解除,完成插入 |
|
使用共同索引不同數據的阻塞示例 |
||
13 |
mysql> update test_innodb_lock set b = ‘bbbbb‘ where a = 1 and b = ‘b2‘; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
14 |
mysql> update test_innodb_lock set b = ‘bbbbb‘ where a = 1 and b = ‘b1‘; 被阻塞 |
|
15 |
mysql> commit; Query OK, 0 rows affected (0.02 sec) |
|
16 |
mysql> update test_innodb_lock set b = ‘bbbbb‘ where a = 1 and b = ‘b1‘; Query OK, 1 row affected (42.89 sec) Rows matched: 1 Changed: 1 Warnings: 0 session 提交事務,阻塞去除,更新完成 |
|
死鎖示例 |
||
17 |
mysql> update t1 set id = 110 where id = 11; Query OK, 0 rows affected (0.00 sec) Rows matched: 0 Changed: 0 Warnings: 0 |
|
18 |
mysql> update t2 set id = 210 where id = 21; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 |
|
19 |
mysql>update t2 set id=2100 where id=21; 等待sessionb釋放資源,被阻塞 |
|
20 |
mysql>update t1 set id=1100 where id=11; Query OK,0 rows affected (0.39sec) Rows matched: 0 Changed: 0 Warnings:0 等待sessiona釋放資源,被阻塞 |
|
兩個 session 互相等等待對方的資源釋放之後才能釋放自己的資源,造成了死鎖
|
MySQL存儲引擎 -- MyISAM(表鎖定) 與 InnoDB(行鎖定) 鎖定機制