為什麼加鎖 你正在讀著你喜歡的女孩遞給你的信,看到一半的時候,她的好閨蜜過來瞄了一眼(假設她會隱身術,你看不到她),她想把“我很喜歡你”改成“我不喜歡你”,剛把“很”字擦掉,“不”字還沒寫完,只寫了一橫一撇,這時候你正讀到這個字,她怕你察覺到也就沒繼續往下寫了,這時候你讀到的這句話就是“我丆喜歡你” ...
為什麼加鎖
你正在讀著你喜歡的女孩遞給你的信,看到一半的時候,她的好閨蜜過來瞄了一眼(假設她會隱身術,你看不到她),她想把“我很喜歡你”改成“我不喜歡你”,剛把“很”字擦掉,“不”字還沒寫完,只寫了一橫一撇,這時候你正讀到這個字,她怕你察覺到也就沒繼續往下寫了,這時候你讀到的這句話就是“我丆喜歡你”,這是什麼鬼?!這位閨蜜樂了:沒錯,確實是鬼在整蠱你呢,嘿嘿!
資料庫也會鬧鬼嗎?很有可能!假設會話1正在讀取表裡的一條記錄(還沒讀取完),另一個會話2突然插隊過來更新表裡的同一條記錄(還沒更新完),那麼會話1拿到的數據就可能是錯誤的(還沒更新完的內容和原內容混在一起,造成亂碼,就像上面的“我丆喜歡你”)。
怎麼避免這種情況呢?加鎖,當有一個人在讀的時候,別人能讀不能寫,當有一個人在寫的時候,別人不能讀和寫。
所以,加鎖是為了在併發操作的時候,能夠確保數據的完整性和一致性。
加鎖的規則
MyISAM鎖的粒度是表級鎖,在執行查詢(SELECT)之前,嘗試在表上面加讀鎖,在執行更新(UPDATE,DELETE,INSERT)之前,嘗試在表上面加寫鎖。
加寫鎖:
如果在表上沒有鎖(讀鎖和寫鎖),在它上面放一個寫鎖。
否則,把鎖定請求放在寫鎖定隊列中。
加讀鎖:
如果在表上沒有寫鎖定,把一個讀鎖定放在它上面。
否則,把鎖定請求放在讀鎖定隊列中。
優先順序:
當一個鎖定被釋放時,鎖定優先被寫鎖定隊列中的線程得到,然後是讀鎖定隊列中的線程。這意味著如果有大量的寫操作,讀操作將會一直等待,直到寫完成。可以通過以下命令看到加鎖的情況:
SHOW STATUS LIKE 'table%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 42 |
| Table_locks_waited | 3 |
+-----------------------+-------+
Table_locks_immediate是加鎖立刻執行成功的次數,Table_locks_waited是造成等待的加鎖次數。另外,可以通過LOW_PRIORITY來改變優先順序。
實例分析
開一個會話視窗1,輸入下麵的語句執行:
CREATE TABLE `users`(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(15) NOT NULL,
PRIMARY KEY (`id`)
)ENGINE=MYISAM DEFAULT CHARSET=utf8 COMMENT='用戶';
INSERT INTO `users` VALUES (null, 'pigfly'),(null,'zhupp');
為了模擬,我們手動執行LOCK TABLES語句把表鎖住:
LOCK TABLES `users` READ LOCAL;
SELECT * FROM `users`;
UPDATE `users` SET name='aa' where id=1;
SELECT正常返回,UPDATE報錯了,原因是當前表加了讀鎖,則當前會話只能執行讀操作,不能執行更新操作。
新開一個會話視窗2:
INSERT INTO `users` VALUES (null, 'zhupp');
UPDATE `users` SET name='xxx' where id=1;
可以看到插入執行成功,但是UPDATE操作被視窗1加的讀鎖阻塞了,我們回到視窗1執行:
UNLOCK TABLES;
這時候視窗2的更新語句馬上返回更新成功了。
為什麼插入不會被讀鎖阻塞呢?原因是當表加了讀鎖並且表不存在空閑塊的時候(刪除或者更新表中間的記錄會導致空閑塊,OPTIMIZE TABLE可以清除空閑塊),MYISAM預設允許其他線程從表尾插入。可以通過改變系統變數concurrent_insert(併發插入)的值來控制併發插入的行為。
SHOW VARIABLES LIKE 'concurrent%';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| concurrent_insert | AUTO |
+-------------------+-------+
Value的值:
- NEVER(0): 不允許併發插入
- AUTO(1): 表裡沒有空行時允許從表尾插入(預設)
- ALWAYS(2): 任何時候都允許併發插入
註意:鎖表的時候加了LOCAL關鍵字表示允許走併發插入的邏輯,具體是否可以併發插入還需要看是否滿足concurrent_insert指定的條件,只有手動鎖表的時候才需要指定LOCAL關鍵字。
測試一下當表裡有空閑塊的情況,視窗1執行:
DELETE FROM `users` WHERE id=1;
LOCK TABLES `users` READ LOCAL;
然後在視窗2執行:
INSERT INTO `users` VALUES (null, 't1');
果然被阻塞了。我們把併發插入的值改成2試試,在視窗1執行:
UNLOCK TABLES;
SET GLOBAL concurrent_insert=2;
DELETE FROM `users` WHERE id=2;
LOCK TABLES `users` READ LOCAL;
然後在視窗2執行:
INSERT INTO `users` VALUES (null, 't2');
SELECT * FROM `users`;
這一次沒有被阻塞,插入成功了。
表級鎖的特點
開銷小、加鎖快、不會產生死鎖,鎖定力度大,發生鎖衝突的概率最高,不適合高併發場景。
性能優化
- 對於併發插入,一般預設配置AUTO就可以了,如果有大量插入操作,可以把concurrent_insert設置為2,然後定期在流量低峰期執行OPTIMIZE TABLE來清除空閑塊。
- 調整優先順序。
- 在大量更新操作前手動鎖表,這樣鎖表只執行了一次,不然每執行一次更新就鎖一次表。
- 存在大量更新操作造成等待,又要兼顧查詢的時候,給max_write_lock_count設置一個低值,在寫鎖達到一定數量時允許執行掛起的讀請求。