mysql中的事務隔離級別及可重複讀讀提交詳細分析(mvcc多版本控制/undo log)

<font color=red>一.事物隔離級別</font>

  • 讀未提交(read uncommitted)是指,一個事務還沒提交時,它作的變動就能被別的事務看到.通俗理解,別人改數據的事務還沒有提交,我在個人事務中也能讀到。
  • 讀提交(read committed)是指,一個事務提交以後,它作的變動纔會被其餘事務看到。通俗理解,別人改數據的事務已經提交,我在個人事務中才能讀到。
  • 可重複讀(repeatable read)是指,一個事務執行過程當中看到的數據,老是跟這個事務在啓動時看到的數據 是一致的。固然在可重複讀隔離級別下,未提交變動對其餘事務也是不可見的。通俗理解,別人改數據的事務已經提交,我在個人事務中也不去讀。
  • 串行化(serializable ),顧名思義是對於同一行記錄,「寫」會加「寫鎖」,「讀」會加「讀鎖」。當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行。通俗理解,個人事務還沒有提交,別人就別想改數據。

圖片示例講解

  • 若隔離級別是「讀未提交」, 則 V1 的值就是 2。這時候事務 B 雖然尚未提交,可是 結果已經被 A 看到了。所以,V二、V3 也都是 2。
  • 若隔離級別是「讀提交」,則 V1 是 1,V2 的值是 2。事務 B 的更新在提交後才能被 A 看到。因此, V3 的值也是 2。
  • 若隔離級別是「可重複讀」,則 V一、V2 是 1,V3 是 2。之因此 V2 仍是 1,遵循的就 是這個要求:事務在執行期間看到的數據先後必須是一致的。
  • 若隔離級別是「串行化」,則在事務 B 執行「將 1 改爲 2」的時候,會被鎖住。直到事 務 A 提交後,事務 B 才能夠繼續執行。因此從 A 的角度看, V一、V2 值是 1,V3 的值 是 2。
在實現上,數據庫裏面會建立一個視圖,訪問的時候以視圖的邏輯結果爲準。在「可重複 讀」隔離級別下,這個視圖是在事務啓動時建立的,整個事務存在期間都用這個視圖。 在「讀提交」隔離級別下,這個視圖是在每一個 SQL 語句開始執行的時候建立的。這裏須要 注意的是,「讀未提交」隔離級別下直接返回記錄上的最新值,沒有視圖概念;而「串行 化」隔離級別下直接用加鎖的方式來避免並行訪問。

<font color=red>二.查看mysql的隔離級別</font>

  • mysql> show variables like 'transaction_isolation';
  • mysql默認級別: repeatable read

<font color=red>三.事務隔離的實現</font>

  • 每條記錄在更新的時候都會同時記錄一條回滾操做(也就是說redo log會記錄,undo log也會同時記錄).mysql

  • 記錄上的最新值,經過回滾操做,均可以獲得前一個狀態的值。sql

  • 回滾日誌刪除問題.數據庫

    在不須要的時候才刪除。也就是說,系統會判斷,當沒有事務再須要用到這些回滾日誌時,回滾日誌會被刪除。
    何時纔不須要了呢?就是當系統裏沒有比這個回滾日誌更早的 read-view 的時候。
  • 回滾日誌存儲位置數組

    在 MySQL 5.5 及之前的版本,回滾日誌是跟數據字典一塊兒放在 ibdata 文件裏的(系統表空間),即便長 事務最終提交,回滾段被清理,文件也不會變小。我見過數據只有 20GB,而回滾段有 200GB 的庫。最終只好爲了清理回滾段,重建整個庫。
  • 回滾流程session

<font color=red>四.儘可能不要使用長事務</font>

  • 長事務意味着系統裏面會存在很老的事務視圖。因爲這些事務隨時可能訪問數據庫裏面的任何數據,因此這個事務提交以前,數據庫裏面它可能用到的回滾記錄都必須保留,這就會致使大量佔用存儲空間。spa

  • 還佔用鎖資源,也可能拖垮整個庫線程

  • 詳解日誌

    好比,在某個時刻(今天上午9:00)開啓了一個事務A(對於可重複讀隔離級別,此時一個視圖read-view A也建立了),這是一個很長的事務……
    
    事務A在今天上午9:20的時候,查詢了一個記錄R1的一個字段f1的值爲1……
    
    今天上午9:25的時候,一個事務B(隨之而來的read-view B)也被開啓了,它更新了R1.f1的值爲2(同時也建立了一個由2到1的回滾日誌),這是一個短事務,事務隨後就被commit了。
    
    今天上午9:30的時候,一個事務C(隨之而來的read-view C)也被開啓了,它更新了R1.f1的值爲3(同時也建立了一個由3到2的回滾日誌),這是一個短事務,事務隨後就被commit了。
    
    ……
    
    到了下午3:00了,長事務A尚未commit,爲了保證事務在執行期間看到的數據在先後必須是一致的,那些老的事務視圖、回滾日誌就必須存在了(read-view B,read-view C),這就佔用了大量的存儲空間。
    
    源於此,咱們應該儘可能不要使用長事務。

