MySQL 中的表級鎖很差勁嗎?
當然不是!
其實本來今天是要和大家聊一聊 MyISAM 引擎的,結果在寫的過程中,又發現鎖這個話題可以單獨寫一篇,于是就有了今天的文章。
說到 MyISAM 和 InnoDB 的區別,很多人都知道,區別在于一個是表鎖一個是行鎖,那么小伙伴們有沒有想過,表鎖和行鎖有什么區別?各自又有哪些玩法?今天松哥就來和大家聊聊這個話題。
1.鎖
我們先來大致說一下 MySQL 中的鎖。
當多個事務或者多個進程訪問同一個資源的時候,為了保證數據的一致性,就需要用到 MySQL 鎖機制,從鎖定資源的角度來看,MySQL 中的鎖大致上可以分為三種:
- 表級鎖(table-level locking):表級鎖的特點是開銷小,加鎖快,不會出現死鎖,但是鎖定粒度較大,發生鎖沖突的概率高,而且并發度也低。
- 行級鎖(row-level locking):行級鎖的特點是開銷大,加鎖慢,有可能會出現死鎖,但是它的鎖定粒度小,發生鎖沖突的概率低,并發度也高。
- 頁面鎖:開銷和加鎖時間界于表鎖和行鎖之間,會出現死鎖,鎖定粒度界于表鎖和行鎖之間,并發度一般。
雖然理論上有三種鎖,但是對于在坐的各位小伙伴包括松哥而言,我們日常開發接觸最多的還是前兩種,就是表級鎖和行級鎖。
在 MySQL 中,MyISAM 引擎是表級鎖,而 InnoDB 引擎則支持行級鎖,不過需要注意,其實 InnoDB 也支持表級鎖,只不過默認情況下是行級鎖。
2.表級鎖
MySQL 的表級鎖有兩種模式:
表共享讀鎖(Table Read Lock)。
表獨占寫鎖(Table Write Lock)。
MyISAM 引擎在執行 select 時會自動給相關表加讀鎖,在執行 update、delete 和 insert 時會自動給相關表加寫鎖。
2.1 表共享讀鎖
我們先來看表共享讀鎖,加了共享讀鎖的表,不會阻塞其他 session 的讀請求,但是會阻塞其他 session 的寫請求。
我們來演示一下這個效果。
在下面的案例中,我們會準備兩個窗口,代表兩個 session。
首先我們新建一張表,選擇 MyISAM 作為存儲引擎,DDL 如下:
- CREATE TABLE `user` (
- `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
- `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
添加一條測試數據:
- insert into user(name) values('javaboy');
然后我們在兩個命令行窗口分別登錄 mysql,模擬出兩個 session。
首先在第一個窗口為表添加讀鎖,如下:
- lock table user read;
然后在第二個窗口去讀取數據:
- select * from user;
可以發現,是可以正常讀取的。
然后我們嘗試在第二個窗口寫入一條數據:
- insert into user(name) values('itboyhub');
這條寫入語句會 卡住,如下:
卡住的原因是因為 user 表目前被上了共享讀鎖,這個時候需要我們去到第一個窗口中,解除表的鎖定,這個時候第二個窗口中的這條插入 sql 就可以執行了。如下:
- unlock tables;
當這條 SQL 完畢后,第二個窗口中的插入語句立馬就執行了。
如下是窗口2的截圖:
可以看到,加了共享讀鎖的表,不會阻塞其他 session 的讀(select)請求,但是會阻塞其他 session 的寫(insert、update、delete)請求。
需要注意的是,如果在同一條 SQL 中,同一個表名出線了 N 次,該表就要鎖定 N 次,如下:
思考:
我們在窗口 1 中給 user 表加了鎖,那么在窗口 1 中是否可以對 user 表執行 insert/update/delete 等寫操作呢?評論區 show 出你的答案~
2.2 表獨占寫鎖
這個獨占寫鎖就是大家鎖所熟知的排他鎖,它會阻塞其他進程對同一表的讀寫操作,只有當當前鎖釋放后,才會執行其他進程的讀寫操作。
我們來演示一下這個過程。
還是兩個窗口,首先我們我們在第一個窗口中執行鎖表操作:
- lock table user write;
然后去第二個窗口中做查詢操作,如下:
可以看到,由于是排他鎖,所以查詢操作也被阻塞了。此時需要在窗口 1 中解除表的鎖定,窗口 2 中的查詢操作才會繼續執行下去。
這就是表獨占寫鎖,也就是排他鎖。
在 MyISAM 存儲引擎中,會自動為 SELECT 語句加上共享鎖,為 update/delete/insert 操作加上排他鎖。
2.3 concurrent_insert
前面我們講的是表級鎖的兩種基本模式,在具體的使用過程中,我們還可以通過 concurrent_insert 去配置一些并發行為。
concurrent_insert 有三種不同的取值:
NEVER:加了讀鎖之后,不允許其他 session 并發插入。
AUTO:加了讀鎖之后,如果表里沒有刪除過數據,其他 session 就可以并發插入。
ALWAYS:加了讀鎖之后,允許其他 session 并發插入。
需要注意的是,在 MySQL5.5.3 之前,NEVER、AUTO 以及 ALWAYS 分別使用 0、1、2 代替。
通過 show global variables like '%concurrent_insert%' 命令我們可以查看當前數據庫中 concurrent_insert 的取值,如下:
可以看到,數據庫中默認的 concurrent_insert 取值為 AUTO。有小伙伴可能會說,啥?AUTO?那為啥我在 2.1 小結中,當表加了讀鎖之后,其他 session 無法插入數據呢?這其實跟加鎖方式有關,我們一起來看下。
還是兩個窗口,首先我們在第一個窗口中為表添加讀鎖,如下:
- lock table user read local;
可以看到,最后多了一個 local,這就是關鍵。
接下來我們在窗口 2 中去嘗試讀寫操作,如下:
從圖中可以看到,讀寫操作都可以順利執行。
但是這個時候,如果我們去窗口 1 中執行查詢,如下:
可以看到,這里并看不到窗口 2 中剛剛添加的那條數據,換句話說,窗口 2 中添加的數據對窗口 1 是不可以見的,必須等窗口 1 中的鎖釋放之后,才可以看到窗口 2 中添加的數據。
如下圖,釋放鎖之后,就可以看到另外一個窗口添加進來的數據了:
這是我給大家演示的默認的 concurrent_insert 的行為,大家也可以通過如下 SQL 修改該值:
- set global concurrent_insert = ALWAYS;
2.4 鎖的優先級
在 MyISAM 中,默認情況下,寫鎖的優先級要高,不過開發者也可以自行調整這個默認鎖的優先級。
話說回來,由于 MyISAM 是表鎖,所以不建議用在需要頻繁更新的場景下,否則可能會造成長時間的鎖等待。所以下面的優先級調整,僅僅作為技術層面的探討。
修改 SQL 優先級
首先我們可以在執行 SQL 的時候,順便修改其優先級:
例如執行 select 的時候可以使用 HIGH_PRIORITY 來提高該語句的優先級,如下:
在執行 delete/update/insert 等操作的時候,可以使用 LOW_PRIORITY 來降低其優先級,以便讓讀取操作先執行:
當然我們也可以通過如下 SQL 讓所有支持 LOW_PRIORITY 選項的語句都默認地按照低優先級來處理。
- set LOW_PRIORITY_UPDATES = 1
修改寫鎖上限
我們可以修改 MAX_WRITE_LOCK_COUNT 的值,該變量默認值如下圖:
這個值表示當一個表的寫鎖數量達到給定的值后,就降低寫鎖的優先級,讓讀鎖有機會執行。如果有需要,我們可以自行調整這個值,調整方式如下:
- set GLOBAL MAX_WRITE_LOCK_COUNT=1024;
3.行級鎖
行級鎖松哥留到講 InnoDB 的時候再和大家聊,今天我們就先扯這么多~
參考資料:
1.https://database.51cto.com/art/201910/604421.htm
2.https://zhuanlan.zhihu.com/p/123962424
本文轉載自微信公眾號「江南一點雨」,可以通過以下二維碼關注。轉載本文請聯系江南一點雨公眾號。