標籤: 「咱們都是小青蛙」公衆號文章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
),示意圖以下:bash
如上圖,Session A
和Session B
各開啓了一個事務,Session B
中的事務先將id
爲1
的記錄的列c
更新爲'關羽'
,而後Session A
中的事務再去查詢這條id
爲1
的記錄,那麼在未提交讀
的隔離級別下,查詢結果就是'關羽'
,也就是說某個事務讀到了另外一個未提交事務修改過的記錄。可是若是Session B
中的事務稍後進行了回滾,那麼Session A
中的事務至關於讀到了一個不存在的數據,這種現象就稱之爲髒讀
,就像這個樣子:服務器
髒讀
違背了現實世界的業務含義,因此這種READ UNCOMMITTED
算是十分不安全的一種隔離級別
。架構
若是一個事務只能讀到另外一個已經提交的事務修改過的數據,而且其餘事務每對該數據進行一次修改並提交後,該事務都能查詢獲得最新值,那麼這種隔離級別
就稱之爲已提交讀
(英文名:READ COMMITTED
),如圖所示:併發
從圖中能夠看到,第4步時,因爲Session B
中的事務還沒有提交,因此Session A
中的事務查詢獲得的結果只是'劉備'
,而第6步時,因爲Session B
中的事務已經提交,因此Session B
中的事務查詢獲得的結果就是'關羽'
了。性能
對於某個處在在已提交讀
隔離級別下的事務來講,只要其餘事務修改了某個數據的值,而且以後提交了,那麼該事務就會讀到該數據的最新值,比方說:
咱們在Session B
中提交了幾個隱式事務,這些事務都修改了id
爲1
的記錄的列c的值,每次事務提交以後,Session A
中的事務均可以查看到最新的值。這種現象也被稱之爲不可重複讀
。
在一些業務場景中,一個事務只能讀到另外一個已經提交的事務修改過的數據,可是第一次讀過某條記錄後,即便其餘事務修改了該記錄的值而且提交,該事務以後再讀該條記錄時,讀到的還是第一次讀到的值,而不是每次都讀到不一樣的數據。那麼這種隔離級別
就稱之爲可重複讀
(英文名:REPEATABLE READ
),如圖所示:
從圖中能夠看出來,Session A
中的事務在第一次讀取id
爲1
的記錄時,列c
的值爲'劉備'
,以後雖然Session B
中隱式提交了多個事務,每一個事務都修改了這條記錄,可是Session A
中的事務讀到的列c
的值仍爲'劉備'
,與第一次讀取的值是相同的。
以上3種隔離級別都容許對同一條記錄進行讀-讀
、讀-寫
、寫-讀
的併發操做,若是咱們不容許讀-寫
、寫-讀
的併發操做,可使用SERIALIZABLE
隔離級別,示意圖以下:
如圖所示,當Session B
中的事務更新了id
爲1
的記錄後,以後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
,那麼此刻該條記錄的示意圖以下所示:
假設以後兩個id
分別爲100
、200
的事務對這條記錄進行UPDATE
操做,操做流程以下:
小貼士: 能不能在兩個事務中交叉更新同一條記錄呢?哈哈,這是不能夠滴,第一個事務更新了某條記錄後,就會給這條記錄加鎖,另外一個事務再次更新時就須要等待第一個事務提交了,把鎖釋放以後才能夠繼續更新。本篇文章不是討論鎖的,有關鎖的更多細節咱們以後再說。
每次對記錄進行改動,都會記錄一條undo日誌
,每條undo日誌
也都有一個roll_pointer
屬性(INSERT
操做對應的undo日誌
沒有該屬性,由於該記錄並無更早的版本),能夠將這些undo日誌
都連起來,串成一個鏈表,因此如今的狀況就像下圖同樣:
對該記錄每次更新後,都會將舊值放到一條undo日誌
中,就算是該記錄的一箇舊版本,隨着更新次數的增多,全部的版本都會被roll_pointer
屬性鏈接成一個鏈表,咱們把這個鏈表稱之爲版本鏈
,版本鏈的頭節點就是當前記錄最新的值。另外,每一個版本中還包含生成該版本時對應的事務id,這個信息很重要,咱們稍後就會用到。
對於使用READ UNCOMMITTED
隔離級別的事務來講,直接讀取記錄的最新版本就行了,對於使用SERIALIZABLE
隔離級別的事務來講,使用加鎖的方式來訪問記錄。對於使用READ COMMITTED
和REPEATABLE 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 COMMITTED
和REPEATABLE READ
隔離級別的的一個很是大的區別就是它們生成ReadView
的時機不一樣,咱們來看一下。
比方說如今系統裏有兩個id
分別爲100
、200
的事務在執行:
# 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是遞增的。
此刻,表t
中id
爲1
的記錄獲得的版本鏈表以下所示:
假設如今有一個使用READ COMMITTED
隔離級別的事務開始執行:
# 使用READ COMMITTED隔離級別的事務
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'劉備'
複製代碼
這個SELECT1
的執行過程以下:
在執行SELECT
語句時會先生成一個ReadView
,ReadView
的m_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
的事務中更新一下表t
中id
爲1的記錄:
# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
...
UPDATE t SET c = '趙雲' WHERE id = 1;
UPDATE t SET c = '諸葛亮' WHERE id = 1;
複製代碼
此刻,表t
中id
爲1
的記錄的版本鏈就長這樣:
而後再到剛纔使用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
語句時會先生成一個ReadView
,ReadView
的m_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
隔離級別的事務中查詢表t
中id
值爲1
的記錄時,獲得的結果就是'諸葛亮'
了,具體流程咱們就不分析了。總結一下就是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的ReadView。
REPEATABLE READ
---在第一次讀取數據時生成一個ReadView對於使用REPEATABLE READ
隔離級別的事務來講,只會在第一次執行查詢語句時生成一個ReadView
,以後的查詢就不會重複生成了。咱們仍是用例子看一下是什麼效果。
比方說如今系統裏有兩個id
分別爲100
、200
的事務在執行:
# Transaction 100
BEGIN;
UPDATE t SET c = '關羽' WHERE id = 1;
UPDATE t SET c = '張飛' WHERE id = 1;
複製代碼
# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
...
複製代碼
此刻,表t
中id
爲1
的記錄獲得的版本鏈表以下所示:
假設如今有一個使用REPEATABLE READ
隔離級別的事務開始執行:
# 使用REPEATABLE READ隔離級別的事務
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM t WHERE id = 1; # 獲得的列c的值爲'劉備'
複製代碼
這個SELECT1
的執行過程以下:
在執行SELECT
語句時會先生成一個ReadView
,ReadView
的m_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
的事務中更新一下表t
中id
爲1的記錄:
# Transaction 200
BEGIN;
# 更新了一些別的表的記錄
...
UPDATE t SET c = '趙雲' WHERE id = 1;
UPDATE t SET c = '諸葛亮' WHERE id = 1;
複製代碼
此刻,表t
中id
爲1
的記錄的版本鏈就長這樣:
而後再到剛纔使用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
值爲80
,80
小於m_ids
列表中最小的事務id100
,因此這個版本是符合要求的,最後返回給用戶的版本就是這條列c
爲'劉備'
的記錄。
也就是說兩次SELECT
查詢獲得的結果是重複的,記錄的列c
值都是'劉備'
,這就是可重複讀
的含義。若是咱們以後再把事務id爲200
的記錄提交了,以後再到剛纔使用REPEATABLE READ
隔離級別的事務中繼續查找這個id爲1
的記錄,獲得的結果仍是'劉備'
,具體執行過程你們能夠本身分析一下。
從上邊的描述中咱們能夠看出來,所謂的MVCC(Multi-Version Concurrency Control ,多版本併發控制)指的就是在使用READ COMMITTD
、REPEATABLE READ
這兩種隔離級別的事務在執行普通的SEELCT
操做時訪問記錄的版本鏈的過程,這樣子可使不一樣事務的讀-寫
、寫-讀
操做併發執行,從而提高系統性能。READ COMMITTD
、REPEATABLE READ
這兩個隔離級別的一個很大不一樣就是生成ReadView
的時機不一樣,READ COMMITTD
在每一次進行普通SELECT
操做前都會生成一個ReadView
,而REPEATABLE READ
只在第一次進行普通SELECT
操做前生成一個ReadView
,以後的查詢操做都重複這個ReadView
就行了。
想看更多MySQL進階知識能夠到小冊中查看:《MySQL是怎樣運行的:從根兒上理解MySQL》的連接 。小冊的內容主要是從小白的角度出發,用比較通俗的語言講解關於MySQL進階的一些核心概念,好比記錄、索引、頁面、表空間、查詢優化、事務和鎖等,總共的字數大約是三四十萬字,配有上百幅原創插圖。主要是想下降普通程序員學習MySQL進階的難度,讓學習曲線更平滑一點~