Mysql心路歷程:兩個"log"引起的"血案"

今年開始,本身開始修煉儲存與消息相關的技術"內功",想着在當下的開發工做與將來的路途發展中,這兩大塊是不管如何都沒法避開的,因此就開始增強:Mysql、redis與Mq。Mysql的文章看至一半,前幾天在咱們幾個的技術羣裏,和另一個小夥伴,就兩個核心的日誌文件,展開了爭論:到底和事務相關的是redolog仍是undolog呢?原本本身很瞭解來着,沒想到,最終居然搞混了!實在驚歎於Mysql的設計。今天我就用Mysql如何使用undolog這個日誌,來講一說很是難懂且核心的Mysql技術點:MVCC(多版本併發控制)。注意:整片文章使用Mysql默認的存儲引擎InnoDB來說解,具體不作與MyIASM的對比。mysql

1、一些基礎概念的鋪墊

要理解MVCC如何工做,必需要掌握一些Mysql的基礎概念,其中包括:事務隔離級別、鎖(共享鎖,排它鎖)c++

一、事務隔離級別

這一點,多是各位開發人員爛熟於心的知識點:讀未提交(ru)、讀已提交(rc)、可重複讀(rr)、序列化(serializable)。具體的表現形式,下面來講說:redis

  • 讀未提交:一個事務還沒提交時,它作的變動就能被別的事務看。就是說本事務開啓以後的任何修改,能馬上被其餘事務讀取到
  • 讀已提交:一個事務對一個值的修改,必須等到此事務提交以後,這個被修改的值才能被別的事務讀取到
  • 可重複讀:一個事務開啓之時,就保證一條數據的一致性,直到此事務結束。即便事務過程當中,有其餘事務對此條數據進行了修改,本事務也是不可見的。
  • 序列化:這一點很好理解,每次事務開啓,都對數據進行加鎖,其餘事務要等此事務結束,才能進行操做

四種隔離級別是依次變嚴格的,固然性能也是依次降低的。所引起的問題相似於:髒讀、不可重複讀、幻讀等等都是一些具體的場景,我這裏用一個統一的兩事務統一場景,來講明一下,具體到底什麼事髒讀,什麼是不可重複讀,我不一一舉例(幻讀要到後面講間隙鎖的時候,才能涉及),下面是例子:sql

  • 若隔離級別是「讀未提交」, 則 V1 的值就是 2。這時候事務B還沒提交,可是在這個隔離級別下面,對於事務A來講已是可看到的了,因此V二、V3都是2了
  • 若隔離級別是"讀已提交",因爲事務B未提交,因此對於字段修改,其餘事務是不可見的,因此事務A中V1的值是1,而當事務A到了V2查詢之時,事務B已經提交了,因此V2,V3的值都是2
  • 隔離級別是"可重複讀",根據這個隔離級別的描述,因爲事務A從開啓到提交,都是統一的視圖,因此,事務A中的V一、V2的值都是1,雖然過程當中事務B對值進行了修改,並且也提交了,可是對於事務A中,仍是不可見的,固然到了V3的時候,事務A提交,固然就能夠看到修改的值,因此是2
  • 若隔離級別是"序列化",那就簡單了,事務A一開始就所記錄進行了加鎖,而後事務B被阻塞,事務A裏面的V一、V2都是1,事務提交以後,啓動事務B,又對記錄加了鎖,而後事務執行update,提交事務B以後,才能查到值V3,結果是2

整個四大隔離級別,用着一個例子就能完美的說清了~另外Mysql默認是處於第三隔離級別的(可重複讀)數據庫

二、鎖(共享鎖,排它鎖)

涉及到的兩種類型的鎖主要以下:數組

  • 共享鎖(S):容許一個事務去讀一行,阻止其餘事務得到相同數據集的排他鎖。
  • 排他鎖(X):容許得到排他鎖的事務更新數據,阻止其餘事務取得相同數據集的共享讀鎖和排他寫鎖。另外,爲了容許行鎖和表鎖共存,實現多粒度鎖機制,InnoDB還有兩種內部使用的意向鎖(Intention Locks),這兩種意向鎖都是表鎖。
  • 意向共享鎖(IS):事務打算給數據行加行共享鎖,事務在給一個數據行加共享鎖前必須先取得該表的IS鎖。
  • 意向排他鎖(IX):事務打算給數據行加行排他鎖,事務在給一個數據行加排他鎖前必須先取得該表的IX鎖。

