MySQL事務隔離級別和MVCC

標籤: 「咱們都是小青蛙」公衆號文章mysql


事前準備

爲了故事的順利發展,咱們須要建立一個表:程序員

CREATE TABLE t (
    id INT PRIMARY KEY,
    c VARCHAR(100)
) Engine=InnoDB CHARSET=utf8;
複製代碼

而後向這個表裏插入一條數據:sql

INSERT INTO t VALUES(1, '劉備');
複製代碼

如今表裏的數據就是這樣的:數據庫

mysql> SELECT * FROM t;
+----+--------+
| id | c      |
+----+--------+
|  1 | 劉備   |
+----+--------+
1 row in set (0.01 sec)
複製代碼

隔離級別

MySQL是一個服務器/客戶端架構的軟件,對於同一個服務器來講,能夠有若干個客戶端與之鏈接,每一個客戶端與服務器鏈接上以後,就能夠稱之爲一個會話(Session)。咱們能夠同時在不一樣的會話裏輸入各類語句,這些語句能夠做爲事務的一部分進行處理。不一樣的會話能夠同時發送請求,也就是說服務器可能同時在處理多個事務,這樣子就會致使不一樣的事務可能同時訪問到相同的記錄。咱們前邊說過事務有一個特性稱之爲隔離性,理論上在某個事務對某個數據進行訪問時,其餘事務應該進行排隊,當該事務提交以後,其餘事務才能夠繼續訪問這個數據。可是這樣子的話對性能影響太大,因此設計數據庫的大叔提出了各類隔離級別,來最大限度的提高系統併發處理事務的能力,可是這也是以犧牲必定的隔離性來達到的。安全

未提交讀(READ UNCOMMITTED)

若是一個事務讀到了另外一個未提交事務修改過的數據,那麼這種隔離級別就稱之爲未提交讀(英文名:READ UNCOMMITTED),示意圖以下:bash

image_1d6t5hhamcd61qkjk9v1ag8171o7u.png-95.6kB

如上圖,Session ASession B各開啓了一個事務,Session B中的事務先將id1的記錄的列c更新爲'關羽',而後Session A中的事務再去查詢這條id1的記錄,那麼在未提交讀的隔離級別下,查詢結果就是'關羽',也就是說某個事務讀到了另外一個未提交事務修改過的記錄。可是若是Session B中的事務稍後進行了回滾,那麼Session A中的事務至關於讀到了一個不存在的數據,這種現象就稱之爲髒讀,就像這個樣子:服務器

image_1d6uqql7n55t1k7mmellrh14a495.png-105.3kB

髒讀違背了現實世界的業務含義,因此這種READ UNCOMMITTED算是十分不安全的一種隔離級別架構

已提交讀(READ COMMITTED)

若是一個事務只能讀到另外一個已經提交的事務修改過的數據,而且其餘事務每對該數據進行一次修改並提交後,該事務都能查詢獲得最新值,那麼這種隔離級別就稱之爲已提交讀(英文名:READ COMMITTED),如圖所示:併發

image_1d6t64lgg1j4mtp818f61n09t6l8o.png-133.1kB

從圖中能夠看到,第4步時,因爲Session B中的事務還沒有提交,因此Session A中的事務查詢獲得的結果只是'劉備',而第6步時,因爲Session B中的事務已經提交,因此Session B中的事務查詢獲得的結果就是'關羽'了。性能

對於某個處在在已提交讀隔離級別下的事務來講,只要其餘事務修改了某個數據的值,而且以後提交了,那麼該事務就會讀到該數據的最新值,比方說:

image_1d6urs4l0g799959e1jsj1cvqai.png-170.6kB

咱們在Session B中提交了幾個隱式事務,這些事務都修改了id1的記錄的列c的值,每次事務提交以後,Session A中的事務均可以查看到最新的值。這種現象也被稱之爲不可重複讀

可重複讀(REPEATABLE READ)

在一些業務場景中,一個事務只能讀到另外一個已經提交的事務修改過的數據,可是第一次讀過某條記錄後,即便其餘事務修改了該記錄的值而且提交,該事務以後再讀該條記錄時,讀到的還是第一次讀到的值,而不是每次都讀到不一樣的數據。那麼這種隔離級別就稱之爲可重複讀(英文名:REPEATABLE READ),如圖所示:

image_1d6useq9aagi9981sm21b011dt4bf.png-171.1kB

從圖中能夠看出來,Session A中的事務在第一次讀取id1的記錄時,列c的值爲'劉備',以後雖然Session B中隱式提交了多個事務,每一個事務都修改了這條記錄,可是Session A中的事務讀到的列c的值仍爲'劉備',與第一次讀取的值是相同的。

