今年開始,本身開始修煉儲存與消息相關的技術"內功",想着在當下的開發工做與將來的路途發展中,這兩大塊是不管如何都沒法避開的,因此就開始增強:Mysql、redis與Mq。Mysql的文章看至一半,前幾天在咱們幾個的技術羣裏,和另一個小夥伴,就兩個核心的日誌文件,展開了爭論:到底和事務相關的是redolog仍是undolog呢?原本本身很瞭解來着,沒想到,最終居然搞混了!實在驚歎於Mysql的設計。今天我就用Mysql如何使用undolog這個日誌,來講一說很是難懂且核心的Mysql技術點:MVCC(多版本併發控制)。注意:整片文章使用Mysql默認的存儲引擎InnoDB來說解,具體不作與MyIASM的對比。mysql
要理解MVCC如何工做,必需要掌握一些Mysql的基礎概念,其中包括:事務隔離級別、鎖(共享鎖,排它鎖)c++
這一點,多是各位開發人員爛熟於心的知識點:讀未提交(ru)、讀已提交(rc)、可重複讀(rr)、序列化(serializable)。具體的表現形式,下面來講說:redis
四種隔離級別是依次變嚴格的,固然性能也是依次降低的。所引起的問題相似於:髒讀、不可重複讀、幻讀等等都是一些具體的場景,我這裏用一個統一的兩事務統一場景,來講明一下,具體到底什麼事髒讀,什麼是不可重複讀,我不一一舉例(幻讀要到後面講間隙鎖的時候,才能涉及),下面是例子:sql
整個四大隔離級別,用着一個例子就能完美的說清了~另外Mysql默認是處於第三隔離級別的(可重複讀)數據庫
涉及到的兩種類型的鎖主要以下:數組
這些鎖,前面兩個是針對行記錄,後面兩個針對整表的。具體各類鎖的兼容狀況以下:session
X | IX | S | IS | |
---|---|---|---|---|
X | 衝突 | 衝突 | 衝突 | 衝突 |
IX | 衝突 | 兼容 | 衝突 | 兼容 |
S | 衝突 | 衝突 | 兼容 | 兼容 |
IX | 衝突 | 兼容 | 兼容 | 兼容 |
若是一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之,若是二者不兼容,該事務就要等待鎖釋放。多線程
意向鎖是InnoDB自動加的,不需用戶干預。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖;事務能夠經過如下語句顯示給記錄集加共享鎖或排他鎖。併發
用SELECT ... IN SHARE MODE得到共享鎖,主要用在須要數據依存關係時來確認某行記錄是否存在,並確保沒有人對這個記錄進行UPDATE或者DELETE操做。可是若是當前事務也須要對該記錄進行更新操做,則頗有可能形成死鎖,對於鎖定行記錄後須要進行更新操做的應用,應該使用SELECT... FOR UPDATE方式得到排他鎖。mvc
下面是針對兩種行級別的鎖(共享鎖與排它鎖),作的一些實驗:
session1 | session2 |
---|---|
mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; 這種狀況下的查詢是沒有問題的!由於不加鎖 |
mysql> set autocommit = 0 Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; 一樣沒問題 |
當前session對actor_id=178的記錄加share mode 的共享鎖: mysql> select actor_id,first_name,last_name from actor where actor_id = 178 lock in share mode; 注意加了共享鎖 |
|
其餘session仍然能夠查詢記錄,並也能夠對該記錄加share mode的共享鎖: mysql> select actor_id,first_name,last_name from actor where actor_id = 178 lock in share mode; 這種也是沒有問題的:對同一條已經有共享鎖的數據添加共享鎖 |
|
當前session對鎖定的記錄進行更新操做,等待鎖: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; 等待,由於此條數據上有了共享鎖,加不上叉鎖,就是所謂的排它鎖! |
|
其餘session也對該記錄進行更新操做,則會致使死鎖退出: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction 注意這裏是死鎖了:由於這條記錄已經加了共享鎖,然而session1阻塞了,若是這裏再進行阻塞,那系統裏沒有事務去釋放共享鎖,因此就出現了死鎖,這裏mysql使用了本身的死鎖檢測機制 |
|
得到鎖後,能夠成功更新: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; Query OK, 1 row affected (17.67 sec) Rows matched: 1 Changed: 1 Warnings: 0 這裏成功的緣由是:session2經過死鎖偵測機制強行結束事務了(回滾),那對178的這條記錄的共享鎖也一併釋放,這個時候,update語句就能夠添加排它鎖了,並執行成功 |
以上就是兩個行級鎖的實踐,具體的,InnoDB行鎖是經過給索引上的索引項加鎖來實現的,這一點MySQL與Oracle不一樣,後者是經過在數據塊中對相應數據行加鎖來實現的。InnoDB這種行鎖實現特色意味着:只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然,InnoDB將使用表鎖!
Multi-Version Concurrency Control 多版本併發控制(MVCC),是Mysql中InnoDB這個存儲引擎實現事物隔離級別的主要手段~這裏要強調InnoDB的緣由是,主要實現事務,是經過存儲引擎實現的,而Mysql原本是不具有這個功能的。
在InnoDB這個裏面,主要就是使用MVCC的整個邏輯,來實現事物的第三隔離級別的,就是實現併發控制。具體的作法,我使用我本身的語言,來儘可能簡要的寫寫,都是基於原理的一些講說,涉及在深刻的,例如如何進行命令的查看,如何看mvcc的c++實現源碼,暫時能力還不到那個級別。下面我分小節,一步步來講說這個原理
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, k) values(1,1),(2,2);
事務A | 事務B | 事務C |
---|---|---|
start transaction with consistent snapshot; | ||
start transaction with consistent snapshot; | ||
update t set k = k+1 where id = 1; | ||
Update t set k = k+1where id = 1; select k from t where id = 1; |
||
select k from t where id = 1; commit; |
||
Commit; |
快照,再innoDB裏面叫:consistent read view(一致性視圖),是用來實現重複讀與讀已提交的主要手段。其原理就是每次事務啓動的時候,都會對整個庫,建立一個快照,其實我本身的理解,就是對整個數據庫建立一個內存空間(開始並不是主動所有加載數據頁到內存),接下來每次的修改,都是直接針對內存裏面數值的修改(固然第一次sql執行,要進行數據頁的磁盤加載操做),這樣就能很是高效且多線程的進行修改了!
每次修改一條數據的時候,內存中會加載這條數據的頁,而後建立一個視圖,這個視圖內部保存了當前已經修改爲功的值,和數據庫爲這個事務自動申請的事務Id,記爲row tx_id,而後記錄一條可以回滾到上個修改記錄的日誌:undolog。下面就是一個具體的示意圖:
這裏v一、v二、v三、v4就是咱們所說的快照!而這裏u一、u二、u3就是咱們所說的undolog。真實的,內存中只保留最新的修改數據,就是上圖的v4,若是事物10想讀取k的值,而且v二、v三、v4這三個視圖對應的事物,都沒有提交的話,是要順序執行u一、u二、u3這三個undolog的,這樣就能實現第三事物級別的可重複讀。這裏咱們注意,我加了前綴定語:v2,v3,v4都沒有提交!後面會立刻說到緣由!
每次進行update記錄以前,都要進行一致讀,所謂的一致讀,其實就是讀所讀記錄加上一章所說的排它鎖,這個排它鎖,使得這個記錄每次讀取的都是最新的數據。那若是這樣,就說明一個問題,若是其餘事務首先進行對這個字段的update,就會首先加排它鎖,其餘的事務再次去update的時候,就必須等待。等他這個排它鎖釋放。那何時釋放呢?顯然,必需要等其餘事務結束的時候,下面用具體的實例說明,咱們仍是使用第一個小節給出的數據表進行說明:
事務A | 事務B |
---|---|
start transaction with consistent snapshot; | start transaction with consistent snapshot; |
select k from t where id = 1 | |
update t set k = k +1 where id = 1 | |
update t set k = k +1 where id = 1; 這時會阻塞,一直到事務B結束 |
|
commit; | |
Commit; |
這裏就會涉及到一些mvcc的核心理念,聽說內部c++實現是很是生澀的,這裏我針對我讀到的邏輯進行了簡化,進行講解。
咱們的問題是:針對一條數據,同時存在多個版本,那咱們在一個事物裏面,每次select(不加鎖)讀取到的值究竟是什麼版本的呢?而一個事務中,又是如何感知其餘事務更新的呢?
其實,InnoDB爲每一個事務建立一個數組,在每次事務啓動的時候,保存當前mysql系統中針對這一條數據,活躍的(就是還沒提交)事務id。每次更新這條數據,都會拿最新的內存快照(這一條數據),對快照中的row tx_id進行判斷,具體的判斷邏輯以下:
整個視圖+當前事務+undolog的使用原理,大體如此
咱們來看看第2小節中,select語句查詢的k值,分別是多少,按照5中的分析,一點點的往上捋。咱們先作以下的假設:
如此的話,那麼事務A啓動時候,活躍視圖數組值是:[99,100];事務B啓動時候的數組是:[99,100,101];事務C啓動時候,活躍數組值是:[99,100,101,102]。下面是整個更新視圖建立過程圖:
咱們使用第5小節中的分析,咱們來看一下,事務A中的get k這個值,究竟是怎麼獲取的:
整個過程如此,其實寫下來發現,並無想象的那麼複雜。是不?其實InnoDB中,徹底就是使用這一套的邏輯,"通殺"的!包括讀提交這個隔離級別,在這個隔離級別下,無非就是建立視圖的時機再每次update的時候罷了,其餘查找判斷邏輯和咱們這裏討論的如出一轍!