在information_schema 庫的 innodb_trx 這個表中查詢長事務

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60code

<font color=red>五.事務的啓動方式</font>

  • 顯式啓動事務語句orm

    begin 或 start transaction。
    配套的提交語句是 commit,
    回滾語句是 rollback。
  • set autocommit=0,會將線程的自動提交關掉.

    意味着若是你只執行一個 select 語句,這個事務就啓動了,並且並不會自動提交。這個事務持續存在直到你主 動執行 commit 或 rollback 語句,或者斷開鏈接。
  • select也是事物

<font color=red>六.可重複讀隔離級詳細分析</font>

可重複讀隔離級別下的更新和讀取

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);

注意的是事務的啓動時機。

在可重複讀RR隔離級別模式下,begin/start transaction 命令並非一個事務的起點,在執行到它們以後的第一個操做 InnoDB 表的語句,事務才真正啓動。若是你想要立刻啓動一個事務,可使用 start transaction with consistent snapshot 這個命令。

  • 第一種啓動方式,begin/start transaction一致性視圖是在第執行第一個快照讀語句時建立的;
  • 第二種啓動方式,start transaction with consistent snapshot一致性視圖是在執行 start transaction with consistent snapshot 時建立的。

上面圖1執行的結果是:

  • sessionB查詢到的k值爲3
  • sessionA查詢到的k值爲1
  • 執行順序是,先執行sessionC更新,在執行sessionB更新和查詢,再執行sessionA的查詢
  • 獲得上面的執行結果的緣由是什麼呢,下面分析.

mysql中兩個視圖概念

  • view。它是一個用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結 果。建立視圖的語法是 create view ... ,而它的查詢方法與表同樣。
  • InnoDB 在實現 MVCC 時用到的一致性讀視圖,即 consistent read view, 用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔 離級別的實現。它沒有物理結構,做用是事務執行期間用來定義「我能看到什麼數據」。