串行化(SERIALIZABLE)

以上3種隔離級別都容許對同一條記錄進行讀-讀讀-寫寫-讀的併發操做,若是咱們不容許讀-寫寫-讀的併發操做,可使用SERIALIZABLE隔離級別,示意圖以下:

image_1d6uu0sk41213olj102t1tsa10o9ds.png-122.9kB

如圖所示,當Session B中的事務更新了id1的記錄後,以後Session A中的事務再去訪問這條記錄時就被卡住了,直到Session B中的事務提交以後,Session A中的事務才能夠獲取到查詢結果。

版本鏈

對於使用InnoDB存儲引擎的表來講,它的聚簇索引記錄中都包含兩個必要的隱藏列(row_id並非必要的,咱們建立的表中有主鍵或者非NULL惟一鍵時都不會包含row_id列):

  • trx_id:每次對某條聚簇索引記錄進行改動時,都會把對應的事務id賦值給trx_id隱藏列。

  • roll_pointer:每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到undo日誌中,而後這個隱藏列就至關於一個指針,能夠經過它來找到該記錄修改前的信息。

比方說咱們的表t如今只包含一條記錄:

mysql> SELECT * FROM t;
+----+--------+
| id | c      |
+----+--------+
|  1 | 劉備   |
+----+--------+
1 row in set (0.01 sec)
複製代碼

假設插入該記錄的事務id爲80,那麼此刻該條記錄的示意圖以下所示:

image_1d6vemvvn1db6h431ekvsp158m19.png-15kB

假設以後兩個id分別爲100200的事務對這條記錄進行UPDATE操做,操做流程以下:

image_1d6vfo4g814h019mj1jqb1ggu72o3j.png-106.5kB

小貼士: 能不能在兩個事務中交叉更新同一條記錄呢?哈哈,這是不能夠滴,第一個事務更新了某條記錄後,就會給這條記錄加鎖,另外一個事務再次更新時就須要等待第一個事務提交了,把鎖釋放以後才能夠繼續更新。本篇文章不是討論鎖的,有關鎖的更多細節咱們以後再說。

每次對記錄進行改動,都會記錄一條undo日誌,每條undo日誌也都有一個roll_pointer屬性(INSERT操做對應的undo日誌沒有該屬性,由於該記錄並無更早的版本),能夠將這些undo日誌都連起來,串成一個鏈表,因此如今的狀況就像下圖同樣:

image_1d6vfrv111j4guetptcts1qgp40.png-57.1kB

對該記錄每次更新後,都會將舊值放到一條undo日誌中,就算是該記錄的一箇舊版本,隨着更新次數的增多,全部的版本都會被roll_pointer屬性鏈接成一個鏈表,咱們把這個鏈表稱之爲版本鏈,版本鏈的頭節點就是當前記錄最新的值。另外,每一個版本中還包含生成該版本時對應的事務id,這個信息很重要,咱們稍後就會用到。

ReadView

對於使用READ UNCOMMITTED隔離級別的事務來講,直接讀取記錄的最新版本就行了,對於使用SERIALIZABLE隔離級別的事務來講,使用加鎖的方式來訪問記錄。對於使用READ COMMITTEDREPEATABLE READ隔離級別的事務來講,就須要用到咱們上邊所說的版本鏈了,核心問題就是:須要判斷一下版本鏈中的哪一個版本是當前事務可見的。因此設計InnoDB的大叔提出了一個ReadView的概念,這個ReadView中主要包含當前系統中還有哪些活躍的讀寫事務,把它們的事務id放到一個列表中,咱們把這個列表命名爲爲m_ids。這樣在訪問某條記錄時,只須要按照下邊的步驟判斷記錄的某個版本是否可見:

  • 若是被訪問版本的trx_id屬性值小於m_ids列表中最小的事務id,代表生成該版本的事務在生成ReadView前已經提交,因此該版本能夠被當前事務訪問。

  • 若是被訪問版本的trx_id屬性值大於m_ids列表中最大的事務id,代表生成該版本的事務在生成ReadView後才生成,因此該版本不能夠被當前事務訪問。

  • 若是被訪問版本的trx_id屬性值在m_ids列表中最大的事務id和最小事務id之間,那就須要判斷一下trx_id屬性值是否是在m_ids列表中,若是在,說明建立ReadView時生成該版本的事務仍是活躍的,該版本不能夠被訪問;若是不在,說明建立ReadView時生成該版本的事務已經被提交,該版本能夠被訪問。

