MySQL併發事務控制:鎖、MVCC

緣起

最近同事開發遇到了一個事務阻塞的問題,查了一些資料發現關於MySQL事務、鎖這一塊的資料都比較絮亂,讓人看的雲裏霧裏.借這個機會,恰好也對這一塊作一個總結梳理,但願能比較全面去寫一下MySQL的併發事務處理mysql

事務的特性與隔離級別

要講鎖,必需要先講事務的特性與隔離級別,由於鎖機制的存在是爲了保證事務對應隔離級別下的特性.事務具備如下幾個特性 在MySQL中,存在如下幾種隔離級別 RU 讀未提交,顧名思義,在這種隔離級別下,當多個事務並行對同一數據進行操做時,會讀取未提交的數據,也被稱之爲髒讀.這種隔離級別由於會出現髒讀現象,因此在實際場景中不多用.sql

RC 讀提交,一個事務只能看見已經提交事務所作的改變.但這種隔離級別會出現 不可重複讀現象,即在一個事務內,屢次讀同一數據,在這個事務尚未結束時,若是另外一個事務剛好修改了這個數據,那麼,在第一個事務中,兩次讀取的數據就可能不一致.數據庫

RR 可重複讀,這是MySQL的默認事務隔離級別,在這種隔離級別下,解決了RC存在的不可重複讀問題,確保在同一事務中,會看到一樣的數據行.但可能會出現幻讀,即當一個事務在執行讀取操做,第一次查詢數據總量後,另外一個事務執行了新增數據的操做並提交後,這個時候第一個事務讀取的數據總量和以前統計的不同,就像產生幻覺同樣.markdown

SERIALIZABLE 串行化,是4種隔離級別中最高的級別,解決了髒讀、可重複讀、幻讀的問題,可是性能最差,它將事務的執行變爲順序執行,與其餘三個隔離級別相比,在並行事務執行過程當中,後一個事務的執行必須等待前一個事務結束.併發

MySQL中的鎖

在MySQL中,按鎖類型劃分,有如下種類 提到鎖到種類,須要提一下MySQL到存儲引擎,MySQL經常使用引擎有MyISAM和InnoDB,而InnoDB是mysql默認的引擎。MyISAM是不支持行鎖的,而InnoDB支持行鎖和表鎖。
MyISAM在執行查詢語句(SELECT)前,會自動給涉及的全部表加讀鎖,在執行更新操做(UPDATE、DELETE、INSERT等)前,會自動給涉及的表加寫鎖,讀鎖會阻塞對同一張表對寫操做,而寫鎖既會阻塞對同一張表對寫操做,也會阻塞讀操做.性能

對於InnoDB來講,你們都知道InnoDB相對與MyISAM,支持了事務和行鎖.而行鎖顧名思義,就是針對具體某一行數據上的鎖,更切確的說是針對索引加的鎖(這個會在下文鎖的實現中講到).優化

排他鎖,一般咱們在InnoDB中執行一個更新操做,針對這一行數據會持有排他鎖,持有排他鎖時,不容許再在數據行上添加寫鎖與讀鎖,其餘事務對此行數據的讀、寫操做都會被阻塞,只有當前事務提交了,鎖釋放了才容許其餘事務進行讀寫,達到避免 髒讀 的效果spa

共享鎖,主要是爲了支持併發的讀取數據而出現的,當一個事務持有某一數據行的共享鎖時,容許其它事務再獲取共享鎖,但不容許其它事務獲取排他鎖,也就是說,在持有共享鎖時,多個事務能夠讀取當前數據,但不不容許任何事務對當前數據進行修改操做,從而避免 不可重複 的問題3d

意向鎖,首先須要明白一點,意向鎖的做用是在表上的,當一個事務須要獲取共享鎖或排他鎖時,首先要獲取對應的意向鎖,爲何要這樣作呢,舉個例子,假設在事務A中,某一行數據持有共享鎖,這一行只能讀,不能寫.此時事務B申請得到表的寫鎖,假如加鎖成功,那麼事務B將可以對整個表的數據進行讀寫,與事務A衝突.這種操做確定是不容許的,因此MySQL會在申請共享鎖或者排他鎖的時候,先獲取對應的意向鎖,也就是說,你要操做表中的某一行鎖數據,先要看看整個表能不能被操做.意向鎖的申請是有數據庫完成的,不須要人爲申請.code

行鎖的3種實現

上文對幾種鎖類型進行了分析,其實平時開發中接觸到最多的仍是行鎖,行鎖的實現有如下幾種

在InnoDB中,鎖的實現是基於索引的
Record Lock(記錄鎖),會鎖住索引記錄,好比 update table where id = 1;,會是這種實現

Gap Lock(間隙鎖),實質上是對索引先後的間隙上鎖,不對索引自己上鎖,目的是爲了防止幻讀.當使用範圍條件而不是相等條件檢索數據並請求排他鎖、或共享鎖時,對於該範圍內不存在的記錄,不容許其修改插入.舉個例子,當表中只有一條id=101的記錄,一個事務執行select * from user where user_id > 100 for update;,此時另外一個事務執行插入一條id=102的數據是會阻塞的,必須等待第一個事務提交後才能完成.間隙鎖是針對事務隔離級別爲可重複讀或以上級別

Next-Key Lock,是記錄鎖和間隙鎖對結合,會同時鎖住記錄與間隙.在可重複讀(Repeatable Read)隔離級別下,會以Next-Key Lock的方式對數據行進行加鎖

MVCC