這些鎖,前面兩個是針對行記錄,後面兩個針對整表的。具體各類鎖的兼容狀況以下:session

  X IX S IS
X 衝突 衝突 衝突 衝突
IX 衝突 兼容 衝突 兼容
S 衝突 衝突 兼容 兼容
IX 衝突 兼容 兼容 兼容

若是一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之,若是二者不兼容,該事務就要等待鎖釋放。多線程

意向鎖是InnoDB自動加的,不需用戶干預。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,InnoDB不會加任何鎖;事務能夠經過如下語句顯示給記錄集加共享鎖或排他鎖。併發

  • 共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
  • 排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE。

用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將使用表鎖!

2、咱們來看MVCC

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;  

三、引出快照與undolog理念

快照,再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,就會首先加排它鎖,其餘的事務再次去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;  

五、update以後如何讀取數值

這裏就會涉及到一些mvcc的核心理念,聽說內部c++實現是很是生澀的,這裏我針對我讀到的邏輯進行了簡化,進行講解。

咱們的問題是:針對一條數據,同時存在多個版本,那咱們在一個事物裏面,每次select(不加鎖)讀取到的值究竟是什麼版本的呢?而一個事務中,又是如何感知其餘事務更新的呢?

其實,InnoDB爲每一個事務建立一個數組,在每次事務啓動的時候,保存當前mysql系統中針對這一條數據,活躍的(就是還沒提交)事務id。每次更新這條數據,都會拿最新的內存快照(這一條數據),對快照中的row tx_id進行判斷,具體的判斷邏輯以下:

  • 這個id在快照表中,說明是還未提交的事務進行的更新,不能用,使用undolog回滾到上一個視圖,查找上一個版本中的tx_id
  • 這個id不在快照表中,那就要看當前事務與所比對視圖的id值
    • 當前事務的id比視圖中的事務id值大,說明視圖的是在當前事務開始以前建立更新並提交的,那這個值是可使用的,有效的,返回
    • 當前事務的id比視圖中的事務id值小,說明視圖是在當前事務開始以後開始的事務,就是說,當前事務開啓的時候,系統中活躍的事務並無這個視圖,這個視圖對應的事務是在以後纔開始的,因此這個視圖裏面針對這個記錄的更新,對於當前事務是不可見的。一樣,使用undolog回滾到上一個視圖,繼續按照這個套路查找。

整個視圖+當前事務+undolog的使用原理,大體如此

六、看看第2小節的結果

咱們來看看第2小節中,select語句查詢的k值,分別是多少,按照5中的分析,一點點的往上捋。咱們先作以下的假設:

  1. 事務 A 開始前,系統裏面只有一個活躍事務 ID 是 99;
  2. 事務 A、B、C 的版本號分別是 100、10一、102,且當前系統中只有這四個事務
  3. 三個事務開始前,(1,1)這一行數據的 row trx_id是90

如此的話,那麼事務A啓動時候,活躍視圖數組值是:[99,100];事務B啓動時候的數組是:[99,100,101];事務C啓動時候,活躍數組值是:[99,100,101,102]。下面是整個更新視圖建立過程圖:

咱們使用第5小節中的分析,咱們來看一下,事務A中的get k這個值,究竟是怎麼獲取的:

  • 首先超找到系統中這條記錄的最新的視圖記錄,101這個版本,發現,101的這個事務id,不在當前事務的活躍視圖數組中,且比當前事務id要大,不可見,使用undolog向上,到102這個版本
  • 102這個版本的視圖,裏面的事務id是102,一樣的,102也是不在活躍數組中且比當前事務id要大,一樣是不可見的,照樣的使用undolog向上查找上一個版本:90
  • 發現90這個版本的視圖中事務Id值也不在活躍數組當中,可是這個id值比當前事務的id值要小,因此這個值可見,返回90這個版本視圖中的k的值1

整個過程如此,其實寫下來發現,並無想象的那麼複雜。是不?其實InnoDB中,徹底就是使用這一套的邏輯,"通殺"的!包括讀提交這個隔離級別,在這個隔離級別下,無非就是建立視圖的時機再每次update的時候罷了,其餘查找判斷邏輯和咱們這裏討論的如出一轍!

相關文章
相關標籤/搜索