若是某個版本的數據對當前事務不可見的話,那就順着版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最後一個版本,若是最後一個版本也不可見的話,那麼就意味着該條記錄對該事務不可見,查詢結果就不包含該記錄。

MySQL中,READ COMMITTEDREPEATABLE READ隔離級別的的一個很是大的區別就是它們生成ReadView的時機不一樣,咱們來看一下。

READ COMMITTED --- 每次讀取數據前都生成一個ReadView

比方說如今系統裏有兩個id分別爲100200的事務在執行:

# Transaction 100
BEGIN;

UPDATE t SET c = '關羽' WHERE id = 1;

UPDATE t SET c = '張飛' WHERE id = 1;
複製代碼
# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...
複製代碼

小貼士: 事務執行過程當中,只有在第一次真正修改記錄時(好比使用INSERT、DELETE、UPDATE語句),纔會被分配一個單獨的事務id,這個事務id是遞增的。

此刻,表tid1的記錄獲得的版本鏈表以下所示:

image_1d6vgdl0j1c9d16rbelo1deh17324d.png-42.2kB

假設如今有一個使用READ COMMITTED隔離級別的事務開始執行:

# 使用READ COMMITTED隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'劉備'
複製代碼

這個SELECT1的執行過程以下:

  • 在執行SELECT語句時會先生成一個ReadViewReadViewm_ids列表的內容就是[100, 200]

  • 而後從版本鏈中挑選可見的記錄,從圖中能夠看出,最新版本的列c的內容是'張飛',該版本的trx_id值爲100,在m_ids列表內,因此不符合可見性要求,根據roll_pointer跳到下一個版本。

  • 下一個版本的列c的內容是'關羽',該版本的trx_id值也爲100,也在m_ids列表內,因此也不符合要求,繼續跳到下一個版本。

  • 下一個版本的列c的內容是'劉備',該版本的trx_id值爲80,小於m_ids列表中最小的事務id100,因此這個版本是符合要求的,最後返回給用戶的版本就是這條列c'劉備'的記錄。

以後,咱們把事務id爲100的事務提交一下,就像這樣:

# Transaction 100
BEGIN;

UPDATE t SET c = '關羽' WHERE id = 1;

UPDATE t SET c = '張飛' WHERE id = 1;

COMMIT;
複製代碼

而後再到事務id爲200的事務中更新一下表tid爲1的記錄:

# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...

UPDATE t SET c = '趙雲' WHERE id = 1;

UPDATE t SET c = '諸葛亮' WHERE id = 1;
複製代碼

此刻,表tid1的記錄的版本鏈就長這樣:

image_1d6vgrt5jeh2itl5e41ocl944q.png-57.6kB

而後再到剛纔使用READ COMMITTED隔離級別的事務中繼續查找這個id爲1的記錄,以下:

# 使用READ COMMITTED隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'劉備'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'張飛'
複製代碼

這個SELECT2的執行過程以下:

  • 在執行SELECT語句時會先生成一個ReadViewReadViewm_ids列表的內容就是[200](事務id爲100的那個事務已經提交了,因此生成快照時就沒有它了)。

  • 而後從版本鏈中挑選可見的記錄,從圖中能夠看出,最新版本的列c的內容是'諸葛亮',該版本的trx_id值爲200,在m_ids列表內,因此不符合可見性要求,根據roll_pointer跳到下一個版本。

  • 下一個版本的列c的內容是'趙雲',該版本的trx_id值爲200,也在m_ids列表內,因此也不符合要求,繼續跳到下一個版本。

  • 下一個版本的列c的內容是'張飛',該版本的trx_id值爲100,比m_ids列表中最小的事務id200還要小,因此這個版本是符合要求的,最後返回給用戶的版本就是這條列c'張飛'的記錄。

以此類推,若是以後事務id爲200的記錄也提交了,再此在使用READ COMMITTED隔離級別的事務中查詢表tid值爲1的記錄時,獲得的結果就是'諸葛亮'了,具體流程咱們就不分析了。總結一下就是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的ReadView

REPEATABLE READ ---在第一次讀取數據時生成一個ReadView

對於使用REPEATABLE READ隔離級別的事務來講,只會在第一次執行查詢語句時生成一個ReadView,以後的查詢就不會重複生成了。咱們仍是用例子看一下是什麼效果。

比方說如今系統裏有兩個id分別爲100200的事務在執行:

# Transaction 100
BEGIN;

UPDATE t SET c = '關羽' WHERE id = 1;

UPDATE t SET c = '張飛' WHERE id = 1;
複製代碼
# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...
複製代碼

此刻,表tid1的記錄獲得的版本鏈表以下所示:

image_1d6vgdl0j1c9d16rbelo1deh17324d.png-42.2kB

假設如今有一個使用REPEATABLE READ隔離級別的事務開始執行:

# 使用REPEATABLE READ隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'劉備'
複製代碼

這個SELECT1的執行過程以下:

  • 在執行SELECT語句時會先生成一個ReadViewReadViewm_ids列表的內容就是[100, 200]

  • 而後從版本鏈中挑選可見的記錄,從圖中能夠看出,最新版本的列c的內容是'張飛',該版本的trx_id值爲100,在m_ids列表內,因此不符合可見性要求,根據roll_pointer跳到下一個版本。

  • 下一個版本的列c的內容是'關羽',該版本的trx_id值也爲100,也在m_ids列表內,因此也不符合要求,繼續跳到下一個版本。

  • 下一個版本的列c的內容是'劉備',該版本的trx_id值爲80,小於m_ids列表中最小的事務id100,因此這個版本是符合要求的,最後返回給用戶的版本就是這條列c'劉備'的記錄。

以後,咱們把事務id爲100的事務提交一下,就像這樣:

# Transaction 100
BEGIN;

UPDATE t SET c = '關羽' WHERE id = 1;

UPDATE t SET c = '張飛' WHERE id = 1;

COMMIT;
複製代碼

而後再到事務id爲200的事務中更新一下表tid爲1的記錄:

# Transaction 200
BEGIN;

# 更新了一些別的表的記錄
...

UPDATE t SET c = '趙雲' WHERE id = 1;

UPDATE t SET c = '諸葛亮' WHERE id = 1;
複製代碼

此刻,表tid1的記錄的版本鏈就長這樣:

image_1d6vgrt5jeh2itl5e41ocl944q.png-57.6kB

而後再到剛纔使用REPEATABLE READ隔離級別的事務中繼續查找這個id爲1的記錄,以下:

# 使用REPEATABLE READ隔離級別的事務
BEGIN;

# SELECT1:Transaction 100、200均未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'劉備'

# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值仍爲'劉備'
複製代碼

這個SELECT2的執行過程以下:

  • 由於以前已經生成過ReadView了,因此此時直接複用以前的ReadView,以前的ReadView中的m_ids列表就是[100, 200]

  • 而後從版本鏈中挑選可見的記錄,從圖中能夠看出,最新版本的列c的內容是'諸葛亮',該版本的trx_id值爲200,在m_ids列表內,因此不符合可見性要求,根據roll_pointer跳到下一個版本。

  • 下一個版本的列c的內容是'趙雲',該版本的trx_id值爲200,也在m_ids列表內,因此也不符合要求,繼續跳到下一個版本。

  • 下一個版本的列c的內容是'張飛',該版本的trx_id值爲100,而m_ids列表中是包含值爲100的事務id的,因此該版本也不符合要求,同理下一個列c的內容是'關羽'的版本也不符合要求。繼續跳到下一個版本。

  • 下一個版本的列c的內容是'劉備',該版本的trx_id值爲8080小於m_ids列表中最小的事務id100,因此這個版本是符合要求的,最後返回給用戶的版本就是這條列c'劉備'的記錄。

也就是說兩次SELECT查詢獲得的結果是重複的,記錄的列c值都是'劉備',這就是可重複讀的含義。若是咱們以後再把事務id爲200的記錄提交了,以後再到剛纔使用REPEATABLE READ隔離級別的事務中繼續查找這個id爲1的記錄,獲得的結果仍是'劉備',具體執行過程你們能夠本身分析一下。

MVCC總結

從上邊的描述中咱們能夠看出來,所謂的MVCC(Multi-Version Concurrency Control ,多版本併發控制)指的就是在使用READ COMMITTDREPEATABLE READ這兩種隔離級別的事務在執行普通的SEELCT操做時訪問記錄的版本鏈的過程,這樣子可使不一樣事務的讀-寫寫-讀操做併發執行,從而提高系統性能。READ COMMITTDREPEATABLE READ這兩個隔離級別的一個很大不一樣就是生成ReadView的時機不一樣,READ COMMITTD在每一次進行普通SELECT操做前都會生成一個ReadView,而REPEATABLE READ只在第一次進行普通SELECT操做前生成一個ReadView,以後的查詢操做都重複這個ReadView就行了。

小冊

想看更多MySQL進階知識能夠到小冊中查看:《MySQL是怎樣運行的:從根兒上理解MySQL》的連接 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,好比記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是三四十萬字,配有上百幅原創插圖。主要是想下降普通程序員學習MySQL進階的難度,讓學習曲線更平滑一點~

相關文章
相關標籤/搜索