鎖機制能夠控制併發操做,來保證一致性,可是系統開銷會很大.在RC、RR的隔離級別下,MySQL InnoDB經過MVCC (多版本併發控制)機制來解決幻讀,使事務在併發過程當中,SELECT 操做不用加鎖,讀寫不衝突從而提升性能.其原理是經過保存數據在某個時間點的快照來實現的.經過在每行記錄後面保存隱藏列來存放事務ID,這樣每個事務,都會對應一個遞增的事務ID.假設三個事務同時更新來同一行數據,那麼就會對應三個數據版本,但實際上版本一、版本2並非物理存在的,而是經過關聯記錄在undo log中,這樣就能夠經過undo log找回數據的歷史版本,好比回滾的操做,會使用上一個版本的數據覆蓋數據頁上的數據

下面舉例一個RR隔離級別下快照讀例子1:開啓事務A按條件A查詢到兩條數據,此時事務B再插入1條數據知足條件A的數據,並提交事務,此時事務A再按條件A進行查詢,查詢到的依然是兩條數據,也就是說,事務A查詢到的並非當前最新的數據版本,而是經過MVCC實現的歷史快照版本.這也是可重複讀的實現.

上面的例子介紹了讀操做,那麼寫操做呢,也是如此事務之間互不干擾嗎.再舉例一個RR隔離級別下更新操做的例子2:假設事務A執行一個更新語句,知足更新條件A的的數據是2條,更新成功後不提交事務,此時事務B插入一條新的知足條件A的數據,此時事務A再按條件A去更新數據,實驗發現事務B新插入的數據也被更新了.出現了幻讀,這就是當前讀,即對數據修改的操做(update、insert、delete)都會讀到已提交事務的最新數據.

那麼當前讀的幻讀問題如何解決呢?MVCC不能解決的問題固然是交給鎖來解決了.上文提到的Next-Key Lock正是解決這個問題的方法,還以上面的例子2爲例,給條件A字段非惟一索引,事務B進行插入數據的時候就會被阻塞,緣由是事務A持有了Gap Lock,只有事務A提交了,事務B才能成功插入數據.這就解決了當前讀操做下的幻讀問題.

因此MVCC機制可防止快照讀引發的幻讀,next-key鎖可防止當前讀引發的幻讀.須要說明的是,MVCC只在RC和RR兩個隔離級別下工做。其餘兩個隔離級別和MVCC不兼容, 由於 RU老是讀取最新的數據行, 而不是符合當前事務版本的數據行.而SERIALIZABLE 則會對全部讀取的行都加鎖.

鎖的觸發和升級

以默認的InnoDB引擎RR級別說明,表鎖能夠理解爲每一行記錄都持有Record Lock,更新記錄時,當更新字段沒有走索引時,沒法獲取對應記錄的Record Lock,行鎖便會升級爲表鎖,這一點能夠結合MySQL Explain去分析.須要注意的是當普通索引值區分度低時,此時觀察Explain顯示是走了索引的,但當另外一個事務併發操做不一樣數據時,依然發現第二個事務會阻塞,這是由於MySQL的執行優化器認爲給多行記錄一次一次當加鎖不如表鎖來的高效,因此不會把這個普通索引當作索引,而當區分度高時,則認爲是高效的,不會升級爲表鎖.因此,建立合適的索引很重要,區分度低的字段不建議建立索引.

何時會出現DeadLock

什麼是死鎖呢?死鎖是指兩個或兩個以上的進程在執行過程當中,因爲競爭資源或者因爲彼此通訊而形成的一種阻塞的現象,若無外力做用,它們都將沒法推動下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱爲死鎖進程。舉個例子4: 事務A獲取 id=20的鎖,事務B獲取id=30的鎖,而後,事務A試圖獲取id=30的鎖,而該鎖已經被事務B持有,因此事務A等待事務 B釋放該鎖,而後事務B又試圖獲取id =20 的鎖這個鎖被事務 A 佔有,因而兩個事務之間相互等待,這就會致使死鎖.死鎖的場景還有許多,歸根結底,都是由於多個事務想要獲取的鎖互斥且獲取的順序不一致所形成.如何避免死鎖呢,一般Record Lock引發的死鎖問題開發時都會比較當心,但Gap Lock可能致使死鎖的問題一般會被忽略,因此這一點要多加註意,另外就是創建合適的索引,若是沒有索引,那麼在操做數據時會鎖住每一行,會增大死鎖的機率.

鎖問題的排查命令

show open tabbles; SHOW OPEN TABLES where In_use > 0;查看那些表被鎖了
show status like 'table%';
table_locks_waited 出現表級鎖定爭用發生等待的次數,此值高說明存在驗證的表記鎖爭用狀況
table_locks_immediate 表示當即釋放表鎖的次數
show status like 'innodb_row_lock%'; Innodb_row_lock_current_waits 當前正在等待鎖定的數量
Innodb_row_lock_time 系統啓動到如今鎖定總時間
Innodb_row_lock_time_avg 每次等待話費的平均時間 Innodb_row_lock_time_max 系統啓動到如今等待最長一次所花時間 Innodb_row_lock_waits 系統啓動後到如今共等待次數
information_schema
information_schema是MySQL專門記錄性能信息的庫,在5.7版本後默認打開
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; 查看當前InnoDB的鎖的信息,會顯示是什麼鎖類型,屬於那個事務ID
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX; 查看InnoDB事務ID,會顯示是什麼操做和一些常規信息,例如是否在運行running,仍是等待鎖. SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;查看InnoDB鎖的等待時間,和等待的是哪一個事務ID的鎖

相關文章
相關標籤/搜索