存在并發(fā)操作的時候喜最,必然需要一種機制來保證數(shù)據(jù)的完整性和一致性偎蘸。鎖就是這樣一種技術實現(xiàn)。
當多個用戶并發(fā)地存取數(shù)據(jù)時,數(shù)據(jù)庫會存在多個事務同時存取同一數(shù)據(jù)的情況迷雪。如果對此不加控制限书,就很有可能會讀取和存儲不正確的數(shù)據(jù),破壞數(shù)據(jù)庫的完整性和一致性章咧。當事務在對某個數(shù)據(jù)進行操作前倦西,先向系統(tǒng)發(fā)出請求,對其加鎖赁严。加鎖后事務就對數(shù)據(jù)對象有了一定的控制扰柠。
在MySQL中存在多種鎖,根據(jù)概念疼约,可分為悲觀鎖和樂觀鎖卤档。
悲觀鎖,也叫悲觀并發(fā)控制程剥,顧名思義就是對事務操作很悲觀劝枣,認為每次拿數(shù)據(jù)都會對數(shù)據(jù)進行修改,因此每次讀取數(shù)據(jù)都會加鎖倡缠。當事務A對某行數(shù)據(jù)應用了鎖哨免,只有當事務A把鎖釋放以后,其他事務才能執(zhí)行與該鎖沖突的操作昙沦,這里的事務A施加的鎖就叫悲觀鎖琢唾。
樂觀鎖,也叫樂觀并發(fā)控制盾饮,顧名思義就是對事務操作比較樂觀采桃,認為多用戶并發(fā)的事務在處理時不會互相影響,各事務能夠在不產(chǎn)生鎖的情況下處理各自影響的那部分數(shù)據(jù)丘损。在提交數(shù)據(jù)更新前普办,每個事務會先檢查在該事務讀取數(shù)據(jù)后,有沒有其他事務又修改了數(shù)據(jù)徘钥。如果有衔蹲,那么當前正在提交的事務會進行回滾。
在MySQL數(shù)據(jù)庫中呈础,悲觀鎖依靠數(shù)據(jù)庫提供的鎖機制來實現(xiàn)的舆驶,樂觀鎖需要使用者自己去實現(xiàn)。
從鎖的功能上看而钞,可以分為共享鎖沙廉、排他鎖(獨占鎖)、意向鎖等臼节。共享鎖也稱S鎖撬陵,排他鎖也被稱為X鎖珊皿。
從鎖的粒度來看,又可以分為表鎖巨税、頁鎖蟋定、行鎖。在MySQL中垢夹,最常見的是表鎖和行鎖溢吻。其中MyISAM引擎只有表鎖维费,而InnoDB既有表鎖也有行鎖果元。
表鎖的特點是開銷小,加鎖塊犀盟,不會出現(xiàn)死鎖而晒,鎖定粒度大,發(fā)生鎖沖突的概率最高阅畴,并發(fā)度最低倡怎。相反,行鎖的加鎖開銷比較大贱枣,發(fā)生沖突的概率比較低监署,但是并發(fā)度也高,帶來的副反應就是死鎖纽哥。而頁鎖則界于表鎖和行鎖之間钠乏。
1. MySQL表鎖
MySQL的表鎖有兩種模式:表共享鎖和表獨占鎖。
- 對于MyISAM的讀操作春塌,不會阻塞其他用戶對同一表的讀請求晓避,但會阻塞對同一表的寫請求;
- 對于MyISAM的寫操作只壳,則會阻塞其他用戶對于同一表的讀和寫請求俏拱;
- MyISAM的讀和寫操作之間,以及寫和寫操作之間是串行的吼句。
如何加表鎖
MyISAM表在執(zhí)行查語句之前锅必,會自動給涉及的所有表加讀鎖,在執(zhí)行更新操作(增刪改)前惕艳,會自動給涉及的表加寫鎖搞隐,不需要用戶顯式加鎖。
給MyISAM表顯式加鎖尔艇,一般是為了一定程度模擬事務操作尔许,實現(xiàn)對某一時間點多個表的一致性讀取。比如一個訂單表order有total字段终娃,有訂單明細表order_detail記錄一個訂單多個產(chǎn)品的金額小計money味廊。如果你要判斷這兩個表的金額是否相等,可能需要分別查詢兩個表的金額:
SELECT SUM(`money`) FROM `order_detail`;
SELECT SUM(`total`) FROM `order`;
如果不加鎖,在執(zhí)行一個表的查詢過程中余佛,order_detail表可能已經(jīng)發(fā)生了改變柠新,因此正確的做法是:
LOCK TABLES `order` READ LOCAL,`order_detail` READ LOCAL;
SELECT SUM(`money`) FROM `order_detail`;
SELECT SUM(`total`) FROM `order`;
UNLOCK TABLES;
需要說明的是,在鎖表的同時加了local選項辉巡,其作用就是在滿足MySQL表并發(fā)插入條件的情況下恨憎,允許其他用戶在表尾插入數(shù)據(jù)。
在用LOCK TABLES給表顯示加鎖時郊楣,必須同時取得所有涉及表的鎖憔恳,在執(zhí)行LOCK TABLES后,當前會話只能訪問顯式加鎖的這些表净蚤,不能訪問未加鎖的表钥组;同時,如果加的是讀鎖今瀑,那么只能執(zhí)行查詢操作程梦,而不能操作更新操作,非當前會話可以執(zhí)行UPDATE操作橘荠,但會被阻塞屿附,可以在滿足一定條件時執(zhí)行INSERT操作,不受阻塞哥童。被加鎖的會話不會讀取到其他會話的INSERT數(shù)據(jù)挺份,自動加鎖的情況下也基本如此,這也是MyISAM表不會出現(xiàn)死鎖的原因如蚜。
MyISAM存儲引擎有一個系統(tǒng)變量concurrent_insert压恒,專門用以控制并發(fā)插入行為,因此上述提到的“滿足一定條件”就是:
- concurrent_insert設置為0時错邦,不允許并發(fā)插入探赫。
- concurrent_insert設置為1時,如果MyISAM允許線程讀表的同時撬呢,另一個線程從表尾插入記錄伦吠。這是默認設置。
- concurrent_insert設置為2時魂拦,無論MyISAM有沒有空間毛仪,都允許在表尾插入記錄。
無論是讀鎖還是寫鎖芯勘,兩者的機制和上面提到的差不多箱靴。前面提到,MyISAM表的讀鎖和寫鎖是互斥的荷愕,讀之間是串行的衡怀。
那么如果一個進程請求某個MyISAM表既有讀鎖又有寫鎖時棍矛,MyISAM的默認設置是寫優(yōu)先,可以修改LOW_PRIORITY_UPDATES參數(shù)抛杨,也可以在INSERT够委、UPDATE、DELETE語句中指定LOW_PRIORITY選項來調(diào)節(jié)讀寫鎖的競爭怖现。
另外茁帽,MySQL還提供了一種折中的辦法來調(diào)節(jié)讀寫沖突,即給系統(tǒng)參數(shù)max_write_lock_count設置一個合適的值屈嗤,當一個表的讀鎖達到這個值后潘拨,MySQL暫時將寫請求的優(yōu)先級降低,給讀進程一定獲得鎖的機會恢共。
2. MySQL行鎖
MySQL行鎖主要存在于InnoDB引擎中战秋。InnoDB主要實現(xiàn)了以下兩種類型的行鎖:
- 共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同數(shù)據(jù)集的排他鎖讨韭。
- 排它鎖(X):允許獲得排他鎖的事務更新數(shù)據(jù),阻塞其他事務取得相同數(shù)據(jù)集的共享讀鎖和排他寫鎖癣蟋。
InnoDB還存在一種意向鎖透硝,分為意向共享鎖和意向排他鎖,這兩種鎖都由系統(tǒng)自動添加和自動釋放疯搅,無需人工干預濒生。
依然使用上面的兩張表,把引擎修改為InnoDB幔欧。注意:使用悲觀鎖時罪治,必須關閉MySQL默認開啟的自動提交模式。默認情況下礁蔗,當你執(zhí)行更新操作后觉义,MySQL會立刻將結果進行提交。
讀共享鎖:
SET autocommit = 0;
SELECT `money` FROM `order_detail` WHERE `id` = 2 LOCK IN SHARE MODE;
UPDATE `order_detail` SET `money` = 14 WHERE `id` = 2;
SELECT `money` FROM order_detail WHERE `id` = 2;
COMMIT;
寫排他鎖(獨占鎖):
SET autocommit = 0;
SELECT `money` FROM `order_detail` WHERE `id` = 2 FOR UPDATE;
UPDATE `order_detail` SET `money` = 14 WHERE `id` = 2;
如果事務A先獲得了某行的寫共享鎖浴井,事務B可以獲得該記錄晒骇,但不能對數(shù)據(jù)加寫鎖(可以加S鎖),必須等待事務A提交或回滾后才可以加X鎖磺浙。
需要注意的是洪囤,InnoDB并不總是給數(shù)據(jù)加行鎖,有時候也會加表鎖撕氧。原因在于瘤缩,InnoDB行鎖是通過給索引上的索引項加鎖來實現(xiàn)的。這也意味著只有通過索引條件檢索數(shù)據(jù)伦泥,InnoDB才使用行鎖剥啤,否則將使用表鎖何暮。另外要注意,因為MySQL的行鎖是針對索引加的鎖铐殃,所以雖然可能訪問的是不同的行記錄海洼,但是如果條件中用了相同的索引建,還是會造成鎖沖突富腊。比如order_detail表的order_id是加了索引的坏逢,下面查詢了兩條訂單明細,會出現(xiàn)鎖沖突:
select * from order_detail where order_id = 1 and good_id = 1;
select * from order_detail where order_id = 1 and good_id = 2;
此外赘被,還存在一種“間隙鎖”的概念是整。當我們使用范圍查詢而非相等條件查詢,并請求共享或排他鎖時民假,InnoDB會自動對符合條件的已有數(shù)據(jù)記錄的索引項加鎖浮入,對于鍵值在條件范圍內(nèi)但并不存在的記錄,我們稱之為“間隙”(GAP)羊异,InnoDB也會對這個間隙加鎖事秀,這種機制就是所謂的“間隙鎖”。
InnoDB表大部分情況下都應該使用行鎖野舶,因為事務和行鎖往往就是我們選擇InnoDB表的理由易迹。但個別情況下,我們也可以考慮使用表鎖:
- InnoDB使用表鎖的情況一:事務需要更新大部分或全部數(shù)據(jù)平道,表又比較大睹欲,如果使用默認的行鎖,效率比較低不說一屋,還可能造成其他事務長時間的等待和鎖沖突窘疮,建議用表鎖提高事務執(zhí)行速度。
- InnoDB使用表鎖的情況二:事務涉及多個表冀墨,比較復雜闸衫,很可能引起死鎖,造成大量事務回滾轧苫。這種情況可以考慮使用表鎖以避免死鎖楚堤、減少數(shù)據(jù)庫因事務回滾等帶來的開銷。
InnoDB下使用表鎖需要注意以下兩點含懊。
- 使用LOCK TABLES雖然也可以加表鎖身冬,但由于表鎖并不是由InnoDB引擎管理,而是由MySQL Server負責的岔乔,因此僅當autocommit = 0酥筝、innodb_table_locks = 1(默認設置)時,InnoDB層才能知道MySQL加的表鎖雏门,MySQL Server也才能感知InnoDB加的行鎖嘿歌。這種情況下顶岸,InnoDB才能自動識別涉及表級鎖的死鎖益眉,否則InnoDB無法自動檢測并處理死鎖。
- 在LOCK TABLES對InnoDB表加鎖時要注意將AUTOCOMMIT設為0,否則MySQL不會給表加鎖端逼;在事務結束前笔喉,不要用UNLOCK TABLES釋放表鎖堡僻,因為UNLOCK TABLES會隱式地提交事務观谦;COMMIT或ROLLBACK并不能釋放用LOCK TABLES加的表鎖,必須要用UNLOCK TABLES釋放表鎖靴患。
正確寫法舉例仍侥,寫表t1并從表t2讀:
SET AUTOCOMMIT = 0;
LOCK TABLES t1 WRITE,t2 READ;
-- todo ... [do sth with t1 and t2 here];
COMMIT;
UNLOCK TABLES;