一、base:ACID屬性,併發控制mysql
二、MySql事務的隔離級別有哪些,含義是什麼?算法
三、鎖知多少,讀鎖,寫鎖,排他鎖,共享鎖,間隙鎖,樂觀鎖,悲觀鎖。sql
四、Mysql的事務與鎖有什麼關聯?MySq中的事務實例。數據庫
1.1 ACID屬性,多版本併發控制安全
在數據庫彙總,事務能夠看做是一組SQL語句組成的邏輯處理單元,事務主要具備如下4個屬性,簡稱ACID屬性:服務器
1.2 多版本併發控制併發
MySQL的大多數的事務型存儲引擎實現的都不是簡單的行級鎖,基於提高併發性能的考慮,都引入了多版本併發控制技術(MVCC),能夠認爲MVCC是行級鎖的一個變種,在不少狀況下能夠避免加鎖的操做,讓開銷更低。InnoDB的MVCC,是經過在每行記錄後面保存兩個隱藏的列來實現的。一個保存的行的建立時間,一個保存行過時時間(或刪除時間)。存儲的不是實際的時間值,是系統的版本號。每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會做爲事務的版本的版本號,用來和查詢列的每行記錄的版本號進行比較。性能
MVCC只在已提交讀(Read Committed)和可重複讀(Repeatable Read)兩個隔離級別下工做,其餘兩個隔離級別和MVCC是不兼容的。由於未提交讀,總數讀取最新的數據行,而不是讀取符合當前事務版本的數據行。而串行化(Serializable)則會對讀的全部數據多加鎖,下面以可重複讀(Repeatable Read)爲例子,說明下以select、delete、 insert、 update語句MVCC具體是如何操做的。測試
SELECT優化
Innodb檢查每行數據,確保他們符合兩個標準:
一、InnoDB只查找版本早於當前事務版本的數據行(也就是數據行的版本必須小於等於事務的版本),這確保當前事務讀取的行都是事務以前已經存在的,或者是由當前事務建立或修改的行
二、行的刪除操做的版本必定是未定義的或者大於當前事務的版本號,肯定了當前事務開始以前,行沒有被刪除符合了以上兩點則返回查詢結果。
INSERT
InnoDB爲每一個新增行記錄當前系統版本號做爲建立ID。
DELETE
InnoDB爲每一個刪除行的記錄當前系統版本號做爲行的刪除ID。
UPDATE
InnoDB複製了一行。這個新行的版本號使用了系統版本號。它也把系統版本號做爲了刪除行的版本。
1.3 事務的一致性和數據庫的一致性讀
2.1 事務併發時有可能產生的問題
因爲數據庫在處理過程當中是併發進行處理的,所以在事務併發執行過程當中,可能會帶來以下一些問題:
當多個事務對同一條數據庫記錄進行操做時,因爲每一個事務不知道其餘事務的存在,若是其中一個事務讀取了數據庫記錄後,該記錄又被另一個事務進行了更新,那麼該事務基於最初讀取到的值進行一些運算並將運算結果寫回數據庫時就可能會出現問題:中間事務的更新被覆蓋,從而致使更新丟失的問題。
解決方法:使用InnoDB存儲引擎時,主要經過排他鎖避免更新丟失的問題,這就要求全部更新都遵循一個基本原則:在更新數據庫記錄前得到該記錄的排他鎖。對於某些特殊的應用,在業務容許的狀況下也可使用增量更新的方式進行優化:即對某些字段進行A+=X之類的操做,利用Mysql更新語句會默認得到行鎖的特性去避免更新丟失。
事務A在某個步驟中修改了一條記錄,在A完成並提交前,該記錄處於一箇中間狀態,此時,另一個事務若是讀取同一條記錄,若是該事務讀到這個中間狀態,那麼就有可能會出現數據不一致狀態,這種現象叫作髒讀。
解決方法:使用InnoDB存儲引擎時,能夠經過共享鎖或者排他鎖避免髒讀,這就要求全部更新操做都遵循一個基本原則:在讀取數據庫記錄前得到該記錄的排他鎖獲取共享鎖。此外,還能夠經過設置InnoDB的事務隔離級別來避免髒讀的問題。
事務A在讀取了某條記錄後,該記錄又被另一個事務進行了修改,此時,若是事務A再次讀取該記錄時,若是該記錄的數據和它首次讀取的記錄不一致了,那麼這種現象就叫作不可重複讀,它與髒讀的區別是:髒讀是讀到事務中間過程的臨時數據,數據處於一個臨時的非穩定狀態,可能被該事務再次修改或者回滾;而不可重複讀是因爲兩次讀取時間點的間隙中,數據被其餘事務修改而致使的數據不一致,這種修改對數據庫的影響是永久性的。
解決方法:使用InnoDB存儲引擎時,能夠經過設置InnoDB的事務隔離級別來控制重複讀的特性,關於事務隔離級別,後續將進行詳細描述。
幻讀出如今多條記錄的查詢中。當事務A用一樣的查詢條件讀取以前某個點檢索過的數據時,檢索到了被其餘事務在其間插入的新記錄(或者少了被其餘事務刪除的記錄),這種現象就稱爲幻讀。
解決方法:幻讀和不可重複讀的本質相同,都是因爲兩次讀取間隙中因爲其餘事務的操做致使的記錄數據變動,其區別是:不可重複讀通常是針對單條記錄的,幻讀是針對一個結果集而言。
事務併發問題不能單靠數據庫事務控制來解決,一般須要結合InnoDB的鎖機制來解決,所以,防止出現事務併發問題是應用層面須要解決的問題,不能單純依靠事務機制。可是髒讀、不可重複讀、幻讀是數據庫讀一致性的問題,須要由數據庫提供必定的事務隔離機制進行解決。
2.2 事務的隔離級別
事務就是一組原子性的SQL查詢,或者說一個獨立的工做單元,若是數據庫引擎能成功對數據庫應用該組查詢的所有語句,那麼事務內的語句,要麼所有執行成功,要麼所有執行失敗,若是其中有任何一條語句由於崩潰或者其餘緣由沒法執行或者執行失敗,那麼其餘的語句就都不會執行。在SQL標準中定義了四種隔離級別,每一種級別都規定了一個事務中所作的修改,哪些在事務內和事務間是可見的,哪些是不可見的。
下面用表格簡單表述下:
隔離級別 | 讀一致性 | 髒讀 | 不可重複讀 | 幻讀 |
未提交讀(Read Uncommitted) | 最低級別 | 是 | 是 | 是 |
已提交讀(Read Committed) | 語句級別 | 否 | 是 | 是 |
可重複讀(Repeatable Read) | 事務級別 | 否 | 否 | 是 |
串行化(Serializable) | 最高級別,事務級 | 否 | 否 | 否 |
在未提交讀模式下,SELECT語句以不加鎖方式提交執行,並且有可能使用以前較早版本的數據。通俗的能夠理解爲可能讀到其餘事務未提交的中間狀態數據,這個中間數據有可能被從新修改或者被回滾。因此在該模式下,數據庫查詢是不符合一致性讀的原則。
在事務的讀取操做點上能夠看到該事務開始後到讀取操做點之間被其餘事務提交的修改。只要其餘事務提交後的數據,當前事務在任一點都能看見。
在一個事務內部,對記錄的讀取有如下特性,全部一致性讀是讀取由第一次讀所肯定的同一快照。也就是意味着,在同一個事務內,以一樣的查詢條件執行查詢均以第一次查詢的結果爲準,哪怕後續有其餘事務對記錄進行過更改。
可序列化是讀一致性的最高級別, 該級別與可重複讀相似,可是在該級別下,即便是普通的不加鎖查詢,InnoDB也會用LOCK IN SHARE MODE方式提交該查詢。
數據庫的事務隔離級別越嚴格,併發反作用就越小,可是付出的代價也越大,由於事務隔離級別越高,就要求數據庫在必定程度上進行「串行化」操做,從而致使併發能力下降。Mysql的缺省隔離模式爲可重複讀,實際應用中大都採用已提交讀,這兩種隔離級別的主要差別有兩點:1、可重複讀特性;2、間隙鎖的鎖定模式。(其餘兩種隔離模式應用比較少)。事務隔離級別在一致性讀特性上的影響是針對當前事務的,也就是說:若是事務A的隔離級別是未提交讀,則無論其餘事務的隔離級別是如何設置的,A總會讀到其餘事務的中間狀態。可是對於鎖的影響,則是以最早對記錄加鎖的事務的隔離級別爲準,例如:一個設置爲未提交讀模式的事務使用FOR UPDATE對某個範圍的記錄進行了鎖操做,另一個可重複讀模式的事務容許在該範圍內進行插入操做,反之,則插入操做會被阻塞。因爲採用不一樣隔離級別將會致使難以預計的複雜性,所以,不要針對單個鏈接設定事務的隔離級別,只針對數據庫設定全局的事務隔離級別。
2.3 自動提交模式
使用自動提交模式,意味着任何SQL語句都在一個事務當中。即便沒有顯式的調用BEGIN之類的命令,Mysql也會加上BEGIN,這些場景可能包括:建了一個鏈接後、COMMIT以後、ROLLBACK以後…、建立表以後…。使用SET AUTOCOMMIT來設置鏈接的事務提交模式:1-自動提交;0-關閉自動提交。
MYSQL缺省使用自動提交模式,可是什麼是自動提交呢?首先來作一個測試,在兩個終端A和B上執行以下MySQL操做:
隔離級別(缺省=可重複讀)提交模式(缺省=1:自動提交) | ||
TIME | | | V |
終端A | 終端B |
update test.my_table set F=1; | ||
select F from test.my_table; |
B的查詢結果是1,「任何SQL語句都在一個事務當中」,由於update語句老是位於一個事務當中,然而A沒有進行顯式提交(COMMIT),B在A執行完UPDATE以後,就能看到A的修改了,因此,說明在update以後,MySQL進行了自動提交操做,因此這種模式叫作自動提交模式。這種行爲符合咱們的常識。那下面再看看提交模式爲0:關閉自動提交。
隔離級別(缺省=可重複讀)提交模式(缺省=0:關閉自動提交) | ||
TIME | | | | v |
終端A | 終端B |
SET AUTOCOMMIT=0; | ||
USE test; | ||
USE test; | ||
UPDATE my_table SET F=1; | ||
SELECT F from my_table; |
假定執行前test.my_table的F字段爲0,而B查詢到的結果就是0。A終端在創建數據庫鏈接以後就已是一個事務當中,可是因爲關閉了自動提交,因此,這個事務必須在明確的執行一個COMMIT、ROLLBACK或者其餘隱式的COMMIT和ROLLBACK後纔會結束,因此B查詢出來的結果爲0。
2.4 demo小測試
不一樣的隔離級別下,對數據庫的一致性讀問題:髒讀,不可重複讀,幻讀的解決程序是不同的,下面demo開始前設置:提交模式=0(關閉自動提交模式),SET AUTOCOMMIT=0;
use test; create table T(Fid int(11), Fname varchar(24), primary key(Fid)) engine=InnoDB; insert into T(Fid, Fname) values(1, 'superman'); insert into T(Fid, Fname) values(2, 'Jack');
A:髒讀和不可重複讀的差異?
髒讀,不可重複讀 | ||
TIME | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
update t set Fname ='loleina' where Fid='1' ; | ||
select * from t; | ||
commit; | ||
select * from t; | ||
commit; | ||
select * from t; | ||
現象:未提交讀:事務B在第一個select sql執行時就能讀取到事務A更新的數據。事務A未提交,事務B也會讀取該數據。事務B讀取的數據有多是事務中的中間狀態數據,會有髒讀的問題。 已提交讀:事務B在第二個select sql執行時才能讀取到事務A更新的數據。事務A未提交,事務B不能看見事務A所作的修改,可見已提交讀,解決了髒讀的問題,可是事務B的過程當中2次讀取的數據會不一致,出現了不可重複讀的問題。 可重複讀:事務B在第三個select sql執行時才能讀取到事務A更新的數據。事務B的過程當中2次讀取的數據一致,解決了不可重複讀的問題。 |
不可重讀與髒讀的區別是:髒讀是讀到事務中間過程的臨時數據,數據處於一個臨時的非穩定狀態,可能被該事務再次修改或者回滾;而不可重複讀是因爲兩次讀取時間點的間隙中,數據被其餘事務修改而致使的數據不一致,這種修改對數據庫的影響是永久性的。
B:幻讀是什麼?
幻讀,並非說兩次讀取獲取的結果集不一樣,幻讀側重的方面是某一次的 select 操做獲得的結果所表徵的數據狀態沒法支撐後續的業務操做。更爲具體一些:select 某記錄是否存在,不存在,準備插入此記錄,但執行 insert 時發現此記錄已存在,沒法插入;或者更新某範圍的全部值時,明明都已經所有更新了,最後提交的時候發現新增了一條數據竟然沒有更新,此時就發生了幻讀。
下面以可重複讀(Repeatable Read)模式下爲例:
幻讀 | ||
TIME | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
update t set Fname ='loleina' where Fid >=0 AND Fid <='2' ; | ||
insert into T(Fid, Fname) values(0, 'Jack'); | ||
commit; | ||
commit; | ||
現象: 業務側的真實想法是把Fid從0-2範圍內的記錄的值的Fname全更新,事務提交後查詢,竟然發現有Fid=0的記錄未完成需求。 |
在RR隔離級別下,這種業務場景,是能夠經過加間隙鎖來防止該類事件的發生。一般就是在查詢語句後面加上for update,如:select * from t where Fid>=0 and Fid <=2 for update;行級鎖其實是索引鎖。可是須要注意的是在不一樣的隔離模式下,不一樣的SQL語句在是否使用間隙鎖上存在較大的差別,例如SELECT … FOR UPDATE在READ COMMITTED模式下,並不會使用間隙鎖,這種狀況下容許插入索引間隙的記錄;而REPETABLE READ模式下,則會使用間隙鎖,不容許插入鎖定的索引範圍內的記錄。有關各類語句的鎖定方式請參考後續章節更詳細的說明。
3.1 常見的鎖類型
若是三個進程,第一個進程企圖修改t表的Fid=1的Fname值爲"test",第二個進程企圖修改值爲"develop",第三個進程但願能查詢該值,若是不作任何的併發控制,第三個查詢進程返回的值是不肯定的,有可能會返回test,也有可能會返回develop,不管返回哪個數據,都存在數據丟失的狀況。解決這類經典問題的方法就是併發控制,能夠經過實現一個由兩種類型的鎖組成的鎖系統來解決。一個叫共享鎖(shared lock),也叫讀鎖(read lock),還有一個叫排他鎖(exclusive lock),也叫寫鎖(write lock)。先大概描述下鎖的概念:讀鎖是共享的。多個客戶在同一時刻能夠同時讀取同一個資源,互不干擾。而寫鎖是排他的,一個寫鎖會阻塞其餘的寫鎖和讀鎖。只有這樣,才能確保在給定的時間內,只有一個進程能執行寫入,並防止其餘用戶讀取正在寫入的同一資源。
3.2 常見的鎖策略
一種提升共享資源併發性的方式就是讓鎖定對象更有選擇性。儘可能只鎖定須要修改的部分數據,而不是全部的資源。在給定的資源上,鎖定的數據量越少,則系統的併發程序就越高,只要處理好關係,相互之間不發生衝突便可。加鎖是會消耗資源的,鎖的各種操做,包括獲取鎖,檢查鎖是否已經解除,釋放鎖等等,都是會增長系統的開銷的。若是系統花費大量的時間來管理鎖,而不是存取數據,那系統的性能可能會所以收到影響。MySQL的不一樣存儲引擎均可以實現本身的鎖策略和鎖粒度,經常使用的有兩種重要的鎖策略。
表鎖:是MySQL中最基本的鎖策略,開銷也是最小的。表鎖會鎖定整張表,一個用戶在對錶進行寫操做時前,須要選得到寫鎖,可是這會阻塞其餘用戶對該表的全部的讀寫操做。只能在沒有寫鎖時,其餘讀取的用戶才能獲取讀鎖。寫鎖比讀鎖有更高的優先級,所以一個寫鎖請求可能會被插入到讀鎖隊列的前面。
行鎖:行級鎖能夠最大程序的支持併發處理(同時也帶來了最大的鎖開銷),在MySQL中,行級鎖只在存儲引擎層實現,而在服務器層沒有實現,服務器層徹底不瞭解存儲引擎中的鎖實現。
行級鎖中,InnoDB引入了一種叫作下一鍵鎖定(next-key locking)的算法,俗稱"間隙鎖"。它經過以下方式實現行級鎖:掃描表索引,並對符合條件的索引項設置共享鎖/排他鎖。所以,行級鎖其實是索引鎖。InnoDB設置在索引上的鎖也會影響索引項以前的間隙(鍵值在索引範圍內但並不存在的記錄)。若是一個用戶經過索引對記錄R設置了共享鎖或排他鎖,那麼另外一個用戶不能在記錄R以前(按索引順序)插入一個新的索引項。這種鎖定間隙的方式用於阻止所謂的「幻讀」問題,例如假定你但願讀取並鎖定全部的標識大於100的child記錄,以便更新該範圍內的某些字段,下邊是查找的SQL(假定id是一個索引字段):SELECT * FROM child WHERE id > 100 FOR UPDATE;該查詢從第一個id大於100的索引開始掃描,如果設置在索引上的鎖不能鎖定索引間隙的話,就存在更新過程當中有新記錄被插入的可能,當從新執行一次一樣的查詢時,就可能會在查詢返回的結果集中發現新插入的行,這與事務的隔離原則時違背的:必須在整個事務的執行過程當中,它所讀取的數據沒有發生變動。在應用程序中,可使用間隙鎖進行惟一性檢查:使用共享模式讀取數據時,若是沒有找到即將要插入的記錄,那麼就能夠安全的插入新的記錄,由於在記錄上設置的間隙鎖能夠阻止其餘人同時插入一條一樣的記錄。也就是說,間隙鎖容許鎖定表中並不存在的記錄。
間隙鎖在InnoDB的惟一做用就是防止其它事務的插入操做,以此來達到防止幻讀的發生,因此間隙鎖不分什麼共享鎖與排它鎖。若是InnoDB掃描的是一個主鍵、或是一個惟一索引的話,那InnoDB只會採用行鎖方式來加鎖,而不會使用Next-Key Lock的方式,也就是說不會對索引之間的間隙加鎖,
3.3 經常使用的鎖方法
悲觀鎖:正如其名,它指的是對數據被外界(包括本系統當前的其餘事務,以及來自外部系統的事務處理)的修改持保守態度,所以,在整個數據處理過程當中,將數據處於鎖定狀態。悲觀鎖的實現,每每依靠數據庫提供的鎖機制(也只有數據庫層提供的鎖機制才能真正保證數據訪問的排他性,不然,即便在本系統中實現了加鎖機制,也沒法保證外部系統不會修改數據)。悲觀併發控制主要用於數據爭用激烈的環境,以及發生併發衝突時使用鎖保護數據的成本要低於回滾事務的成本的環境中。經常使用的手段是使用select..for update來實現行鎖機制,例如2.4的第一個demo裏修改成select * from t where Fid='1' for update;時,其餘的寫操做都會被阻塞,此刻其餘事務只能進行讀操做。只有當前事務能夠對該數據進行修改操做。鎖住選中的數據行或者數據集,待後面sql更新操做完成事務提交後,再釋放鎖。
須要注意的是,在事務中,只有SELECT ... FOR UPDATE 或LOCK IN SHARE MODE 同一筆數據時會等待其它事務結束後才執行,使用select…for update會把數據給鎖住,不過須要注意一些鎖的級別,MySQL InnoDB默認Row-Level Lock,因此只有明確地指定主鍵,MySQL 纔會執行Row lock (只鎖住被選取的數據) ,不然MySQL 將會執行Table Lock (將整個數據表單給鎖住)。除了主鍵外,使用索引也會影響數據庫的鎖定級別。
樂觀鎖:在關係數據庫管理系統裏,樂觀併發控制(又名「樂觀鎖」,Optimistic Concurrency Control,縮寫「OCC」)是一種併發控制的方法。它假設多用戶併發的事務在處理時不會彼此互相影響,各事務可以在不產生鎖的狀況下處理各自影響的那部分數據。在提交數據更新以前,每一個事務會先檢查在該事務讀取數據後,有沒有其餘事務又修改了該數據。若是其餘事務有更新的話,正在提交的事務會進行回滾。樂觀事務控制最先是由孔祥重(H.T.Kung)教授提出。相對悲觀鎖而言,樂觀鎖假設認爲數據通常狀況下不會形成衝突,因此在數據進行提交更新的時候,纔會正式對數據的衝突與否進行檢測,若是發現衝突了,則讓返回用戶錯誤的信息,讓用戶決定如何去作。那麼如何實現樂觀鎖呢,通常來講有如下2種方式:
1.使用數據版本(Version)記錄機制實現,這是樂觀鎖最經常使用的一種實現方式。何謂數據版本?即爲數據增長一個版本標識,通常是經過爲數據庫表增長一個數字類型的 「version」 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值+1。當提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,若是數據庫表當前版本號與第一次取出來的version值相等,則予以更新,不然認爲是過時數據。
2. 第二種實現方式和第一種差很少,一樣是在須要樂觀鎖控制的table中增長一個字段,字段類型使用時間戳(timestamp), 和上面的version相似,也是在更新提交的時候檢查當前數據庫中數據的時間戳和本身更新前取到的時間戳進行對比,若是一致則OK,不然就是版本衝突。
通用以2.4的第一個小demo爲例,若是使用樂觀鎖,本質上就是遵照一個加前置條件更新的規則。則大體實現過程以下 ;
begin;
select (Fid,Fname) from t where Fid=1;-- 假設查詢回來值爲(1,‘’test')
update t set Fname=‘develop’ where id=1 and Fname='test';
commit;
這種狀況下,更新數據是先get,修改get回來的數據,而後put回系統。若是事務在更新以前Fname已經被其餘事務修改了,這當前事務更新的時候就會失敗。
4.1 SELECT … IN SHARE MODE和SELECT … FOR UPDATE
2.4下的兩個demo驗證測試,充分的說明了在經常使用的RR和RC模式下,單靠事務隔離級別來解決併發引入的問題是不可能的。即便使用RR,仍然存在幻讀的可能,InnoDB的事務ACID屬性除了與事務的隔離級別有關以外,還須要InnoDB的行鎖支持,鎖是InnoDB事務的一部分,是解決事務併發問題的核心機制。併發問題不單是事務機制須要考慮的問題,更是業務須要考慮的問題。InnoDB鎖操做的影響與索引、事務隔離級別等緊密關聯。
經常使用的兩種在事務中加鎖的方法是,使用SELECT … IN SHARE MODE和SELECT … FOR UPDATE,SELECT … IN SHARE MODE是加共享鎖進行讀取時,意味着能獲取到數據最後的更新狀態,並同時爲該記錄設置一個共享鎖。共享鎖能夠阻止其餘操做對該記錄的更新和刪除操做。可是別的事務也能夠同時獲取該共享鎖,此時若是當前事務企圖更新該條語句,則會出現死鎖,下面是一個小測試。
SELECT … IN SHARE MODE可能會產生死鎖 | ||
TIME | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
select * from t where Fid =0 lock in share mode; | ||
select * from t where Fid =0 lock in share mode; | ||
update t set Fname ='wang' where Fid=0; | ||
update t set Fname ='wang' where Fid=0; | ||
現象: 事務A企圖執行update 語句時,會報獲取鎖超時的錯誤:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction。由於事務B查詢數據的時候使用了SELECT … IN SHARE MODE,獲取了共享鎖致使。同理,事務B也不能執行更新語句,最後兩邊相互等待共享鎖釋放發致使死鎖超時。 |
若是把刪除的小測試的語句換成SELECT … FOR UPDATE,就能很好的避免這個問題。
SELECT … FOR UPDATE 惟一索引加排他鎖 | ||
TIME | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
select * from t where Fid =0 for update; | ||
select * from t where Fid =0 ; | ||
select * from t where Fid =0 for update; | ||
update t set Fname ='wang' where Fid=0; | ||
commit; | ||
現象: 事務A查詢時對惟一索引Fid使用SELECT … FOR UPDATE時,加了排他鎖,事務B此刻能夠正常查詢Fid =0 的數據,可是若是也但願執行通用的sql,則會提示獲取鎖超時錯誤,最終事務A能按照預期更新數據。 |
4.2 事務與鎖
InnoDB的實現遵循標準的行鎖方式,支持兩種類型的鎖:共享鎖(S鎖)用於讀取行(記錄);排他鎖(X鎖)用於更新或者刪除行。結合4.1的兩個小測試說明下結果:
使用SELECT … FOR UPDATE,在不一樣的狀況下,mysql採起的鎖策略是不同的,這裏的不一樣,包括隔離級別的不一樣,where 條件字段是不是惟一索引,查詢時記錄是否存在等常見狀況。下面對此一一作個小測試:
隔離級別(已提交讀) | ||
TIME | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from T where Fid=1 for update; | ||
select * from T where Fid=2 for update; | ||
select * from T where Fid=1 for update; | ||
提示:此處被阻塞 | ||
commit; | ||
commit; | ||
現象:在B上執行的第一個select查詢順利執行,可是第二個select被阻塞,直等到終端A的事務被提交或者等待鎖超時爲止。 | ||
結論:在已提交讀的模式下,針對惟一索引使用FOR UPDATE會對該記錄(已存在)加排他鎖。 | ||
隔離級別(可重複讀) | ||
TIME | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
select * from T where Fid=1 for update; | ||
select * from T where Fid=2 for update; | ||
select * from T where Fid=1 for update; | ||
提示:此處被阻塞 | ||
commit; | ||
commit; | ||
現象:在B上執行的第一個select查詢順利執行,可是第二個select被阻塞,直等到終端A的事務被提交或者等待鎖超時爲止。 | ||
結論:在可重複讀模式下,使用FOR UPDATE會對記錄(已存在)加排他鎖,其表現與已提交讀模式相同。 |
隔離級別(可重複讀) | ||
TIME | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
select * from t where Fname='wang' for update; | ||
select * from t where Fname='wang' for update; | ||
提示:此處被阻塞 | ||
select * from t where Fname='loleina' for update; | ||
提示:此處被阻塞 | ||
commit; | ||
commit; | ||
現象:在B上執行的第一個select查詢被阻塞,第二個select也被阻塞,直等到終端A的事務被提交或者等待鎖超時爲止。 | ||
結論:在可重複讀的模式下,針對非索引的字段使用FOR UPDATE會對全部記錄(已存在)加排他鎖。 | ||
隔離級別(已提交讀) | ||
TIME | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin; | |
select * from t where Fname='wang' for update; | ||
select * from t where Fname='wang' for update; | ||
提示:此處被阻塞 | ||
select * from t where F name='loleina' for update; |
||
提示:此處被阻塞 | ||
commit; | ||
commit; | ||
現象:在B上執行的第一個select被阻塞,第二個select也會被阻塞,直等到終端A的事務被提交或者等待鎖超時爲止。 | ||
結論:在提交讀模式下,對非索引字段使用FOR UPDATE會對該表的全部記錄(已存在)加排他鎖,其表現與已提交讀模式相同。 |
隔離級別(可重複讀) | ||
TIME | | | | | | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from T where Fid=3 for update; | select * from T where Fid=3 for update; | |
insert into T(Fid, Fname) values(3, ‘bad’); | ||
提示:此處被阻塞 | ||
insert into T(Fid, Fname) values(3, ‘bad’); | ||
提示:此處發生死鎖,事務被重啓 | ||
insert into T(Fid, Fname) values(4, ‘bad’); | ||
提示:此處發生死鎖,事務被重啓 | ||
提示:B的事務被重啓後,阻塞被解除 | ||
commit; | ||
現象:在A和B上分別用FOR UPDATE鎖定不存在的記錄時,沒有阻塞,可是當A執行插入操做時被阻塞;此時若是B繼續執行插入,則B報死鎖,事務被重啓,因爲B事務重啓會釋放B佔有的鎖,A的阻塞被解除。 | ||
我的理解:在重複讀模式下,當使用FOR UPDATE鎖定不存在的惟一索引時,InnoDB認爲用戶企圖插入新的記錄,會使用意圖鎖(IX)鎖定全表的插入操做,以阻塞其餘插入請求(不影響已存在記錄的更新)。因爲IX和IX是兼容的,所以,多個FOR UPDATE是被容許的,當A執行insert操做時,A必須得到X鎖,因爲B已經得到了IX鎖,因此A被阻塞,而後B也企圖得到X鎖,InnoDB檢測到死鎖,從而重啓B的事務並釋放B的鎖資源。 | ||
結論:在可重複讀模式下,使用FOR UPDATE對不存在的記錄加鎖時,InnoDB不會阻塞在FOR UPDATE處,從而在使用FOR UPDATE+INSERT的模式執行插入時,在併發條件下幾乎不可避免的會致使死鎖的發生。 | ||
隔離級別(已提交讀) | ||
TIME | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from T where Fid=3 for update; | select * from T where Fid=3 for update; | |
Insert into T(Fid,Fname) values(3, ‘ok’) | ||
Insert into T(Fid,Fname) values(3, ‘bad’) | ||
提示:被阻塞 | ||
commit | ||
提示:報主鍵衝突 | ||
現象:在B上執行的select未被阻塞,A上的插入操做能夠執行,B的插入操做被阻塞,當A的事務被提交時,B的插入操做會繼續,而後報主鍵衝突。 | ||
我的理解:在已提交讀模式下鎖定不存在的惟一索引使用的是排他鎖(非間隙),而記錄並不存在,因此並不會對記錄加鎖。爲了進一步驗證上述猜想,將B的主鍵鍵值改成其餘值,發現A和B的操做都能順利進行,說明這種狀況下InnoDB確實沒有使用間隙鎖來禁止插入操做。 | ||
結論:在已提交讀模式下,用惟一索引進行FOR UPDATE操做不能阻止插入重複記錄,可能會致使主鍵衝突。 |
下面的測試須要使用非惟一索引,爲此創建一張新表:
CREATE TABLE `H` ( `Fid` int(11) DEFAULT NULL, `Fname` varchar(24) DEFAULT NULL, KEY `Fid` (`Fid`,`Fname`) ) ENGINE=InnoDB DEFAULT CHARSET=gbk insert into H(Fid, Fname) values(1, 'superman'); insert into H(Fid, Fname) values(2, 'JACK');
隔離級別(可重複讀) | ||
TIME | | | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from H where Fid=1 for update; | ||
select * from H where Fid=2 for update; | ||
select * from H where Fid=1 for update; | ||
提示:此處被阻塞 | ||
commit; | ||
commit; | ||
現象:在B上執行的第一個select查詢順利執行,可是第二個select被阻塞,直等到終端A的事務被提交或者等待鎖超時爲止。 | ||
結論:在可重複讀模式下,使用FOR UPDATE對不存在的記錄加鎖時,InnoDB會使用排他鎖對索引進行加鎖,阻止其餘事務對該索引的加鎖操做,可是並不會對其餘非相關索引值設置間隙鎖。 | ||
隔離級別(已提交讀) | ||
TIME | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from H where Fid=1 for update; | ||
select * from H where Fid=1 for update; | ||
提示:在此處會被阻塞 | ||
commit | ||
現象:在B上執行的select被阻塞,直等到終端A的事務被提交或者等待鎖超時爲止。 | ||
結論:使用非惟一索引(或聯合惟一索引的部分字段)鎖定已存在記錄時,已提交讀和可重複讀的鎖特性並無區別。 |
隔離級別(可重複讀) | ||
TIME | | | | | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from H where Fid=3 for update; | ||
select * from H where Fid=3 for update; | ||
insert into H(Fid, Fname) values(3, ‘bad‘) | ||
提示:此處被阻塞 | insert into H(Fid, Fname) values(3, ‘bad‘) | |
insert into H(Fid, Fname) values(4, ‘bad‘) | ||
提示:此處出現死鎖 | ||
現象:在B上執行的select查詢順利執行,可是A上的insert語句會被阻塞,須要注意的是,即便將B的Fid改成其餘不存在的值,A上的insert也一樣會被阻塞。 | ||
我的理解:用FOR UPDATE鎖定不存在的記錄時,InnoDB認爲用戶企圖插入新的記錄,會使用意圖鎖(IX)鎖(鎖間隙)定全表的插入操做,以阻塞其餘插入請求(不影響已存在記錄的更新)。因爲IX和IX是兼容的,所以,多個FOR UPDATE是被容許的,當A執行insert操做時,A必須得到X鎖,因爲B已經得到了IX鎖,因此A被阻塞,而後B也企圖得到X鎖,InnoDB檢測到死鎖,從而重啓B的事務並釋放B的鎖資源。 | ||
結論:使用非惟一索引鎖定不存在記錄時,會鎖定索引對應的所有記錄。 | ||
隔離級別(已提交讀) | ||
TIME | | | | | | | | | | | | | | | | v |
終端A | 終端B |
use test; | use test; | |
begin; | begin | |
select * from H where Fid=3 for update; | select * from H where Fid=3 for update; | |
insert into H(Fid, Fname) values(3, 'Tom'); | ||
insert into H(Fid,Fname) values(3, ‘Win’); | ||
提示:此處被阻塞,直到A提交或超時 | ||
insert into H(Fid, Fname) values(4, 'Tom'); | ||
commit | commit | |
現象:在B中第一個事務裏邊的insert語句會被阻塞,若A執行提交,則B的insert會報主鍵衝突;而第二個事務中A和B的insert均可以成功。 | ||
分析:因爲在已提交讀隔離級別下,使用非惟一索引時, A和B的FOU UPDAET蛻化爲共享鎖,所以SELECT語句均可以順利執行;第一個事務中,當A執行insert操做時,對索引是使用了排他鎖(非間隙鎖),因此,B的插入操做被阻塞;第二個事務當中,A和B的insert操做雖然都是排他鎖,可是因爲針對的是不一樣索引,因此互相不影響。 | ||
結論:在已提交讀隔離級別下,使用FOR UPDATE並不能鎖定記錄的插入,可能會出現主鍵衝突的錯誤。 |
測試五和測試三說明:在RC(已提交讀)和RR(可重複讀)隔離級別下,無論是使用惟一索引仍是非惟一索引鎖定不存在記錄。RC模式下,使用的是排他鎖,會有一個能執行成功,另外一個事務插入時提示主鍵衝突,不影響其餘行的插入;而RR模式下會使用意圖鎖(IX)鎖定全表的插入操做,兩個事務都不能執行成功,且影響別的記錄插入。
測試四和測試一說明:在RC(已提交讀)和RR(可重複讀)隔離級別下,無論是惟一索引仍是使用非惟一索引鎖定存在記錄。都會對該條記錄加排他鎖,其餘事務只能讀取數據,不能更新數據。跟測試一針對惟一索引鎖定存在記錄同樣。
測試二說明:使用非索引字段,在任何隔離級別下,都會鎖全表。