爲啥要進行併發控制?程序員
併發的任務對同一個臨界資源進行操做,若是不採起措施,可能致使不一致,故必須進行併發控制(Concurrency Control)。數據庫
技術上,一般如何進行併發控制?服務器
經過併發控制保證數據一致性的常見手段有:session
鎖(Locking)架構
數據多版本(Multi Versioning)併發
事務之間的隔離,是經過鎖機制實現的。當一個事務須要對數據庫中的某行數據進行修改時,須要先給數據加鎖;加了鎖的數據,其它事務是不運行操做的,只能等待當前事務提交或回滾將鎖釋放。鎖機制並非一個陌生的概念,在許多場景中都會利用到不一樣實現的鎖對數據進行保護和同步。而在MySQL中,根據不一樣的劃分標準,還可將鎖分爲不一樣的種類。高併發
InnoDB內核的第一種鎖:自增鎖(Auto-inc Locks)性能
InnoDB內核的三種鎖:共享/排他鎖(Shared and Exclusive Locks)、意向鎖(Intention Locks)、插入意向鎖(Insert Intention Locks)學習
InnoDB三種細粒度行鎖:記錄鎖(Record Locks)、間隙鎖(Gap Locks)、臨鍵鎖(Next-Key Locks)ui
粒度:指數據倉庫的數據單位中保存數據的細化或綜合程度的級別。細化程度越高,粒度級就越小;相反,細化程度越低,粒度級就越大。
MySQL按照鎖的粒度劃分能夠分爲行鎖、表鎖和頁鎖。
數據庫的粒度劃分
這三種鎖是在不一樣層次上對數據進行鎖定,因爲粒度的不一樣,其帶來的好處和劣勢也不一而同。
表鎖在操做數據時會鎖定整張表,於是併發性能較差;行鎖則只鎖定須要操做的數據,併發性能好。可是因爲加鎖自己須要消耗資源(得到鎖、檢查鎖、釋放鎖等都須要消耗資源),所以在鎖定數據較多狀況下使用表鎖能夠節省大量資源。
MySQL中不一樣的存儲引擎可以支持的鎖也是不同的。MyIsam只支持表鎖,而InnoDB同時支持表鎖和行鎖,且出於性能考慮,絕大多數狀況下使用的都是行鎖。
如何使用普通鎖保證一致性?
普通鎖,被使用最多:
如此這般,來保證一致性。
普通鎖存在什麼問題?
簡單的鎖住太過粗暴,連「讀任務」也沒法並行,任務執行過程本質上是串行的。
因而出現了共享鎖與排他鎖:
共享鎖(Share Locks,記爲S鎖),讀取數據時加S鎖
排他鎖(eXclusive Locks,記爲X鎖),修改數據時加X鎖
共享鎖與排他鎖的玩法是:
共享鎖之間不互斥,簡記爲:讀讀能夠並行
排他鎖與任何鎖互斥,簡記爲:寫讀,寫寫不能夠並行
能夠看到,一旦寫數據的任務沒有完成,數據是不能被其餘任務讀取的,這對併發度有較大的影響。
畫外音:對應到數據庫,能夠理解爲,寫事務沒有提交,讀相關數據的select也會被阻塞。
有沒有可能,進一步提升併發呢?
即便寫任務沒有完成,其餘讀任務也可能併發,這就引出了數據多版本。
-- 表上共享鎖 LOCK TABLE product_comment READ; UPDATE product_comment SET product_id = 10002 WHERE user_id = 912178; -- ERROR 1099 (HY000): Table 'product_comment' was locked with a READ lock and can't be updated UNLOCK TABLE; -- 行上共享鎖 SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 LOCK IN SHARE MODE; -- 表上排他鎖 LOCK TABLE product_comment WRITE; UNLOCK TABLE; -- 行上排他鎖 SELECT comment_id, product_id, comment_text, user_id FROM product_comment WHERE user_id = 912178 FOR UPDATE;
1)記錄鎖(Record Locks)
記錄鎖,它封鎖索引記錄,例如:
select * from t where id=1 for update;
2)間隙鎖(Gap Locks)
間隙鎖,它封鎖索引記錄中的間隔,或者第一條索引記錄以前的範圍,又或者最後一條索引記錄以後的範圍。
間隙鎖的主要目的,就是爲了防止其餘事務在間隔中插入數據,以致使「不可重複讀」。
若是把事務的隔離級別降級爲讀提交(Read Committed, RC),間隙鎖則會自動失效。
3)臨鍵鎖(Next-Key Locks)
臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖範圍,既包含索引記錄,又包含索引區間。
更具體的,臨鍵鎖會封鎖索引記錄自己,以及索引記錄以前的區間。
若是一個會話佔有了索引記錄R的共享/排他鎖,其餘會話不能馬上在R以前的區間插入新的索引記錄。
If one session has a shared or exclusive lock on record R in an index, another session cannot insert a new index record in the gap immediately before R in the index order.
臨鍵鎖的主要目的,也是爲了不幻讀(Phantom Read)。若是把事務的隔離級別降級爲RC,臨鍵鎖則也會失效。
若是從程序員的視角來看鎖的話,能夠將鎖分紅樂觀鎖和悲觀鎖,從名字中也能夠看出這兩種鎖是兩種看待數據併發的思惟方式。
樂觀鎖(Optimistic Locking)認爲對同一數據的併發操做不會總髮生,屬於小几率事件,不用每次都對數據上鎖,也就是不採用數據庫自身的鎖機制,而是經過程序來實現。在程序上,咱們能夠採用版本號機制或者時間戳機制實現。
樂觀鎖的版本號機制
在表中設計一個版本字段 version,第一次讀的時候,會獲取 version 字段的取值。而後對數據進行更新或刪除操做時,會執行UPDATE ... SET version=version+1 WHERE version=version
。此時若是已經有事務對這條數據進行了更改,修改就不會成功。
這種方式相似咱們熟悉的 SVN、CVS 版本管理系統,當咱們修改了代碼進行提交時,首先會檢查當前版本號與服務器上的版本號是否一致,若是一致就能夠直接提交,若是不一致就須要更新服務器上的最新代碼,而後再進行提交。
樂觀鎖的時間戳機制
時間戳和版本號機制同樣,也是在更新提交的時候,將當前數據的時間戳和更新以前取得的時間戳進行比較,若是二者一致則更新成功,不然就是版本衝突。
你能看到樂觀鎖就是程序員本身控制數據併發操做的權限,基本是經過給數據行增長一個戳(版本號或者時間戳),從而證實當前拿到的數據是否最新。
悲觀鎖(Pessimistic Locking)也是一種思想,對數據被其餘事務的修改持保守態度,會經過數據庫自身的鎖機制來實現,從而保證數據操做的排它性。
咱們都不但願出現死鎖的狀況,能夠採起一些方法避免死鎖的發生:
咱們知道事務有 4 個隔離級別,以及可能存在的三種異常問題,以下圖所示:
在 MySQL 中,默認的隔離級別是可重複讀,能夠解決髒讀和不可重複讀的問題,但不能解決幻讀問題。若是咱們想要解決幻讀問題,就須要採用串行化的方式,也就是將隔離級別提高到最高,但這樣一來就會大幅下降數據庫的事務併發能力。
有沒有一種方式,能夠不採用鎖機制,而是經過樂觀鎖的方式來解決不可重複讀和幻讀問題呢?實際上 MVCC 機制的設計,就是用來解決這個問題的,它能夠在大多數狀況下替代行級鎖,下降系統的開銷。
MVCC就是用來實現上面的第三個隔離級別,可重複讀RR。
MVCC:Multi-Version Concurrency Control,即多版本的併發控制協議。
從名字中也能看出來,MVCC 是經過數據行的多個版本管理來實現數據庫的併發控制,簡單來講它的思想就是保存數據的歷史版本。這樣咱們就能夠經過比較版本號決定數據是否顯示出來(具體的
規則後面會介紹到),讀取數據的時候不須要加鎖也能夠保證事務的隔離效果。
經過 MVCC 咱們能夠解決如下幾個問題:
MVCC的特色就是在同一時刻,不一樣事務能夠讀取到不一樣版本的數據,從而能夠解決髒讀和不可重複讀的問題。
MVCC實際上就是經過數據的隱藏列和回滾日誌(undo log),實現多個版本數據的共存。這樣的好處是,使用MVCC進行讀數據的時候,不用加鎖,從而避免了同時讀寫的衝突。
在實現MVCC時,每一行的數據中會額外保存幾個隱藏的列,好比當前行建立時的版本號和刪除時間和指向undo log的回滾指針。這裏的版本號並非實際的時間值,而是系統版本號。每開始新的事務,系統版本號都會自動遞增。事務開始時的系統版本號會做爲事務的版本號,用來和查詢每行記錄的版本號進行比較。每一個事務又有本身的版本號,這樣事務內執行數據操做時,就經過版本號的比較來達到數據版本控制的目的。
提升併發的演進思路,就在如此:
普通鎖,本質是串行執行
讀寫鎖,能夠實現讀讀併發
數據多版本,能夠實現讀寫併發
畫外音:這個思路,比整篇文章的其餘技術細節更重要,但願你們牢記。
1)MVCC是怎樣實現隔離級別的
MVCC多版本併發控制是MySQL中基於樂觀鎖理論實現隔離級別的方式,用於讀已提交和可重複讀取隔離級別的實現。
在MySQL中,會在表中每一條數據後面添加兩個字段:最近修改該行數據的事務ID,指向該行(undolog表中)回滾段的指針。
Read View判斷行的可見性,建立一個新事務時,copy一份當前系統中的活躍事務列表。意思是,當前不該該被本事務看到的其餘事務id列表。
已提交讀隔離級別下的事務在每次查詢的開始都會生成一個獨立的ReadView,而可重複讀隔離級別則在第一次讀的時候生成一個ReadView,以後的讀都複用以前的ReadView。
2)核心問題
舊版本數據存儲在哪裏?
舊版本數據存儲在回滾段裏;
存儲舊版本數據,對MySQL和InnoDB原有架構是否有巨大沖擊?
對MySQL和InnoDB原有架構體系衝擊不大;
3)InnoDB爲什麼可以作到這麼高的併發?
回滾段裏的數據,實際上是歷史數據的快照(snapshot),這些數據是不會被修改,select能夠肆無忌憚的併發讀取他們。
快照讀(Snapshot Read),這種一致性不加鎖的讀(Consistent Nonlocking Read),就是InnoDB併發如此之高的核心緣由之一。
這裏的一致性是指,事務讀取到的數據,要麼是事務開始前就已經存在的數據(固然,是其餘已提交事務產生的),要麼是事務自身插入或者修改的數據。
什麼樣的select是快照讀?
除非顯示加鎖,普通的select語句都是快照讀,例如:
select * from t where id>2;
這裏的顯示加鎖,非快照讀(當前讀,包括了加鎖的讀取和 DML 操做)是指:
select * from t where id>2 lock in share mode; select * from t where id>2 for update; INSERT INTO player values ... DELETE FROM player WHERE ... UPDATE player SET ...
MVCC 能夠解決讀寫互相阻塞的問題,這樣提高了效率,同時由於採用了樂觀鎖的思想,下降了死鎖的機率。
4)數據的隱藏列具體是指什麼?
InnoDB的內核,會對全部row數據增長三個內部屬性:
5)Read View 是如何工做的
在 MVCC 機制中,多個事務對同一個行記錄進行更新會產生多個歷史快照,這些歷史快照保存在 Undo Log 裏。若是一個事務想要查詢這個行記錄,須要讀取哪一個版本的行記錄呢?這時就須要用到 Read View 了,它幫咱們解決了行的可見性問題。Read View 保存了當前事務開啓時全部活躍(尚未提交)的事務列表,換個角度你能夠理解爲 Read View 保存了不該該讓這個事務看到的其餘的事務 ID 列表。
在 Read VIew 中有幾個重要的屬性:
如圖所示,trx_ids 爲 trx二、trx三、trx5 和 trx8 的集合,活躍的最大事務 ID(low_limit_id)爲 trx8,活躍的最小事務 ID(up_limit_id)爲 trx2。
假設當前有事務 creator_trx_id 想要讀取某個行記錄,這個行記錄的事務 ID 爲 trx_id,那麼會出現如下幾種狀況。
若是 trx_id < 活躍的最小事務 ID(up_limit_id),也就是說這個行記錄在這些活躍的事務建立以前就已經提交了,那麼這個行記錄對該事務是可見的。
若是 trx_id > 活躍的最大事務 ID(low_limit_id),這說明該行記錄在這些活躍的事務建立以後才建立,那麼這個行記錄對當前事務不可見。
若是 up_limit_id < trx_id < low_limit_id,說明該行記錄所在的事務 trx_id 在目前 creator_trx_id 這個事務建立的時候,可能還處於活躍的狀態,所以咱們須要在 trx_ids 集合中進行遍歷,若是 trx_id 存在於 trx_ids 集合中,證實這個事務 trx_id 還處於活躍狀態,不可見。不然,若是 trx_id 不存在於 trx_ids 集合中,證實事務 trx_id 已經提交了,該行記錄可見。
瞭解了這些概念以後,咱們來看下當查詢一條記錄的時候,系統如何經過多版本併發控制技術找到它:
你能看到 InnoDB 中,MVCC 是經過 Undo Log + Read View 進行數據讀取,Undo Log 保存了歷史快照,而 Read View 規則幫咱們判斷當前版本的數據是否可見。
須要說明的是,在隔離級別爲讀已提交(Read Commit)時,一個事務中的每一次 SELECT 查詢都會獲取一次 Read View。如表所示:
你能看到,在讀已提交的隔離級別下,一樣的查詢語句都會從新獲取一次 Read View,這時若是 Read View 不一樣,就可能產生不可重複讀或者幻讀的狀況。
當隔離級別爲可重複讀的時候,就避免了不可重複讀,這是由於一個事務只在第一次 SELECT 的時候會獲取一次 Read View,然後面全部的 SELECT 都會複用這個 Read View,以下表所示:
6)InnoDB 是如何解決幻讀的
不過這裏須要說明的是,在可重複讀的狀況下,InnoDB 能夠經過 Next-Key 鎖 +MVCC 來解決幻讀問題。
在讀已提交的狀況下,即便採用了 MVCC 方式也會出現幻讀。若是咱們同時開啓事務 A 和事務 B,先在事務 A 中進行某個條件範圍的查詢,讀取的時候採用排它鎖,在事務 B 中增長一條符合該條件範圍的數據,並進行提交,而後咱們在事務 A 中再次查詢該條件範圍的數據,就會發現結果集中多出一個符合條件的數據,這樣就出現了幻讀。
出現幻讀的緣由是在讀已提交的狀況下,InnoDB 只採用記錄鎖(Record Locking)。這裏要介紹下 InnoDB 三種行鎖的方式:
在隔離級別爲可重複讀時,InnoDB 會採用 Next-Key 鎖的機制,幫咱們解決幻讀問題。
仍是這個例子,咱們能看到當咱們想要插入球員艾利克斯·倫(身高 2.16 米)的時候,事務 B 會超時,沒法插入該數據。這是由於採用了 Next-Key 鎖,會將 height>2.08 的範圍都進行鎖定,就沒法插入符合這個範圍的數據了。而後事務 A 從新進行條件範圍的查詢,就不會出現幻讀的狀況。
總結
今天關於 MVCC 的內容有些多,經過學習你應該能對採用 MVCC 這種樂觀鎖的方式來保證事務的隔離效果更有體會。
咱們須要記住,MVCC 的核心就是 Undo Log+ Read View,「MV」就是經過 Undo Log 來保存數據的歷史版本,實現多版本的管理,「CC」是經過 Read View 來實現管理,經過 Read View 原則來決定數據是否顯示。同時針對不一樣的隔離級別,Read View 的生成策略不一樣,也就實現了不一樣的隔離級別。
MVCC 是一種機制,MySQL、Oracle、SQL Server 和 PostgreSQL 的實現方式均有不一樣,咱們在學習的時候,更主要的是要理解 MVCC 的設計思想。
7)臨鍵鎖(next-key lock)
InnoDB實現的隔離級別RR時能夠避免幻讀現象的,這是經過next-key lock
機制實現的。
next-key lock
實際上就是行鎖的一種,只不過它不僅是會鎖住當前行記錄的自己,還會鎖定一個範圍。好比上面幻讀的例子,開始查詢0<閱讀量<100的文章時,只查到了一個結果。next-key lock
會將查詢出的這一行進行鎖定,同時還會對0<閱讀量<100這個範圍進行加鎖,這其實是一種間隙鎖。間隙鎖可以防止其餘事務在這個間隙修改或者插入記錄。這樣一來,就保證了在0<閱讀量<100這個間隙中,只存在原來的一行數據,從而避免了幻讀。
間隙鎖:封鎖索引記錄中的間隔
雖然InnoDB使用next-key lock
可以避免幻讀問題,但卻並非真正的可串行化隔離。再來看一個例子吧。
首先提一個問題:
在T6時間,事務A提交事務以後,猜一猜文章A和文章B的閱讀量爲多少?
答案是,文章AB的閱讀量都被修改爲了10000。這表明着事務B的提交實際上對事務A的執行產生了影響,代表兩個事務之間並非徹底隔離的。雖然可以避免幻讀現象,可是卻沒有達到可串行化的級別。這還說明,避免髒讀、不可重複讀和幻讀,是達到可串行化的隔離級別的必要不充分條件。可串行化是都可以避免髒讀、不可重複讀和幻讀,可是避免髒讀、不可重複讀和幻讀卻不必定達到了可串行化。
這個月花了一些功夫寫InnoDB:併發控制,MVCC,索引,鎖...
有朋友留言:你TM講了這麼多,鎖分了這麼多類型,又和事務隔離級別相關,又和索引相關,究竟能不能直接告訴我,一個SQL到底加了什麼鎖!?
我竟無言以對。
好吧,作過簡單梳理以後,今天嘗試着直接回答,儘可能作到不重不漏,各類SQL語句究竟加了什麼鎖。
1、普通select
(1)在讀未提交(Read Uncommitted),讀提交(Read Committed, RC),可重複讀(Repeated Read, RR)這三種事務隔離級別下,普通select使用快照讀(snpashot read),不加鎖,併發很是高;
(2)在串行化(Serializable)這種事務的隔離級別下,普通select會升級爲select ... in share mode;
【快照讀】輔助閱讀:
【事務隔離級別】輔助閱讀:
2、加鎖select
加鎖select主要是指:
select ... for update
select ... in share mode
(1)若是,在惟一索引(unique index)上使用惟一的查詢條件(unique search condition),會使用記錄鎖(record lock),而不會封鎖記錄之間的間隔,即不會使用間隙鎖(gap lock)與臨鍵鎖(next-key lock);
【記錄鎖,間隙鎖,臨鍵鎖】輔助閱讀:
舉個栗子,假設有InnoDB表:
t(id PK, name);
表中有三條記錄:
1, shenjian
2, zhangsan
3, lisi
SQL語句:
select * from t where id=1 for update;
只會封鎖記錄,而不會封鎖區間。
(2)其餘的查詢條件和索引條件,InnoDB會封鎖被掃描的索引範圍,並使用間隙鎖與臨鍵鎖,避免索引範圍區間插入記錄;
3、update與delete
(1)和加鎖select相似,若是在惟一索引上使用惟一的查詢條件來update/delete,例如:
update t set name=xxx where id=1;
也只加記錄鎖;
(2)不然,符合查詢條件的索引記錄以前,都會加排他臨鍵鎖(exclusive next-key lock),來封鎖索引記錄與以前的區間;
(3)尤爲須要特殊說明的是,若是update的是彙集索引(clustered index)記錄,則對應的普通索引(secondary index)記錄也會被隱式加鎖,這是由InnoDB索引的實現機制決定的:普通索引存儲PK的值,檢索普通索引本質上要二次掃描彙集索引。
【索引底層實現】輔助閱讀:
【彙集索引與普通索引的實現差別】輔助閱讀:
4、insert
一樣是寫操做,insert和update與delete不一樣,它會用排它鎖封鎖被插入的索引記錄,而不會封鎖記錄以前的範圍。
同時,會在插入區間加插入意向鎖(insert intention lock),但這個並不會真正封鎖區間,也不會阻止相同區間的不一樣KEY插入。
【插入意向鎖】輔助閱讀:
瞭解不一樣SQL語句的加鎖,對於分析多個事務之間的併發與互斥,以及事務死鎖,很是有幫助。