快照在MVCC裏是怎麼工做的?

  • 在可重複讀隔離級別下,事務在啓動的時候就「拍了個快照」。注意,這個快照是基於整庫的。

  • InnoDB 裏面每一個事務有一個惟一的事務 ID,叫做 transaction id。它是在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。

  • 每行數據也都是有多個版本的,涉及到transaction id

    • 每次事務更新數據的時候,都會生成一個新的數據版本,而且把 transaction id 賦值給這個數據版本的事務 ID,記爲 row trx_id

    • 同時,舊的數據版本要保留,而且在新的數據版本中,可以有信息能夠直接拿到它。

    • 也就是說,數據表中的一行記錄,其實可能有多個版本 (row),每一個版本有本身的 row trx_id。

    • 圖中虛線框裏是同一行數據的 4 個版本,當前最新版本是 V4,k 的值是 22,它是被transaction id 爲 25 的事務更新的,所以它的 row trx_id 也是 25。

    • 前面的文章不是說,語句更新會生成 undo log(回滾日誌)嗎?那麼,<font color=red>undo log 在哪呢?</font>

    • 實際上,<font color=red>圖 2 中的三個虛線箭頭,就是 undo log</font>;而 V一、V二、V3 並非物理上真實存 在的,而是每次須要的時候根據當前版本和 undo log 計算出來的。好比,須要 V2 的時 候,就是經過 V4 依次執行 U三、U2 算出來。

  • 按照可重複讀的定義,一個事務啓動的時候,可以看到全部已經提交的事務結果。可是以後,這個事務執行期間,其餘事務的更新對它不可見。

  • 一個事務只須要在啓動的時候聲明說,「以我啓動的時刻爲準,若是一個數據版本是在我啓動以前生成的,就認;若是是我啓動之後才生成的,我就不認,我必需要找到它的上一個版本」。 固然,若是「上一個版本」也不可見,那就得繼續往前找。還有,若是是這個事務本身更 新的數據,它本身仍是要認的。

  • 在實現上, InnoDB 爲每一個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正 在「活躍」的全部事務 ID。「活躍」指的就是,啓動了但還沒提交。數組裏面事務 ID 的最小值記爲低水位,當前系統裏面已經建立過的事務 ID 的最大值加 1 記爲高水位。這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。

  • 當開啓事務時,須要保存活躍事務的數組(A),而後獲取高水位(B)二者中間會不會產生新的事務?

    • A和B之間在事務系統的鎖保護下作的,能夠認爲是原子操做,期間不能建立事務。
    • 高水位不在視圖數組裏面,高水位應該就是屬於將來未開始事務了
    • 事務A啓動時,當前活躍事務數組包不包括本身的trx_id,由於若是是本身更新的,老是可見的
  • <font color=red>數據版本的可見性規則,就是基於數據的 row trx_id 和這個一致性視圖的對比結果獲得 的。這個視圖數組把全部的 row trx_id 分紅了幾種不一樣的狀況。</font>

    • 對於當前事務的啓動瞬間來講,假設當前trx id爲98 , 在當前事務開始後,計算活躍事務以前又產生了個新事務trx id爲99沒有commit,假設活躍事務的id組成的數據爲下面的數組[80,88,99],此時事務80/88/99爲活躍事務,99爲當前系統中事務最大ID, 高水位100是當前系統最大事務id99加1計算出來的,則會有如下幾種可能:

      1. 若是落在綠色部分,表示這個版本是已提交的事務或者是當前事務本身生成的,這個數據是可見的; 即80之前的事務均可見

      2. 若是落在紅色部分,表示這個版本是由未來啓動的事務生成的,是確定不可見的; 100及100之後的事務都不可見

      3. 若是落在黃色部分,那就包括兩種狀況

        a. 若 row trx_id 在數組中,表示這個版本是由還沒提交的事務生成的,不可見; 80/88/99爲活躍事務,不可見

        b. 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。80~99中間,去除80/88/99,好比81等其他的是可見的.

  • InnoDB 利用了「全部數據都有多個版本」的這個特性,利用數據可見性規則實現了「秒級建立快照」的能力。

  • 爲何會出現sessionB查詢到的k值爲3,sessionA查詢到的k值爲1呢,根據上面的數據可見性分析以下:

    • 這裏,咱們不妨作以下假設:

      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]。

      從圖中能夠看到,第一個有效更新是事務 C,把數據從 (1,1) 改爲了 (1,2)。這時候,這個數據的最新版本的 row trx_id 是 102,而 90 這個版本已經成爲了歷史版本。
      
      第二個有效更新是事務 B,把數據從 (1,2) 改爲了 (1,3)。這時候,這個數據的最新版本 (即 row trx_id)是 101,而 102 又成爲了歷史版本。{備註:按理說事務B是[99,100,101],此時找到(1,2)的時候判斷出row trx_id=102,比它本身的高水位大,處於紅色區域,不可見,應該往前找,找(1,1)版本,可是此時它倒是找的(1,2)row trx_id=102的版本,這是什麼緣由的,是由於更新都是「當前讀」(current read),當前讀這個概念下面解釋}
      
      你可能注意到了,在事務 A 查詢的時候,其實事務 B 尚未提交,可是它生成的 (1,3) 這 個版本已經變成當前版本了。但這個版本對事務 A 必須是不可見的,不然就變成髒讀了。
      
      好,如今事務 A 要來讀數據了,它的視圖數組是 [99,100]。固然了,讀數據都是從當前版本讀起的。因此,事務 A 查詢語句的讀數據流程是這樣的:
      		找到 (1,3) 的時候,判斷出 row trx_id=101,比高水位大,處於紅色區域,不可見;
      		接着,找到上一個歷史版本,一看 row trx_id=102,比高水位大,處於紅色區域,不可見;
      		再往前找,終於找到了(1,1),它的 row trx_id=90,比低水位小,處於綠色區域,可見。
      
      這樣執行下來,雖然期間這一行數據被修改過,可是事務 A 不論在何時查詢,看到這 行數據的結果都是一致的,因此咱們稱之爲一致性讀。
  • 上面的分析判斷規則是從代碼邏輯直接轉譯過來的,一個數據版本,對於一個事務視圖來講,除了本身的更新老是可見之外,有三種狀況:

    1. 版本未提交,不可見;
    2. 版本已提交,可是是在視圖建立後提交的,不可見;
    3. 版本已提交,並且是在視圖建立前提交的,可見。

