MYSQL 鎖機制
數據庫在對資源進行高并發的讀寫操作時,為了保證數據的一致性,有效性,鎖是很重要的機制。Mysql的鎖分為三個級別:行級鎖,頁級鎖,表級鎖。對于平時常用的存儲引擎,MyISAM采用的是表級鎖,InnoDB采用的是行級鎖加表級鎖,而支持頁級鎖的BDB引擎已經逐漸被InnoDB替代了,這里暫不討論。
表級鎖的開銷小,加鎖快,不會出現死鎖,鎖定粒度大,大概率發生鎖的沖突,并發度低
行級鎖的開銷大,加鎖滿,會出現死鎖,鎖定粒度小,小概率發生鎖的重讀,并發度高
上述特點來看,很難說哪種鎖更好,只能相對于所處的業務場景來選擇更加適合的鎖機制。如果僅從鎖的角度來看,表級鎖更適合以查詢為主的應用場景,而行級鎖則更適合于大量按索引條件并發更新少量數據的應用場景。
MyISAM表
MyISAM存儲引擎只支持表級鎖,鎖的模式有共享鎖和排他鎖。共享鎖是他人可以讀但不能寫,排它鎖則會阻塞他人的讀寫操作。MyISAM的讀寫之間,以及寫寫之間是串行的。
MyISAM在執行SQL語句時,會自動為SELECT語句加上共享鎖,為UDI操作加上排它鎖。MYSQL不支持鎖升級,如果涉及到更新操作,需要在一開始就加上排它鎖。
MyISAM的并發插入
在存儲引擎中有一個系統變量concurrent_insert,專門控制其并發插入的行為
concurrent_insert=0時,不允許并發插入
concurrent_insert=1時,如果MyISAM表中沒有空洞(即表的中間沒有被刪除的行),其允許在一個進程讀表的同事,另一個進程從表尾插入記錄,這也是MySQL的默認設置
concurrent_insert=2時,如果MyISAM表中沒有空洞,允許在表尾并發插入記錄
MyISAM的鎖調度
在MyISAM存儲引擎中,寫的重要性要大于讀,所以在操作隊列中,即使寫的操作在讀的操作之后,也會讓寫先拿到排它鎖,這也正是MyISAM不適合于大量寫入操作的應用場景的原因,這樣可能會導致讀操作永遠在阻塞中,永遠在等待寫操作的釋放鎖。當然,除了默認的設置,可以通過設置語句的優先級別來管理這個執行順序
InnoDB
他與MyISAM的最大區別有兩個方面,一個是支持事務,另一個是采用了行級鎖
事務的并發處理會帶來幾個問題
1.不可重復讀,A事務在第一次讀和第二次讀之間,如果B對數據進行的修改,則兩次讀取的數據會不一致
2.更新丟失,A和B同時操作一個數據,最后執行完畢的會覆蓋前一個的執行結果
3.臟讀,A事務添加了數據但并未提交,B讀到了這條數據后A回滾了,就會導致臟讀(很形象)
4.幻讀,A事務第二次讀取數據之前,B數據提交了滿足條件的數據,這種現象就叫幻讀
為了解決以上問題,產生了四個隔離級別:未提交讀,提交讀,可重復讀(InnoDB事務默認使用),串行讀
鎖模式
共享鎖(S):允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。
排他鎖(X):允許獲得排他鎖的事務更新數據,阻止其他事務取得相同數據集的共享讀鎖和排他寫鎖。
另外,為了允許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。
語句示例:
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
行鎖的實現方式:
InnoDB行鎖是通過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不同,后者是通過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特點意味著:只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖!在實際應用中,要特別注意InnoDB行鎖的這一特性,不然的話,可能導致大量的鎖沖突,從而影響并發性能。
由于MySQL的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的
當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論是使用主鍵索引、唯一索引或普通索引,InnoDB都會使用行鎖來對數據加鎖。如果不同的索引碰巧都落到了同一個行上,那么同樣會阻塞。
即便在條件中使用了索引字段,但是否使用索引來檢索數據是由MySQL通過判斷不同執行計劃的代價來決定的,如果MySQL認為全表掃描效率更高,比如對一些很小的表,它就不會使用索引,這種情況下InnoDB將使用表鎖,而不是行鎖。因此,在分析鎖沖突時,別忘了檢查SQL的執行計劃,以確認是否真正使用了索引。
間隙鎖
當我們用范圍條件而不是相等條件檢索數據,并請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;對于鍵值在條件范圍內但并不存在的記錄,叫做“間隙(GAP)”,InnoDB也會對這個“間隙”加鎖,這種鎖機制就是所謂的間隙鎖(Next-Key鎖)。
舉例來說,假如emp表中只有101條記錄,其empid的值分別是 1,2,...,100,101,下面的SQL:
Select * from emp where empid > 100 for update;
是一個范圍條件的檢索,InnoDB不僅會對符合條件的empid值為101的記錄加鎖,也會對empid大于101(這些記錄并不存在)的“間隙”加鎖。
InnoDB使用間隙鎖的目的,一方面是為了防止幻讀,以滿足相關隔離級別的要求,對于上面的例子,要是不使用間隙鎖,如果其他事務插入了empid大于100的任何記錄,那么本事務如果再次執行上述語句,就會發生幻讀;另外一方面,是為了滿足其恢復和復制的需要
還要特別說明的是,InnoDB除了通過范圍條件加鎖時使用間隙鎖外,如果使用相等條件請求給一個不存在的記錄加鎖,InnoDB也會使用間隙鎖!
MySQL的恢復機制是通過BINLOG記錄來執行IUD操作來同步Slave的,這就要求:在一個事務未提交前,其他并發事務不能插入滿足其鎖定條件的任何記錄,也就是不允許出現幻讀,這已經超過了ISO/ANSI SQL92“可重復讀”隔離級別的要求,實際上是要求事務要串行化。這也是許多情況下,InnoDB要用到間隙鎖的原因,比如在用范圍條件更新記錄時,無論在Read Commited或是Repeatable Read隔離級別下,InnoDB都要使用間隙鎖,但這并不是隔離級別要求的。
INSERT...SELECT...和 CREATE TABLE...SELECT...語句,可能會阻止對源表的并發更新,造成對源表鎖的等待。如果查詢比較復雜的話,會造成嚴重的性能問題,我們在應用中應盡量避免使用。實際上,MySQL將這種SQL叫作不確定(non-deterministic)的SQL,不推薦使用。
什么時候使用表鎖
對于InnoDB表,在絕大部分情況下都應該使用行級鎖,因為事務和行鎖往往是我們之所以選擇InnoDB表的理由。但在個別特殊事務中,也可以考慮使用表級鎖。
第一種情況是:事務需要更新大部分或全部數據,表又比較大,如果使用默認的行鎖,不僅這個事務執行效率低,而且可能造成其他事務長時間鎖等待和鎖沖突,這種情況下可以考慮使用表鎖來提高該事務的執行速度。
第二種情況是:事務涉及多個表,比較復雜,很可能引起死鎖,造成大量事務回滾。這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖、減少數據庫因事務回滾帶來的開銷。
如果以上兩種事務過多,那我們就可以考慮使用MyISAM引擎了
死鎖
發生死鎖后,InnoDB一般都能自動檢測到,并使一個事務釋放鎖并回退,另一個事務獲得鎖,繼續完成事務。但在涉及外部鎖,或涉及表鎖的情況下,InnoDB并不能完全自動檢測到死鎖,這需要通過設置鎖等待超時參數 innodb_lock_wait_timeout來解決。需要說明的是,這個參數并不是只用來解決死鎖問題,在并發訪問比較高的情況下,如果大量事務因無法立即獲得所需的鎖而掛起,會占用大量計算機資源,造成嚴重性能問題,甚至拖跨數據庫。我們通過設置合適的鎖等待超時閾值,可以避免這種情況發生。
在了解InnoDB鎖特性后,用戶可以通過設計和SQL調整等措施減少鎖沖突和死鎖,包括:
- 盡量使用較低的隔離級別;
- 精心設計索引,并盡量使用索引訪問數據,使加鎖更精確,從而減少鎖沖突的機會;
- 選擇合理的事務大小,小事務發生鎖沖突的幾率也更小;
- 給記錄集顯示加鎖時,最好一次性請求足夠級別的鎖。比如要修改數據的話,最好直接申請排他鎖,而不是先申請共享鎖,修改時再請求排他鎖,這樣容易產生死鎖;
- 不同的程序訪問一組表時,應盡量約定以相同的順序訪問各表,對一個表而言,盡可能以固定的順序存取表中的行。這樣可以大大減少死鎖的機會;
- 盡量用相等條件訪問數據,這樣可以避免間隙鎖對并發插入的影響;
- 不要申請超過實際需要的鎖級別;除非必須,查詢時不要顯示加鎖;
- 對于一些特定的事務,可以使用表鎖來提高處理速度或減少死鎖的可能。