本文主要是針對MySQL/InnoDB的並發控制和加鎖技術做一個比較深入的剖析,並且對其中涉及到的重要的概念,如多版本並發控制(MVCC),臟讀(dirty read),幻讀(phantom read),四種隔離級別(isolation level)等作詳細的闡述,並且基於一個簡單的例子,對MySQL的加鎖進行了一個詳細的分析。本文的總結參考了何登成前輩的博客,並且在前輩總結的基礎上,進行了一些基礎性的說明,希望對剛入門的同學產生些許幫助,如有錯誤,請不吝賜教。按照我的寫作習慣,還是通過幾個關鍵問題來組織行文邏輯,如下:
- 什么是MVCC(多版本並發控制)?如何理解快照讀(snapshot read)和當前讀(current read)?
- 什么是隔離級別?臟讀?幻讀?InnoDB的四種隔離級別的含義是什么?
- 什么是死鎖?
- InnoDB是如何實現MVCC的?
- 一個簡單的sql在不同場景下的加鎖分析
- 一個復雜的sql的加鎖分析
接下來,我將按照這幾個關鍵問題的順序,對以上問題作一一解答,並且在解答的過程中,爭取將加鎖技術的細節,闡述的更加清楚。
1.1 MVCC:Multi-Version Concurrent Control 多版本並發控制
MVCC是為了實現數據庫的並發控制而設計的一種協議。從我們的直觀理解上來看,要實現數據庫的並發訪問控制,最簡單的做法就是加鎖訪問,即讀的時候不能寫(允許多個西線程同時讀,即共享鎖,S鎖),寫的時候不能讀(一次最多只能有一個線程對同一份數據進行寫操作,即排它鎖,X鎖)。這樣的加鎖訪問,其實並不算是真正的並發,或者說它只能實現並發的讀,因為它最終實現的是讀寫串行化,這樣就大大降低了數據庫的讀寫性能。加鎖訪問其實就是和MVCC相對的LBCC,即基於鎖的並發控制(Lock-Based Concurrent Control),是四種隔離級別中級別最高的Serialize隔離級別。為了提出比LBCC更優越的並發性能方法,MVCC便應運而生。
幾乎所有的RDBMS都支持MVCC。它的最大好處便是,讀不加鎖,讀寫不沖突。在MVCC中,讀操作可以分成兩類,快照讀(Snapshot read)和當前讀(current read)。快照讀,讀取的是記錄的可見版本(可能是歷史版本,即最新的數據可能正在被當前執行的事務並發修改),不會對返回的記錄加鎖;而當前讀,讀取的是記錄的最新版本,並且會對返回的記錄加鎖,保證其他事務不會並發修改這條記錄。在MySQL InnoDB中,簡單的select操作,如 select * from table where ? 都屬於快照讀;屬於當前讀的包含以下操作:
- select * from table where ? lock in share mode; (加S鎖)
- select * from table where ? for update; (加X鎖,下同)
- insert, update, delete操作
針對一條當前讀的SQL語句,InnoDB與MySQL Server的交互,是一條一條進行的,因此,加鎖也是一條一條進行的。先對一條滿足條件的記錄加鎖,返回給MySQL Server,做一些DML操作;然后再讀取下一條加鎖,直至讀取完畢。需要注意的是,以上需要加X鎖的都是當前讀,而普通的select(除了for update)都是快照讀,每次insert、update、delete之前都是會進行一次當前讀的,這個時候會上鎖,防止其他事務對某些行數據的修改,從而造成數據的不一致性。我們廣義上說的幻讀現象是通過MVCC解決的,意思是通過MVCC的快照讀可以使得事務返回相同的數據集。如下圖所示:
注意,我們一般說在MyISAM中使用表鎖,因為MyISAM在修改數據記錄的時候會將整個表鎖起來;而InnoDB使用的是行鎖,即我們以上所談的MVCC的加鎖問題。但是,並不是InnoDB引擎不會使用表鎖,比如在alter table的時候,Innodb就會將該表用表鎖鎖起來。
1.2 隔離級別
在SQL的標准中,定義了四種隔離級別。每一種級別都規定了,在一個事務中所做的修改,哪些在事務內和事務間是可見的,哪些是不可見的。低級別的隔離可以執行更高級別的並發,性能好,但是會出現臟讀和幻讀的現象。首先,我們從兩個基礎的概念說起:
臟讀(dirty read):兩個事務,一個事務讀取到了另一個事務未提交的數據,這便是臟讀。
幻讀(phantom read):兩個事務,事務A與事務B,事務A在自己執行的過程中,執行了兩次相同查詢,第一次查詢事務B未提交,第二次查詢事務B已提交,從而造成兩次查詢結果不一樣,這個其實被稱為不可重復讀;如果事務B是一個會影響查詢結果的insert操作,則好像新多出來的行像幻覺一樣,因此被稱為幻讀。其他事務的提交會影響在同一個事務中的重復查詢結果。
下面簡單描述一下SQL中定義的四種標准隔離級別:
- READ UNCOMMITTED (未提交讀) :隔離級別:0. 可以讀取未提交的記錄。會出現臟讀。
- READ COMMITTED (提交讀) :隔離級別:1. 事務中只能看到已提交的修改。不可重復讀,會出現幻讀。(在InnoDB中,會加行所,但是不會加間隙鎖)該隔離級別是大多數數據庫系統的默認隔離級別,但是MySQL的則是RR。
- REPEATABLE READ (可重復讀) :隔離級別:2. 在InnoDB中是這樣的:RR隔離級別保證對讀取到的記錄加鎖 (記錄鎖),同時保證對讀取的范圍加鎖,新的滿足查詢條件的記錄不能夠插入 (間隙鎖),因此不存在幻讀現象。但是標准的RR只能保證在同一事務中多次讀取同樣記錄的結果是一致的,而無法解決幻讀問題。InnoDB的幻讀解決是依靠MVCC的實現機制做到的。
- SERIALIZABLE (可串行化):隔離級別:3. 該隔離級別會在讀取的每一行數據上都加上鎖,退化為基於鎖的並發控制,即LBCC。
需要注意的是,MVCC只在RC和RR兩個隔離級別下工作,其他兩個隔離級別都和MVCC不兼容。
1.3 死鎖
死鎖是指兩個或者多個事務在同一資源上相互作用,並請求鎖定對方占用的資源,從而導致惡性循環的現象。當多個事務試圖以不同的順序鎖定資源時,就可能產生死鎖。多個事務同時鎖定同一個資源時,也會產生死鎖。且看下面的兩個產生死鎖的例子:
第一個死鎖很好理解,而第二個死鎖,由於在主索引(聚簇索引表)上仍舊是對兩條記錄進行了不同順序的加鎖,因此仍舊會造成死鎖。死鎖的發生與否,並不在於事務中有多少條SQL語句,死鎖的關鍵在於:兩個(或以上)的Session加鎖的順序不一致。因此,我們通過分析加鎖細節,可以判斷所寫的sql是否會發生死鎖,同時發生死鎖的時候,我們應該如何處理。
1.4 InnoDB的MVCC實現機制
MVCC可以認為是行級鎖的一個變種,它可以在很多情況下避免加鎖操作,因此開銷更低。MVCC的實現大都都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。InnoDB的MVCC實現,是通過保存數據在某個時間點的快照來實現的。一個事務,不管其執行多長時間,其內部看到的數據是一致的。也就是事務在執行的過程中不會相互影響。下面我們簡述一下MVCC在InnoDB中的實現。
InnoDB的MVCC,通過在每行記錄后面保存兩個隱藏的列來實現:一個保存了行的創建時間,一個保存行的過期時間(刪除時間),當然,這里的時間並不是時間戳,而是系統版本號,每開始一個新的事務,系統版本號就會遞增。在RR隔離級別下,MVCC的操作如下:
- select操作。a. InnoDB只查找版本早於(包含等於)當前事務版本的數據行。可以確保事務讀取的行,要么是事務開始前就已存在,或者事務自身插入或修改的記錄。b. 行的刪除版本要么未定義,要么大於當前事務版本號。可以確保事務讀取的行,在事務開始之前未刪除。
- insert操作。將新插入的行保存當前版本號為行版本號。
- delete操作。將刪除的行保存當前版本號為刪除標識。
- update操作。變為insert和delete操作的組合,insert的行保存當前版本號為行版本號,delete則保存當前版本號到原來的行作為刪除標識。
由於舊數據並不真正的刪除,所以必須對這些數據進行清理,innodb會開啟一個后台線程執行清理工作,具體的規則是將刪除版本號小於當前系統版本的行刪除,這個過程叫做purge。
1.5 一個簡單SQL的加鎖分析
在MySQL的InnoDB中,都是基於聚簇索引表的。而且普通的select操作都是基於快照讀,是不需要加鎖的。那么我們在分析其他的sql語句的時候,如何分析加鎖細節?下面我們以一個簡單的delete操作的SQL為例,進行一個詳細的闡述。且看下面的SQL:
delete from t1 where id=10;
如果對這條SQL進行加鎖分析,那么MySQL是如何加鎖的呢?一般情況下,我們直觀的感受是:會在id=10的記錄上加鎖。但是,這樣輕率的下結論是片面的,要想確定MySQL的加鎖情況,我們還需要知道更多的條件。還需要知道哪些條件呢?比如:
- id列是不是主鍵?
- 系統的隔離級別是什么?
- id非主鍵的話,其上有建立索引嗎?
- 建立的索引是唯一索引嗎?
- 該SQL的執行計划是什么?索引掃描?全表掃描?
接下來,我將這些問題的答案進行組合,然后按照從易到難的順序,逐個分析每種組合下,對應的SQL會加哪些鎖。
- 組合1:id列是主鍵,RC隔離級別
- 組合2:id列是二級唯一索引,RC隔離級別
- 組合3:id列是二級非唯一索引,RC隔離級別
- 組合4:id列上沒有索引,RC隔離級別
- 組合5:id列是主鍵,RR隔離級別
- 組合6:id列是二級唯一索引,RR隔離級別
- 組合7:id列是二級非唯一索引,RR隔離級別
- 組合8:id列上沒有索引,RR隔離級別
- 組合9:Serializable隔離級別
組合1:id列是主鍵,RC隔離級別
當id是主鍵的時候,我們只需要在該id=10的記錄上加上x鎖即可。如下圖所示:
組合2:id列是二級唯一索引,RC隔離級別
在這里我先解釋一下聚簇索引和普通索引的區別。在InnoDB中,主鍵可以被理解為聚簇索引,聚簇索引中的葉子結點就是相應的數據行,具有聚簇索引的表也被稱為聚簇索引表,數據在存儲的時候,是按照主鍵進行排序存儲的。我們都知道,數據庫在select的時候,會選擇索引列進行查找,索引列都是按照B+樹(多叉搜索樹)數據結構進行存儲,找到主鍵之后,再回到聚簇索引表中進行查詢,這叫回表查詢。那我們自然會問,當使用索引進行查詢的時候,與索引相對應的記錄會被上鎖嗎?會的。如果id是唯一索引,那么只給該唯一索引所對應的索引記錄上x鎖;如果id是非唯一索引,那么所對應的所有的索引記錄上都會上x鎖。如下圖所示:
組合3:id列是二級非唯一索引,RC隔離級別
解釋同上,如下圖:
組合4:id列上沒有索引,RC隔離級別
由於id列上沒有索引,因此只能走聚簇索引,進行全部掃描。有人說會在表上加X鎖;有人說會在聚簇索引上,選擇出來的id = 10 的記錄加上X鎖。真實情況如下圖:
若id列上沒有索引,SQL會走聚簇索引的全掃描進行過濾,由於過濾是由MySQL Server層面進行的。因此每條記錄,無論是否滿足條件,都會被加上X鎖。但是,為了效率考量,MySQL做了優化,對於不滿足條件的記錄,會在判斷后放鎖,最終持有的,是滿足條件的記錄上的鎖,但是不滿足條件的記錄上的加鎖/放鎖動作不會省略。同時,優化也違背了2PL的約束(同時加鎖同時放鎖)。
組合5,6同以上(因為只有一條結果記錄,只能在上面加鎖)
組合7:id列是二級非唯一索引,RR隔離級別
在RR隔離級別下,為了防止幻讀的發生,會使用Gap鎖。這里,你可以把Gap鎖理解為,不允許在數據記錄前面插入數據。首先,通過id索引定位到第一條滿足查詢條件的記錄,加記錄上的X鎖,加GAP上的GAP鎖,然后加主鍵聚簇索引上的記錄X鎖,然后返回;然后讀取下一條,重復進行。直至進行到第一條不滿足條件的記錄[11,f],此時,不需要加記錄X鎖,但是仍舊需要加GAP鎖,最后返回結束。如下圖所示:
組合8:id列無索引,RR隔離級別
在這種情況下,聚簇索引上的所有記錄,都被加上了X鎖。其次,聚簇索引每條記錄間的間隙(GAP),也同時被加上了GAP鎖。如下圖:
但是,MySQL是做了相關的優化的,就是所謂的semi-consistent read。semi-consistent read開啟的情況下,對於不滿足查詢條件的記錄,MySQL會提前放鎖,同時也不會添加Gap鎖。
組合9:Serializable隔離級別
和RR隔離級別一樣。
1.6 一個復雜的SQL的加鎖分析
這里我們只是列出一個結論,因為要涉及到MySQL的where查詢條件的分析,因此這里先不做詳細介紹,我會在之后的博客中詳細說明。如下圖:
結論:在RR隔離級別下,針對一個復雜的SQL,首先需要提取其where條件。Index Key確定的范圍,需要加上GAP鎖;Index Filter過濾條件,視MySQL版本是否支持ICP,若支持ICP,則不滿足Index Filter的記錄,不加X鎖,否則需要X鎖;Table Filter過濾條件,無論是否滿足,都需要加X鎖。加鎖的結果如下所示:
總結
本文只是對MVCC的一些基礎性的知識點進行了詳細的總結,參考了網上和書上比較多的資料和實例。希望能對各位的學習有所幫助。
轉自
MySQL的並發控制與加鎖分析 - Yelbosh - 博客園
http://www.cnblogs.com/yelbosh/p/5813865.html
Mysql服務器邏輯架構
每個連接都會在mysql服務端產生一個線程(內部通過線程池管理線程),比如一個select語句進入,mysql首先會在查詢緩存中查找是否緩存了這個select的結果集,如果沒有則繼續執行 解析、優化、執行的過程;否則會之間從緩存中獲取結果集。
Mysql並發控制——共享鎖、排他鎖
共享鎖
共享鎖也稱為讀鎖,讀鎖允許多個連接可以同一時刻並發的讀取同一資源,互不干擾;
排他鎖
排他鎖也稱為寫鎖,一個寫鎖會阻塞其他的寫鎖或讀鎖,保證同一時刻只有一個連接可以寫入數據,同時防止其他用戶對這個數據的讀寫。
鎖策略
鎖的開銷是較為昂貴的,鎖策略其實就是保證了線程安全的同時獲取最大的性能之間的平衡策略。
- Mysql鎖策略:talbe lock(表鎖)
表鎖是Mysql最基本的鎖策略,也是開銷最小的策略,它會鎖定整個表;具體情況是:若一個用戶正在執行寫操作,會獲取排他的“寫鎖”,這是會鎖定整個表,阻塞其他用戶的讀、寫操作;
若一個用戶正在執行讀操作,會先獲取共享鎖“讀鎖”,這個鎖運行其他讀鎖並發的對這個表進行讀取,互不干擾。只要沒有寫鎖的進入,讀鎖可以是並發讀取統一資源的。
- Mysql鎖策略:row lock(行鎖)
行鎖可以最大限度的支持並發處理,當然也帶來了最大開銷,顧名思義,行鎖的粒度實在每一條行數據。
事務
事務就是一組原子性的sql,或者說一個獨立的工作單元。就是說要么mysql引擎會全部執行這一組sql語句,要么全部都不執行(比如其中一條語句失敗的話)。
比如,tim要給bill轉賬100塊錢:
1.檢查tim的賬戶余額是否大於100塊;
2.tim的賬戶減少100塊;
3.bill的賬戶增加100塊;
這三個操作就是一個事務,必須打包執行,要么全部成功,要么全部不執行,其中任何一個操作的失敗都會導致所有三個操作“不執行”——回滾。
CREATE TABLE `employees`.`account` ( `id` BIGINT (11) NOT NULL AUTO_INCREMENT, `p_name` VARCHAR (4), `p_money` DECIMAL (10, 2) NOT NULL DEFAULT 0, PRIMARY KEY (`id`) ) ; INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('1', 'tim', '200'); INSERT INTO `employees`.`account` (`id`, `p_name`, `p_money`) VALUES ('2', 'bill', '200'); START TRANSACTION; SELECT p_money FROM account WHERE p_name="tim";//step1 UPDATE account SET p_money=p_money-100 WHERE p_name="tim";//step2 UPDATE account SET p_money=p_money+100 WHERE p_name="bill";//step3 COMMIT;
一個良好的事務系統,必須滿足ACID特點:
ACID
-
A:atomiciy原子性
一個事務必須保證其中的操作要么全部執行,要么全部回滾,不可能存在只執行了一部分這種情況出現。 -
C:consistency一致性
數據必須保證從一種一致性的狀態轉換為另一種一致性狀態;比如上一個事務中執行了第二步時系統崩潰了,數據也不會出現bill的賬戶少了100塊,但是tim的賬戶沒變的情況。要么維持原裝(全部回滾),要么bill少了100塊同時tim多了100塊,只有這兩種一致性狀態的 -
I:isolation隔離性
在一個事務未執行完畢時,通常會保證其他事務無法看到這個事務的執行結果
- D:durability持久性
事務一旦commit,則數據就會保存下來,即使提交完之后系統崩潰,數據也不會丟失。
隔離級別
- READ UNCOMMITTED(未提交讀)
事務中的修改,即使沒有提交,對其他事務也是可見的。事務可以讀取未提交的數據——臟讀。臟讀會導致很多問題,一般不適用這個隔離級別。
實例:
-- ------------------------- read-uncommitted實例 ------------------------------ -- 設置全局系統隔離級別 SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -- Thread A START TRANSACTION; SELECT * FROM USER; UPDATE USER SET NAME="READ UNCOMMITTED"; -- commit; -- Thread B SELECT * FROM USER; //ThreadB Console 可以看到線程A未提交的事物處理,在另一個線程中也看到了,這就是所謂的臟讀 id name 2 READ UNCOMMITTED 34 READ UNCOMMITTED
- READ COMMITTED(提交讀)
一般數據庫都默認使用這個隔離級別(Mysql不是),這個隔離級別保證了一個事務如果沒有完全成功(commit執行完),事務中的操作對其他事務是不可見的。
-- ------------------------- read-cmmitted實例 ------------------------------ -- 設置全局系統隔離級別 SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED; -- Thread A START TRANSACTION; SELECT * FROM USER; UPDATE USER SET NAME="READ COMMITTED"; -- COMMIT; -- Thread B SELECT * FROM USER; //Console OUTPUT: id name 2 READ UNCOMMITTED 34 READ UNCOMMITTED --------------------------------------------------- -- 當線程A執行了commit,線程B得到如下結果: id name 2 READ COMMITTED 34 READ COMMITTED
也就驗證了read committed級別在事物未完成commit操作之前修改的數據對其他線程不可見,執行了commit之后才會對其他線程可見。
我們可以看到線程B兩次查詢得到了不同的數據。
這種隔離級別解決了臟讀的問題,但是會對其他線程產生兩次不一致的讀取結果。
- REPEATABLE READ(可重復讀)
這個隔離級別解決了臟讀的問題,但會產生幻讀,問題。
臟讀與幻讀與不可重復讀:
臟讀:一個事務讀取到另一事務未提交的更新新據。當一個事務正在訪問數據,並且對數據進行了修改,而這種修改還沒有
提交到數據庫中,這時,另外一個事務也訪問這個數據,然后使用了這個數據。因為這個數據是還沒有提交的數據, 那么另
外一個事務讀到的這個數據是臟數據,依據臟數據所做的操作也可能是不正確的。
不可重復讀:在同一事務中,多次讀取同一數據返回的結果有所不同。換句話說就是,后續讀取可以讀到另一事務已提交的
更新數據。相反,“可重復讀”在同一事務中多次讀取數據時,能夠保證所讀數據一樣,也就是,后續讀取不能讀到另一事務
已提交的更新數據。
幻讀:事務T1執行一次查詢,然后事務T2新插入一行記錄,這行記錄恰好可以滿足T1所使用的查詢的條件。然后T1又使用相同
的查詢再次對表進行檢索,但是此時卻看到了事務T2剛才插入的新行。這個新行就稱為“幻像”,因為對T1來說這一行就像突然
出現的一樣。
- SERIALIZABLE(可串行化)
最強的隔離級別,通過給事務中每次讀取的行加鎖(行鎖),保證不產生幻讀問題,但是會導致大量超時以及鎖爭用問題。
Mysql死鎖問題
死鎖,就是產生了循環等待鏈條,我等待你的資源,你卻等待我的資源,我們都相互等待,誰也不釋放自己占有的資源,導致無線等待下去。
比如:
//Thread A
START TRANSACTION; UPDATE account SET p_money=p_money-100 WHERE p_name="tim"; UPDATE account SET p_money=p_money+100 WHERE p_name="bill"; COMMIT; //Thread B START TRANSACTION; UPDATE account SET p_money=p_money+100 WHERE p_name="bill"; UPDATE account SET p_money=p_money-100 WHERE p_name="tim"; COMMIT;
當線程A執行到第一條語句UPDATE account SET p_money=p_money-100 WHERE p_name=”tim”;鎖定了p_name=”tim”的行數據;並且試圖獲取p_name=”bill”的數據;
,此時,恰好,線程B也執行到第一條語句:UPDATE account SET p_money=p_money+100 WHERE p_name=”bill”;
鎖定了 p_name=”bill”的數據,同時試圖獲取p_name=”tim”的數據;
此時,兩個線程就進入了死鎖,誰也無法獲取自己想要獲取的資源,進入無線等待中,直到超時!
對於死鎖,數據庫一般通過死鎖監測、死鎖超時機制解決;通常會執行回滾,打破死鎖狀態,然后再次執行之前死鎖的事務即可。
Mysql中的事務
- 自動提交(AutoCommit)
mysql默認采用AutoCommit模式,也就是每個sql都是一個事務,並不需要顯示的執行事務
多版本並發控制-MVCC
MVCC是個行級鎖的變種,它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現不同,但通常都是實現非阻塞讀,對於寫操作只鎖定必要的行。
通常MVCC實現有樂觀並發控制與悲觀並發控制,INNODB的MVCC通常是通過在每行數據后邊保存兩個隱藏的列來實現,一個保存了行的創建時間,另一個保存了行的刪除時間。當然存儲的並不是實際的時間值,而是系統版本號,每個事務開始,系統版本號就會遞增!,每個事務開始時刻的版本號也會作為這個事務的版本號,用來和查詢到的每行版本號做比較。下邊在Mysql默認的Repeatable Read隔離級別下,具體看看MVCC操作:
-
Select:
a.InnoDB只查找版本號早於當前版本號的數據行,這樣保證了讀取的數據要么實在這個事務開始之前就已經commit了的(早於當前版本號),要么是在這個事務自身中執行操作的數據(等於當前版本號)。
b.行的刪除版本號要么未定義,要么早於當前的版本號,這樣保證了事務讀取到的數據在事務開始之前未被刪除。 -
Insert
InnoDB為這個事務中新插入的行,保存當前事務版本號的行(作為行的版本號)。
-
Delete
InnoDB為每一個刪除的行保存當前事務版本號,最為行的刪除標記。 -
Update
InnoDB將保存當前版本號最為行版本號,同時保存當前版本號到原來行(更新前)的刪除版本號標識處。
轉自
深入理解Mysql——鎖、事務與並發控制 - 張碩的專欄 - CSDN博客
http://blog.csdn.net/lemon89/article/details/51477497