更新邏輯

<font color=red>事務 B 的 update 語句,若是按照一致性讀,好像結果不對 哦?事務 B 的視圖數組是先生成的,以後事務 C 才提交,不是應該看不見 (1,2) 嗎,怎麼能算出 (1,3) 來?</font>

  • 是的,若是事務 B 在更新以前查詢一次數據,這個查詢返回的 k 的值確實是 1。
  • 可是,當它要去更新數據的時候,就不能再在歷史版本上更新了,不然事務 C 的更新就丟 失了。所以,事務 B 此時的 set k=k+1 是在(1,2)的基礎上進行的操做。
  • 這裏就用到了這樣一條規則: <font color=red>更新數據都是先讀後寫的,而這個讀,只能讀當前的 值,稱爲「當前讀」(current read)。</font>
  • 所以,在更新的時候,當前讀拿到的數據是 (1,2),更新後生成了新版本的數據 (1,3),這 個新版本的 row trx_id 是 101。
  • 因此,在執行事務 B 查詢語句的時候,一看本身的版本號是 101,最新數據的版本號也是 101,是本身的更新,能夠直接使用,因此查詢獲得的 k 的值是 3。
  • 除了 update 語句外,select 語句若是加 鎖,也是當前讀。
    • mysql> select k from t where id=1 lock in share mode; 讀鎖(S 鎖,共享鎖)
    • mysql> select k from t where id=1 for update; 寫鎖(X 鎖,排他鎖)

<font color=red>假設事務 C 不是立刻提交的,而是變成了下面的事務 C’</font>

事務 C’的不一樣是,更新後並無立刻提交,在它提交前,事務 B 的更新語句先發起了。前面說過了,雖然事務 C’還沒提交,可是 (1,2) 這個版本也已經生成了,而且是當前的 最新版本。那麼,事務 B 的更新語句會怎麼處理呢?

  • 上一篇文章中提到的「兩階段鎖協議」就要上場了
  • 事務 C’ 沒提交,也 就是說 (1,2) 這個版本上的寫鎖還沒釋放。而事務 B 是<font color=red>當前讀</font>,必需要讀最新版本,並且 必須加鎖,所以就被鎖住了,必須等到事務 C’釋放這個鎖,才能繼續它的當前讀。
  • 這樣一致性讀、當前讀和行鎖就串起來了.<font color=red>在一致性讀的環境下,事務C' 執行更新,此時C'沒有commit,事務B就開啓了,由於事務B要進行當前讀,獲取最新的信息,讀的時候要加鎖(讀完立馬更新),可是此時事務C'尚未commit,鎖(行鎖)還沒釋放,因此事務B須要等待事務C'釋放鎖以後才能獲取鎖,而後才能執行當前讀,讀到事務B更新了的(1,2),既而更新爲(1,3),同時由於(1,3)是事務B自身更新的,因此事務B在查詢id=1的值時,天然而然的就查到了k爲3. 可是對於事務A來講,查詢的時候,由於事務C'和事務B的更新都是在事務A開始以後,因此對於事務A都不可見,因此事務A讀取到的值爲1. 上面的分析一樣適用於事務A/B/C</font>

可重複讀隔離級別RR核心

  • 核心就是一致性讀(consistent read),正式由於一致性讀的緣由,因此本事務開始以後,就算其餘事務更新了相關的值,此時本事務仍是能查到本事務開始以前的值,而不是其餘事務更新後的值.
  • 讀提交的邏輯和可重複讀的邏輯相似,它們最主要的區別是
    • 在可重複讀隔離級別下,只須要在事務開始的時候建立一致性視圖,以後事務裏的其餘查詢都共用這個一致性視圖;
    • 在讀提交隔離級別下,每個語句執行前都會從新算出一個新的視圖。

讀提交RC隔離級別

<font color=red>start transaction with consistent snapshot; 在都提交下與start transaction等效.</font>

  • 事務 A 的查詢語句的視圖數組是在執行這個語句的時候建立的,時序上 (1,2)、(1,3) 的生成時間都在建立這個視圖數組的時刻以前。
  • 可是(1,3) 還沒提交,屬於狀況 1,不可見; (1,2) 提交了,屬於狀況 3,可見。
  • 因此,這時候事務 A 查詢語句返回的是 k=2。
  • 顯然地,事務 B 查詢結果 k=3。

站在巨人的肩膀上摘蘋果:

https://time.geekbang.org/column/intro/100020801

相關文章
相關標籤/搜索