mysql事務原理及MVCC
mysql事務原理及MVCC
事務是資料庫最為重要的機制之一,凡是使用過資料庫的人,都瞭解資料庫的事務機制,也對ACID四個
基本特性如數家珍。但是聊起事務或者ACID的底層實現原理,往往言之不詳,不明所以。在MySQL中
的事務是由儲存引擎實現的,而且支援事務的儲存引擎不多,我們主要講解InnoDB儲存引擎中的事
務。所以,今天我們就一起來分析和探討InnoDB的事務機制,希望能建立起對事務底層實現原理的具
體瞭解。
事務的特性
- 原子性:事務最小工作單元,事務開始要不全部成功,要不全部失敗.
- 一致性:事務的開始和結束後,資料庫的完整性不會被破壞
- 隔離性:不同事務之間互不影響,四種隔離級別為RU(讀未提交)、RC(讀已提
交)、RR(可重複讀)、SERIALIZABLE (序列化)。 - 永續性:事務提交後,對資料的修改是永久性的,即使系統故障也不會丟失 。
隔離級別
有一張表,結構如下:
未提交讀(RU)
- 一個事務讀取到另一個事務尚未提交的資料,稱之為髒讀
發生時間編號 session A session B 1 begin; 2 begin; 3 update t set c="關羽" where id = 1; 4 select * from t where id = 1;
時間編號為4時,AB兩個session均未提交事務,select語句讀取到的值為關羽,讀取到了B尚未提交的事務,此為髒讀,這種隔離級別是最不安全的一種.
已提交讀(RC)
- 一個事務讀取到另一個事務已提交的資料,導致對同一條記錄讀取兩次以上的結果不一致,稱之為不可重複讀
發生時間編號 session A session B 1 begin; 2 begin; 3 update t set c="關羽" where id = 1; 4 select * from t where id = 1; 5 commit; 6 select * from t where id = 1;
時間編號為4時,B尚未提交,此時讀取到的資料依然是劉備,時間編號為5,B事務提交,時間編號為6時再次讀取到的資料變成了關羽.這種情況是可以被理解的,因為B事務已經提交了.
可重複讀(RR)
- 一個事務讀取到另一個事務已經提交的delete或者insert資料,導致對同一張表讀取兩次以上結果不一致,稱之為幻讀
- 幻讀可以通過序列化或者間隙鎖來解決
發生時間編號 session A session B 1 begin; 2 begin; 3 update t set c="關羽" where id = 1; 4 select * from t where id = 1; 5 commit; 6 select * from t where id = 1; 時間編號為4時,B尚未提交,此時讀取到的資料依然是劉備,時間編號為5,B事務提交,時間編號為6時再次讀取到的資料依然是劉備.同一個事務中讀取到的資料永遠是一致的.
序列化
- 簡單來說就是加鎖,這種隔離級別是最安全的,可以解決其他隔離級別所產生的問題,但是效率較低.
發生時間編號 session A session B 1 begin; 2 begin; 3 update t set c="關羽" where id = 1; 4 select * from t where id = 1; 5 commit; 6 select * from t where id = 1; 時間編號為4時,B尚未提交,此時讀取時,將會被阻塞,處於等待中直到B事務提交釋放鎖,時間編號為5,B事務提交釋放鎖,時間編號為6時再次讀取到的資料是關羽.
丟失更新,兩個事務同時對一條資料進行修改時,會存在丟失更新問題.
時間 取款事務A 取款事務B 1 開始事務 2 開始事務 3 查詢餘額為1000元 4 查詢餘額為1000元 5 匯入100元,餘額變為1100 6 提交事務 7 取出100元,餘額變為900元 8 回滾事務 9 餘額恢復為1000元,丟失更新
mysql的預設隔離級別為RR
資料庫的事務併發問題需要使用併發控制機制去解決,資料庫的併發控制機制有很多,最為常見
的就是鎖機制。鎖機制一般會給競爭資源加鎖,阻塞讀或者寫操作來解決事務之間的競爭條件,
最終保證事務的可序列化。而MVCC則引入了另外一種併發控制,它讓讀寫操作互不阻塞,每一個寫操作都會建立一個新版
本的資料,讀操作會從有限多個版本的資料中挑選一個最合適的結果直接返回,由此解決了事務
的競爭條件。
MVCC
mvcc也是多版本併發控制,mysql中引入了這種併發機制.我們接下來就聊聊mvcc
版本鏈
回滾段/undo log
insert undo log
是在 insert 操作中產生的 undo log。
因為 insert 操作的記錄只對事務本身可見,對於其它事務此記錄是不可見的,所以 insert undo
log 可以在事務提交後直接刪除而不需要進行 purge 操作。
update undo log
- 是 update 或 delete 操作中產生的 undo log
- 因為會對已經存在的記錄產生影響,為了提供 MVCC機制,因此 update undo log 不能在事務提交時就進行刪除,而是將事務提交時放到入 history list 上,等待 purge 執行緒進行最後的刪除操作
為了保證事務併發操作時,在寫各自的undo log時不產生衝突,InnoDB採用回滾段的方式來維護undo
log的併發寫入和持久化。回滾段實際上是一種 Undo 檔案組織方式。
InnoDB行記錄有三個隱藏欄位:分別對應該行的rowid、事務號db_trx_id和回滾指標db_roll_ptr,其
中db_trx_id表示最近修改的事務的id,db_roll_ptr指向回滾段中的undo log。
對於使用 InnoDB 儲存引擎的表來說,它的聚簇索引記錄中都包含兩個必要的隱藏列( row_id 並不是
必要的,我們建立的表中有主鍵或者非NULL唯一鍵時都不會包含 row_id 列):
- trx_id :每次對某條聚簇索引記錄進行改動時,都會把對應的事務id賦值給 trx_id 隱藏列。
- roll_pointer :每次對某條聚簇索引記錄進行改動時,都會把舊的版本寫入到 undo日誌 中,然
後這個隱藏列就相當於一個指標,可以通過它來找到該記錄修改前的資訊。
我們有一張表
create table user(
id int,
name varchar,
primary key (id)
)
insert into user values(1,'張三');
我們此時插入這條資料,假設事務id為80.
ps:咳咳~~理解意思就好,捂臉.jpg
每次對記錄進行改動,都會記錄一條 undo日誌 ,每條 undo日誌 也都有一個 roll_pointer 屬性
( INSERT 操作對應的 undo日誌 沒有該屬性,因為該記錄並沒有更早的版本),可以將這些 undo日誌
都連起來,串成一個連結串列,所以現在的情況就像下圖一樣:
對該記錄每次更新後,都會將舊值放到一條 undo日誌 中,就算是該記錄的一箇舊版本,隨著更新次數
的增多,所有的版本都會被 roll_pointer 屬性連線成一個連結串列,我們把這個連結串列稱之為 版本鏈 ,版本
鏈的頭節點就是當前記錄最新的值。另外,每個版本中還包含生成該版本時對應的事務id,這個資訊很
重要,我們稍後就會用到。
如下圖所示(初始狀態):
當事務2使用UPDATE語句修改該行資料時,會首先使用排他鎖鎖定改行,將該行當前的值複製到undo
log中,然後再真正地修改當前行的值,最後填寫事務ID,使用回滾指標指向undo log中修改前的行。
如下圖所示(第一次修改):
當事務3進行修改與事務2的處理過程類似,如下圖所示(第二次修改):
REPEATABLE READ隔離級別下事務開始後使用MVCC機制進行讀取時,會將當時活動的事務id記錄下
來,記錄到Read View中。READ COMMITTED隔離級別下則是每次讀取時都建立一個新的Read View。
ReadView
對於使用 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 的時機不同,我們來看一下。
RC隔離級別和RR隔離級別區別
每次讀取資料前都生成一個ReadView
比方說現在系統裡有兩個 id 分別為 100 、 200 的事務在執行:
# Transaction 100 BEGIN; UPDATE user SET name = '張三' WHERE id = 1; UPDATE user SET name = '李四' WHERE id = 1; 複製程式碼 # Transaction 200 BEGIN; # 更新了一些別的表的記錄 ...
假設現在有一個使用 READ COMMITTED 隔離級別的事務開始執行:
# 使用READ COMMITTED隔離級別的事務 BEGIN; # SELECT1:Transaction 100、200未提交 SELECT * FROM user WHERE id = 1; # 得到的列name的值為'王五'
這個 SELECT1 的執行過程如下:
- 在執行 SELECT 語句時會先生成一個 ReadView , ReadView 的 m_ids 列表的內容就是 [100,
200] 。 - 然後從版本鏈中挑選可見的記錄,最新版本的列name 的內容是 '張三' ,該版本的trx_id 值為 100 ,在 m_ids 列表內,所以不符合可見性要求,根據 roll_pointer 跳到下一個版本。
- 下一個版本的列 name 的內容是 '李四' ,該版本的 trx_id 值也為 100 ,也在 m_ids 列表內,所以也不符合要求,繼續跳到下一個版本。
- 下一個版本的列 name 的內容是 '王五' ,該版本的 trx_id 值為 80 ,小於 m_ids 列表中最小的事務id 100 ,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列 name 為 '王五' 的記錄。
之後,我們把事務id為 100 的事務提交一下,就像這樣:
# Transaction 100 BEGIN; UPDATE user SET name = '關羽' WHERE id = 1; UPDATE user SET name = '張飛' WHERE id = 1; COMMIT;
然後再到事務id為 200 的事務中更新一下表 user 中 id 為1的記錄:
# Transaction 200 BEGIN; # 更新了一些別的表的記錄 ... UPDATE user SET name = '雲六' WHERE id = 1; UPDATE user SET name = '王麻子' WHERE id = 1;
然後再到剛才使用 READ COMMITTED 隔離級別的事務中繼續查詢這個id為 1 的記錄,如下:
# 使用READ COMMITTED隔離級別的事務 BEGIN; # SELECT1:Transaction 100、200均未提交 SELECT * FROM user WHERE id = 1; # 得到的列name的值為'李四' # SELECT2:Transaction 100提交,Transaction 200未提交 SELECT * FROM user WHERE id = 1; # 得到的列name的值為'張三'
這個 SELECT2 的執行過程如下:
- 在執行 SELECT 語句時會先生成一個 ReadView , ReadView 的 m_ids 列表的內容就是 [200] (事務id為 100 的那個事務已經提交了,所以生成快照時就沒有它了)。
- 然後從版本鏈中挑選可見的記錄,最新版本的列 name 的內容是 '王麻子' ,該版本的 trx_id 值為 200 ,在 m_ids 列表內,所以不符合可見性要求,根據 roll_pointer 跳到下一個版本。
- 下一個版本的列 name 的內容是 '雲六' ,該版本的 trx_id 值為 200 ,也在 m_ids 列表內,所以也不符合要求,繼續跳到下一個版本。
- 下一個版本的列 name 的內容是 '張三' ,該版本的 trx_id 值為 100 ,比 m_ids 列表中最小的事務
id 200 還要小,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列name 為 '張三' 的記錄。
以此類推,如果之後事務id為 200 的記錄也提交了,再此在使用 READ COMMITTED 隔離級別的事務中查詢表user 中 id 值為 1 的記錄時,得到的結果就是 '王麻子' 了,具體流程我們就不分析了。總結一下就
是:使用READ COMMITTED隔離級別的事務在每次查詢開始時都會生成一個獨立的ReadView。- 在執行 SELECT 語句時會先生成一個 ReadView , ReadView 的 m_ids 列表的內容就是 [100,
只在第一次讀取資料生成一個ReadView
對於使用 REPEATABLE READ 隔離級別的事務來說,只會在第一次執行查詢語句時生成一個
ReadView ,之後的查詢就不會重複生成了。我們還是用例子看一下是什麼效果。比方說現在系統裡有兩個 id 分別為 100 、 200 的事務在執行:
# Transaction 100 BEGIN; UPDATE user SET name = '張三' WHERE id = 1; UPDATE user SET name = '李四' WHERE id = 1; 複製程式碼 # Transaction 200 BEGIN; # 更新了一些別的表的記錄 ...
假設現在有一個使用 REPEATABLE READ 隔離級別的事務開始執行:
# 使用REPEATABLE READ隔離級別的事務 BEGIN; # SELECT1:Transaction 100、200未提交 SELECT * FROM user WHERE id = 1; # 得到的列name的值為'王五'
這個 SELECT1 的執行過程如下:
- 在執行 SELECT 語句時會先生成一個 ReadView , ReadView 的 m_ids 列表的內容就是 [100,
200] 。 - 然後從版本鏈中挑選可見的記錄,最新版本的列 name 的內容是 '張三' ,該版本的trx_id 值為 100 ,在 m_ids 列表內,所以不符合可見性要求,根據 roll_pointer 跳到下一個版
本。 - 下一個版本的列name 的內容是 '李四' ,該版本的 trx_id 值也為 100 ,也在 m_ids 列表內,所以也不符合要求,繼續跳到下一個版本。
- 下一個版本的列name 的內容是 '王五' ,該版本的 trx_id 值為 80 ,小於 m_ids 列表中最小的事務id 100 ,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列 name 為 '王五' 的記錄。
之後,我們把事務id為 100 的事務提交一下,就像這樣:
# Transaction 100 BEGIN; UPDATE user SET name = '李四' WHERE id = 1; UPDATE user SET name = '張三' WHERE id = 1; COMMIT;
然後再到事務id為 200 的事務中更新一下表user 中 id 為1的記錄:
# Transaction 200 BEGIN; # 更新了一些別的表的記錄 ... UPDATE user SET name = '雲六' WHERE id = 1; UPDATE user SET name = '王麻子' WHERE id = 1;
然後再到剛才使用 REPEATABLE READ 隔離級別的事務中繼續查詢這個id為 1 的記錄,如下:
# 使用REPEATABLE READ隔離級別的事務 BEGIN; # SELECT1:Transaction 100、200均未提交 SELECT * FROM user WHERE id = 1; # 得到的列name的值為'李四' # SELECT2:Transaction 100提交,Transaction 200未提交 SELECT * FROM user WHERE id = 1; # 得到的列name的值仍為'李四'
這個 SELECT2 的執行過程如下:
- 因為之前已經生成過 ReadView 了,所以此時直接複用之前的 ReadView ,之前的 ReadView 中的
m_ids 列表就是 [100, 200] 。 - 然後從版本鏈中挑選可見的記錄,最新版本的列 name 的內容是 '王麻子' ,該版本的 trx_id 值為 200 ,在 m_ids 列表內,所以不符合可見性要求,根據 roll_pointer 跳到下一個版本。
- 下一個版本的列 name的內容是 '雲六' ,該版本的 trx_id 值為 200 ,也在 m_ids 列表內,所以也不符合要求,繼續跳到下一個版本。
- 下一個版本的列 name 的內容是 '張三' ,該版本的 trx_id 值為 100 ,而 m_ids 列表中是包含值為
100 的事務id的,所以該版本也不符合要求,同理下一個列 name的內容是 '關羽' 的版本也不符合要求。繼續跳到下一個版本。 - 下一個版本的列 name 的內容是 '李四' ,該版本的 trx_id 值為 80 , 80 小於 m_ids 列表中最小的事務id 100 ,所以這個版本是符合要求的,最後返回給使用者的版本就是這條列 name 為 '李四' 的記錄。
也就是說兩次 SELECT 查詢得到的結果是重複的,記錄的列 name 值都是 '李四' ,這就是 可重複讀 的含義。如果我們之後再把事務id為 200 的記錄提交了,之後再到剛才使用 REPEATABLE READ 隔離級別的事務中繼續查詢這個id為 1 的記錄,得到的結果還是 '李四' ,具體執行過程大家可以自己分析一下。
- 在執行 SELECT 語句時會先生成一個 ReadView , ReadView 的 m_ids 列表的內容就是 [100,
InnoDB的MVCC實現
我們首先來看一下wiki上對MVCC的定義:
Multiversion concurrency control (MCC or MVCC), is a concurrency control
method commonly used by database management systems to provide
concurrent access to the database and in programming languages to
implement transactional memory.
由定義可知,MVCC是用於資料庫提供併發訪問控制的併發控制技術。與MVCC相對的,是基於鎖的並
發控制, Lock-Based Concurrency Control 。MVCC最大的好處,相信也是耳熟能詳:讀不加鎖,讀
寫不衝突。在讀多寫少的OLTP應用中,讀寫不衝突是非常重要的,極大的增加了系統的併發效能,這
也是為什麼現階段,幾乎所有的RDBMS,都支援了MVCC。
多版本併發控制僅僅是一種技術概念,並沒有統一的實現標準, 其核心理念就是資料快照,不同的事務
訪問不同版本的資料快照,從而實現不同的事務隔離級別。雖然字面上是說具有多個版本的資料快照,
但這並不意味著資料庫必須拷貝資料,儲存多份資料檔案,這樣會浪費大量的儲存空間。InnoDB通過
事務的undo日誌巧妙地實現了多版本的資料快照。
資料庫的事務有時需要進行回滾操作,這時就需要對之前的操作進行undo。因此,在對資料進行修改
時,InnoDB會產生undo log。當事務需要進行回滾時,InnoDB可以利用這些undo log將資料回滾到修
改之前的樣子。
以上就是本篇部落格分享的內容,歡迎提出問題,討論交流.
聯絡方式:sx_wuyj@163.