目錄
- 1.索引
- (1)局部性原理
- a.局部性原理在計算機中的地位
- b.page
- c.池化技術(Buffer Pool)
- (2)如何理解索引
- (3)索引的原理
- a.page的構成
- b.多層目錄
- c.基于B+樹的索引
- ①B+樹的特性在索引中的作用
- ②為什么不用其它數據結構
- ③聚簇索引和非聚簇索引
- d.輔助索引
- (4)索引的相關操作
- a.創建主鍵索引
- b.創建唯一索引
- c.普通索引的創建
- d.全文索引的創建
- e.索引查詢
- f.刪除索引
- (5)索引創建原則
- 2.事務
- (1)語句分類
- (2)為什么需要事務
- (3)事務的基本理解
- a.begin和commit、回滾
- b.autocommit
- c.事務的保護對象
- (4)事務的隔離級別
- a.讀未提交(Read Uncommitted)
- b.讀提交(Read Committed)
- c.可重復讀(Repeatable Read)
- d.串行化(Serializable)
- e.四種隔離級別特性總結
- f.全局和當前會話隔離性
- (5)事務的特性
- a.原子性(A)
- b.持久性(D)
- c.隔離性(I)
- d.一致性(C)
- (6)多版本并發控制(MVCC)
- a.多事務執行時的線程安全問題
- b.表的三個隱藏字段
- c.版本控制鏈形成過程
- ①加鎖并備份
- ②修改指定列和隱藏列字段
- ③釋放鎖
- ④版本鏈繼續加長
- ⑤delete的版本控制說明
- ⑥回滾
- d.讀視圖(Read View)
- ①快照讀和當前讀
- ②Read View的設計
- ③確定快照讀讀到的歷史版本的邏輯
- e.RC和RR的本質區別
1.索引
(1)局部性原理
a.局部性原理在計算機中的地位
局部性原理在整個計算機的設計理念中非常重要。在微觀設計中,內存和磁盤的讀取方式都是一次性讀取一大塊空間;而在更宏觀的視角來看,內存本身就是局部性原理的產物,系統將CPU高頻訪問和使用的數據從磁盤中提取出來,從而提高訪問效率。
MySQL作為數據庫,應當有著體現局部性原理的設計,就是索引,它也是MySQL的核心原理。
b.page
page就是上面所講的微觀設計,真正系統讀取數據都是一次讀取一個范圍內的數據,而不會一個字節一個字節地讀。 page指的就是MySQL和磁盤進行數據交互的基本單位(16KB — InnoDB,針對不同存儲引擎存在差異),無論讀還是寫,在MySQL眼里一個原子的大小就是16KB。 和內存、磁盤的設計一致,都是典型的用空間換時間,即局部性原理。
c.池化技術(Buffer Pool)
mysqld在內存中運行的時候,其實會單獨為mysqld進程申請一塊名為Buffer Pool的的大內存空間,用來進行各種緩存。這種操作就是前面提及的局部性原理,用于減少系統和磁盤IO的次數,提高效率。 就像一個小孩每次都要花錢,還不如先從家長那里取100塊,最后不買東西的時候再還回去。
(2)如何理解索引
索引用通俗的話來說就是書前面的目錄,用一兩頁的數據來索引幾百頁的正文。在MySQL中,索引具體指主鍵索引(primary key)、唯一鍵索引(unique)、普通索引(index)、全文索引(fulltext,解決中子文索引問題)。
其中最重要的就是主鍵索引,這在前面介紹過。但這個東西不是必要的,難道有的表就沒有主鍵索引嗎?如果沒有主動添加主鍵,MySQL會生成一個默認主鍵(根據我們的插入順序來遞增),只不過不會展示出來罷了。
下面是沒有主鍵索引的表,它會默認生成一個主鍵,我們可以看到顯示的順序是沒有規律的,是按照我們插入的順序顯示的。
我們可以加入主鍵,會發現id變得有序了。但注意,MySQL并不保證select的默認顯示一定按照主鍵排序,這里展示只是說明添加主鍵后存儲結果出現變化,其根源就是索引由默認主鍵索引變成指定的主鍵索引。
針對不同列建立主鍵索引有什么用?使用默認索引不好嗎?
我們依然需要用書目錄來理解,當我們用id來作為索引時,就相當于用id來建立目錄,如 id < 8 的到第7頁,id <10 的到第10頁,這樣的目錄才是真正有效的,因為我們指定的主鍵通常會被我們大量用于定位行。 而默認索引可能會寫第18次插入的數據到第7頁找,第7次插入的數據到第4頁找。這種目錄寫了跟沒寫一樣,最后系統只能以近乎于遍歷的方式去找 id = 8 對應的行。
這里也建議我們指定主鍵索引時盡量用數字,這樣省得系統進行轉換(和用戶名一樣,底層都是轉為數字進行操作的),也比較符合規范,后面的講解中我們都默認主鍵就是數字。
(3)索引的原理
首先我們知道MySQL和磁盤進行IO都是以page為單位進行的,但是我們一直沒探討一個page里面裝的到底是什么,索引在哪呢?接下來就會好好講講數據是如何存到page里面的,索引是以什么樣的方式體現在page里的。
a.page的構成
下面這張表,id是主鍵索引。接下來將從這張表出發來講解索引的原理。
下面是其中一個page(16KB)的簡單組成,可以看到它由數據部分和指針部分組成,用內核鏈表的形式將該表的數據連接起來。
數據存儲部分使用鏈表的原因是鏈表增刪快,查找慢,所以我們后續只需要解決查找問題就行了,索引就是解決這個問題的。
由于id是主鍵,我們可以看到在一個page內主鍵是有序的,數據插入時也會按照主鍵進行有序插入。這意味著只要我想訪問3,找到1就可以推斷出3就在1后面的第3個位置,這種設計是后面索引的關鍵,就和書的頁碼是連續的一樣,目錄里面不會將所有頁的信息保存,只會保存一些關鍵信息。 這里我們要仔細體會。
但是這個結構中并沒有目錄這一概念的體現,我想要找到3,還是得從1開始線性查找,所以這個page是殘缺的,我們需要引入目錄部分。
一個page就叫做一個頁,當我們找到一個page的時候,我們就去它的頁目錄部分,如果我們要找2,我們看到目錄里面有1和3,我們就訪問1,再根據鏈表去訪問2;如果我們要訪問4,我們就通過目錄直接進入3,再向后去找4。這個時候你或許就明白為什么索引需要有序了,只有這樣目錄提供的信息才是有效的。
以上就是單個page的完整組成。
b.多層目錄
一個page里面就有針對page數據的目錄,但這又有一個問題,如果我進入了這個page,但是我想訪問6怎么辦?我想訪問17怎么辦?
這說明我們首先要解決的問題是要找到合適的page,頁目錄只是page內的目錄,我們還需要定位page的目錄。
這里我們就獲得了兩個page的目錄,它們沒有數據部分,只有頁目錄和指針,頁目錄是id = 1和id = 6以及id = 11和id = 16。所以當我們在第一個page想要訪問1~10中的任意一個數字時,頁目錄總能告訴它下一個應該找到哪個page。
但是,如果我們想要訪問1~20中的任意一個呢?我到底該走左邊那個page還是右邊那個呢?這個時候我們應該清楚,我們需要造出一棵樹,只有從樹頂出發,我們才能完全確定后面應該怎么辦。每往上走一層,都會對下一層的page做出一層目錄。
我們再向上做一層目錄。
到這一步,相信很多人就徹底明白這個索引是什么東西了。
如我們想要找17,從第一層的page出發,在其目錄里面看到1和11,根據主鍵索引的有序性,它會直接到11對應的第二層page中。到了第二層之后,看到目錄里面有11和16,它就繼續找到16對應的第三層page,再在其目錄中看到16和18,它就走16,根據鏈表找到17。這就是基于索引整個查找的流程。在這里我們也能想通,如果使用默認的主鍵的話,那這棵樹將毫無任何規律可言,我們用id來找,但是樹的目錄卻是用我們插入的順序來建立的,因此查詢效率幾乎沒有提高。
從物理角度來看,存一張表就是將上圖中的所有page存到磁盤中。但邏輯上上述的這棵樹就叫做B+樹。
c.基于B+樹的索引
①B+樹的特性在索引中的作用
我們來看看這棵B+數有什么特性。首先,真正存的有數據的就是最底下那層,也是我們最初提出的方案。每個page內有頁目錄,對該page內的有效數據進行索引。但是我們還需要對存的有數據的page本身進行索引,這就是搭建上面兩層的核心原因。因此,我們可以將page分成兩個種類,一種稱為專用于索引的page,一種稱為專用于存放數據的page,前者專門用來定位后者,后者里面也有快速定位數據的目錄。
其次,同層級的page都是有指針互相連接的。根據主鍵索引的有序性,當我們第一次訪問10的時候從根出發,那如果我接下來要訪問11呢? 還從根出發是不是有點浪費時間了? 所以同層級的page用指針相互連接可以增加區域訪問數據的效率。
②為什么不用其它數據結構
B+樹是對B樹的改進。B樹的特點是不存在明顯的page分類,每個節點內既有指針又有有效數據,并且同層的各個節點之間沒有指針連接。使用B樹既不滿足數據集中存儲,也不能實現區域訪問,并且由于既存指針又存數據導致目錄條數受限,樹往往更高,所以我們不使用B樹。
二叉樹、AVL樹層數還是偏高,B+樹可以是多叉樹,一個page里面的目錄可以有幾百上千條,壓到3、4層就能存取上億的數據量。而哈希在范圍查找上也不是很好,所以大部分情況下B+樹就是最優解。
當然MySQL官方是支持哈希建立索引的,但主流的存儲引擎InnoDB和MyISAM都不支持,所以我們暫時不考慮這些。
③聚簇索引和非聚簇索引
MyISAM和InnoDB都是采用的B+樹,但MyISAM使用的是聚簇索引,InnoDB是非聚簇索引。
對于這兩個存儲引擎來說,上層的page都是專用于索引的,最底層的page專用于存儲數據,但是非聚簇索引MyISAM還會做一層分離,最底層page存的數據實際上是有效數據的地址,這樣的話在宏觀看來我們就可以把整棵B+樹都當做索引用的。而聚簇索引InnoDB存的就是數據本身,這樣看來整棵B+樹上層用于索引,最底層用于數據存儲。 因此聚簇指的就是索引和存儲相結合,非聚簇是索引和存儲分離。
d.輔助索引
經過上面的講解,我們可以說對主鍵索引已經有了很深的認識。但我們還需要思考一個問題,同一張表還可以有唯一鍵、普通索引,一張表怎么支撐多個索引呢?
對于MyISAM,它的B+樹的最底層存的是有效數據的地址,索引和數據分離(非聚簇索引),所以輔助索引就是創建一棵新的同結構B+樹,和主鍵索引的樹沒有任何區別。
InnoDB就沒這么好操作了,因為InnoDB的主鍵索引B+樹的最底層是有效數據而不是地址。如果多個索引創建多棵同結構的B+樹的話,那就意味著數據會成倍地增加。所以InnoDB的輔助索引和主鍵索引肯定有所不同。
解決辦法是輔助索引的B+樹最底層存的是主鍵值而非完整數據。當我們使用輔助索引時先通過B+樹找到主鍵值,再到主鍵索引的B+樹找到完整數據,整個過程需要兩次索引,這個過程稱為回表查詢。這樣就既解決了同一張表多個索引的問題,也避免了聚簇索引同結構B+樹導致的多份冗雜數據的存儲。
(4)索引的相關操作
a.創建主鍵索引
一張表中最多有一個主鍵索引,可以是復合主鍵。主鍵索引的優勢是效率高(InnoDB只需要查找一棵B+樹即可找到)。但注意創建主鍵索引的列,它的值不能為null,且不能重復。主鍵索引的列基本上是數字
-- 在創建表的時候,直接在字段名后指定 primary key
create table user1(id int primary key, name varchar(30));
-- 在創建表的最后,指定列為主鍵索引
create table user2(id int, name varchar(30), primary key(id));
-- 創建表以后再添加主鍵
create table user3(id int, name varchar(30));
alter table user3 add primary key(id);
b.創建唯一索引
一張表中,可以有多個唯一索引。如果在某一列建立唯一索引,必須保證這列不能有重復數據,但可以可以為空,B+樹自然會留出特殊區域用于null的查詢。
-- 在表定義時,在某列后直接指定unique唯一屬性
create table user4(id int primary key, name varchar(30) unique);
-- 創建表時,在表的后面指定列為unique
create table user5(id int primary key, name varchar(30), unique(name));
-- 創建表以后再添加unique
create table user6(id int primary key, name varchar(30));
alter table user6 add unique(name);
c.普通索引的創建
一張表中可以有多個普通索引(在實際開發中用的比較多),如果經常使用某列來進行查詢等,但是該列有重復的值,那么我們就應該使用普通索引。
--在表的定義最后,指定某列為索引
create table user8(id int primary key,
name varchar(20),
email varchar(30),
index(name)
);--創建完表以后指定列為普通索引
create table user9(id int primary key, name varchar(20), email varchar(30));
alter table user9 add index(name);-- 創建一個索引名為 idx_name 的索引
create table user10(id int primary key, name varchar(20), email varchar(30));
create index idx_name on user10(name);
d.全文索引的創建
當對文章大量字段進行檢索時,會使用到全文索引。但存儲引擎必須是MyISAM,且默認的全文索引僅支持英文,不支持中文。如果對中文進行全文檢索,可以使用sphinx的中文版。
-- 對title和body創建全文索引
create table articles (
id int unsigned auto_increment not null primary key,
title varchar(200),
body text,
fulltext (title, body)
)engine=MyISAM;
e.索引查詢
我們怎么知道這張表有多少索引,有沒有快速的工具來進行查看呢?
-- 向指定的表獲取索引信息
show keys from user;
f.刪除索引
-- 刪除主鍵索引
alter table user1 drop primary key;
-- 普通索引的刪除
alter table user10 drop index idx_name; # 索引名就是show keys from 表名中的 Key_name 字段
drop index name on user8; # drop index 索引名 on 表名
(5)索引創建原則
1.比較頻繁的作為查詢條件的字段應該創建索引
2.唯一性太差的字段不適合單獨創建索引
3.更新非常頻繁的字段不適合作創建索引
4.不會出現在where子句中的字段不該創建索引
2.事務
show engines可以查看MySQL上的存儲引擎信息,只有InnoDB才支持事務。MyISAM不支持,所以后續的講解都是建立在InnoDB之上的。
(1)語句分類
在MySQL中不同關鍵字對應的語句有自己的種類:
DDL(data definition language)數據定義語言,用來維護存儲數據的結構
代表指令: create,drop,alter
DML(data manipulation language)數據操縱語言,用來對數據進行操作
代表指令: insert,delete,update
DML中又單獨分了一個DQL,數據查詢語言
代表指令: select
DCL(Data Control Language)數據控制語言,主要負責權限管理和事務
代表指令: grant,revoke,commit
我們完成的所有功能,都是由上述幾種語句經過對應的排列組合完成的。簡單來說,事務就是對單條或一組語句的包裝和管理,后面會詳細展開。
(2)為什么需要事務
Linux遇到多線程時為什么要考慮加鎖?就是防止一個函數在極短時間內被兩個執行流進入,從而導致邏輯出現錯誤,例如買票系統,兩個執行流同時發現還剩一張票,于是都被if允許進入,導致最終系統剩余票數為負數。這說明在多執行流的時候臨界資源一定要進行保護,要有相應的防范措施。數據庫被多執行流訪問是肯定的,因此MySQL中急需處理多執行流帶來的各種問題,事務就是最終的解決方案。
(3)事務的基本理解
a.begin和commit、回滾
當我們寫下begin;之后,我們就創建了一個事務,通過commit;終止,在這中間我們可以任意執行命令。 我們也可以創建回滾點(savepoint),只要沒有commit,我們可以在任何時候回滾到想要的地方(rollback to默認回滾到begin處)。**如果在會話commit前就退出了,會默認回滾到begin處,等于該事務什么都沒干。
我們在begin和commit間執行的DML語句都可以認為是臨時的,可以被回滾的,并且在ctrl + \異常推出時會自動回滾到begin處。但注意,這只針對DML語句,DDL、DCL語句在MySQL中會隱式commit,即就算沒有commit,這個數據也持久的保存起來了,且無法被回滾。
b.autocommit
那個begin和commit哪來的?之前怎么沒發現它們的存在?事實上,如果沒有顯式寫begin和commit,我們執行的每一條語句都被隱式創建了一個事務,我們可以簡單理解成每條語句都被單獨用隱藏的begin和commit包裝了起來。因此,默認執行時就算崩潰,之前的DML都不會失效,是因為它們早就被單獨當作一個事務commit了。
begin; # 被隱藏
insert into tb21 values (1);
commit; # 被隱藏
這個隱藏的begin和commit由autocommit決定,我們可以將其設置為0,這樣我們就需要手動commit(不需要手寫begin),如果沒有commit,那么退出后我們的DML語句就都失效了。所以我們一般就默認保證autocommit為1就行。
c.事務的保護對象
到這里,我們或許能意識到,事務存在的核心任務是保護DML語句,而DDL和DCL多少會出現隱式commit導致,甚至不受事務隔離等級的約束。因為事務更多地是針對數據本身進行保護,而DDL和DCL是針對數據結構及其權限進行操作。至于DQL不對數據進行任何修改,不在討論范圍內。
(4)事務的隔離級別
根據上述的講解,我們對事務這個概念有了基本的理解。我們執行的每一個DML語句都是被包裝在begin和commit之間的,在commit之前都可稱為臨時數據,提交后才可持久。
但是如果有兩個及以上的事務同時運行,其中一個事務修改了數據,其它事務應不應該看到呢?如果能看到,那這是否說明不同事務之間就存在執行順序的依賴關系了呢?于是我們需要隔離級別來管理它們。
隔離等級至少在兩個會話中才能體現,即多執行流訪問臨界資源的情況。
a.讀未提交(Read Uncommitted)
讀未提交展開成一句話就是 “一個事務可隨時讀到另一個事務位于begin和commit之間指令執行后的結果”,舉個例子就是A會話正處于begin和commit之間,仍未提交,即事務正在處理中,在這個時候A更新了一個數據,這時B會話就能通過select馬上看到這個變化,即B能讀到A未提交的數據(臟讀)。
實際生產中幾乎不可能用這種隔離級別,后面提到的隔離級別所有的缺點在RU這里占完了,還獨占個臟讀的特性。
b.讀提交(Read Committed)
該隔離級別是大多數數據庫的默認的隔離級別,但不是MySQL的默認的隔離級別。 一個事務只能看到其他的已經提交的事務所做的改變。
這種隔離級別會導致一個問題,即一個事務執行時,如果多次 select,可能得到不同的結果,因為事務執行期間可能有其他事務提交了,提交的數據中干擾了該事務的讀取結果。這個問題叫不可重復讀。
c.可重復讀(Repeatable Read)
這是 MySQL 默認的隔離級別,它確保同一個事務在執行中多次讀取數據時,一定會看到同樣的數據,就算其它事務事實上已經修改了該事務讀取的數據。
在理想情況下,可重復讀意味著啟動事務的一瞬間,在這個事務眼里整個數據庫的數據都是靜態的。 大多數情況卻是如此,但是對于有的數據庫,如果出現了insert這條DML語句,那么insert的結果還是會被看到,這種現象叫做幻讀,MySQL解決了幻讀。
d.串行化(Serializable)
這是事務的最高隔離級別,InnoDB通過行級鎖(共享鎖)實現,強制事務排序,使之不可能相互沖突。意思是當有兩個事務修改同一塊資源時,整個mysqld同時只允許一條事務執行,就算它們來自不同會話。這解決了所有數據庫的幻讀問題。
在MySQL中,兩個事務不能同時對同一行數據一邊讀一邊寫(會加鎖讀),一邊寫一邊寫,只有兩邊都在讀才不會觸發串行化。只要還沒有commit就視為正在執行事務,這個時候鎖就一直在該事務手上,其它事務無法訪問,所以串行化被觸發的概率并不低。
串行化可能會導致超時和鎖競爭,這種隔離級別太極端,實際生產基本不使用。
e.四種隔離級別特性總結
下面是對上述4種隔離級別的特性總結
f.全局和當前會話隔離性
我們怎么查看設置當前會話的隔離性呢?
全局隔離性是global.transaction_isolation,它被寫在配置文件中,每創建一個會話默認都是這個隔離性,和環境變量一樣。
當前會話隔離性是session.transaction_isolation,每個會話可以臨時更換而不影響全局設置。
-- 查看全局隔離級別
select @@global.transaction_isolation;
-- 查看當前會話隔離級別
select @@session.transaction_isolation;
-- 查看當前會話隔離級別的簡寫
select @@transaction_isolation;
修改全局隔離性有個易錯點,即通過SQL語句修改的全局隔離性在mysqld重啟后會恢復原樣,因為本質上SQL語句不會修改對應的配置文件。當然,mysqld一般啟動了就很少關,在短時間內修改新連接會話的隔離性是可行的。 session隔離性一般是馬上生效,實在不行就改global然后重啟會話。
-- 修改當前會話隔離級別
set session transaction isolation level read committed;
-- 修改全局隔離級別
set global transaction isolation level read committed;
(5)事務的特性
通過上述的講解,事務是個什么基本上我們已經了解的差不多了,接下來就根據它是什么來總結它實現的特性,這一步總結可以幫我們加深理解事務。
a.原子性(A)
一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。如果事務在執行過程中發生了錯誤,就會被回滾(rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
在上面例子中也深刻體現這一點,在begin和commit中間我們可以隨意執行DML,但是只有提交上去之后數據才會被持久的保存下來,如果中途退出的話該事務的所有DML全部失效,這就是原子性的體現。
b.持久性(D)
經過前面的滲透,這個概念應該不難理解了。當事務處理結束后(commit),對數據的修改就是永久的,即便系統故障也不會丟失,在commit前就是臨時保存的,不具備持久性。
c.隔離性(I)
這就是前面講過的事務的隔離級別體現出來的特性,數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交RU、讀提交RC、可重復讀RR和串行化S。
d.一致性(C)
一致性不對應具體的用法,這個性質其實是由前三個性質共同維護的。
無論是事務開始前和事務結束以后,數據庫的完整性沒有被破壞,并且其寫入的數據完全符合所有的預設規則(原子性、持久性、隔離性),保證后續操作的順利。
另一種說法是,事務執行的結果,使得MySQL從一個一致性狀態,變到另一個一致性狀態。當事務成功提交時,數據庫一直處于一致性狀態(并行時有隔離性和持久性維護)。如果事務中斷,這個時候會通過原子性來保障一致性,否則我們不清楚此時到底寫了多少數據到mysqld里面。
其實一致性和用戶的業務邏輯強相關,需要用戶業務邏輯做支撐,即一致性是由用戶決定的。
(6)多版本并發控制(MVCC)
觀察這四種隔離級別,它們是如何做到的?DML語句修改了數據之后憑什么其他事務觀察不到變化,其底層是什么?這就是下面要討論的多版本并發控制(MVCC)
a.多事務執行時的線程安全問題
隔離級別根本上是為了解決什么問題?
例如,有兩個事務正同時運行,其中一個事務修改了一個數據,另一個事務該不該看到?其中一個事務修改了一個數據,另一個事務能不能繼續修改?
這其實就是讀-讀,讀-寫,寫-寫問題。
讀-讀 : 不存在任何問題,不需要任何并發控制,因為DQL未對數據進行修改等操作。
讀-寫 : 有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀。RC、RR和S是對讀寫問題進行不同級別的處理。
寫-寫 : 有線程安全問題,可能會存在更新丟失問題,需要加鎖處理,RC、RR、S都有對應的加鎖機制。
我們后面重點從讀-寫問題出發,探討RR和RC的如何實現版本控制及快照讀。
b.表的三個隱藏字段
我們創建一張表后除了設定的列,還自帶有隱藏列
DB_ROW_ID:隱含的自增ID(隱藏主鍵),如果表中沒有主鍵,InnoDB會自動生成一個聚簇索引,在索引部分已經提及
DB_TRX_ID: 最近修改該行的事務ID
DB_ROLL_PTR: 回滾指針,指向這條記錄的上一個版本(歷史版本保存在undo log中)
其中undo日志就是mydqld獨立申請的一段內存緩沖區,和Buffer Pool申請的空間不同。
MySQL以服務進程的方式在內存中運行。表數據、索引是在Buffer Pool中緩存的,在合適的時候將數據刷新到磁盤當中的;undo日志則是保存臨時數據和進行版本控制,保存在獨立的緩沖區中。
以上三個隱藏字段是版本控制鏈中的核心成員,后面會進一步展開。
c.版本控制鏈形成過程
①加鎖并備份
當我們有一條數據即將被修改時,就要開始創建版本控制鏈了,即進行MVCC
第一步就是對該行進行加鎖,只有一個執行流當前能對該行進行操作。該行會從Buffer Pool中拷貝一份到undo log中,注意Buffer Pool專用于數據和索引的緩存,undo log獨立用于版本控制等。 注意這個拷貝過程可以是寫時拷貝,即用到的時候再真正拷貝,這里我們就按普通拷貝理解就行。
②修改指定列和隱藏列字段
接下來就根據我們的指令修改指定列和隱藏列,事務ID即造成數據修改的事務的ID,回滾指針指向上一條undo log里面的記錄。
③釋放鎖
只有當事務提交之后,該行的鎖才會被釋放,也就是說在這期間其它事務無法來再次修改該行的數據,這樣的話寫-寫問題就解決了。 可以看出RR和RC也都是有鎖的。
④版本鏈繼續加長
按照上面的邏輯,事務3又來修改該行了。按照加鎖備份,修改數據和隱藏列,釋放鎖的順序,我們可以不斷加長版本鏈,每次都能完整保存該條數據是由誰修改的。
這樣,我們就有了一條基于鏈表記錄的歷史版本鏈。回滾,其實就是用歷史數據覆蓋當前數據。上面的一個一個版本,我們可以稱之為一個一個的快照。
⑤delete的版本控制說明
delete也都有自己的版本控制方式,delete其實不會真正刪除數據,而是會置標簽flag為0,就相當于刪除了,這樣的話回滾也能找到之前的刪除的數據。
⑥回滾
在上述版本鏈中有個需要注意的點,就是這個回滾不僅指事務內回滾,例如當一個事務提交后它就沒辦法回滾了,但這個時候RR級別的其它事務仍可以通過版本鏈回滾到之前的版本。所以上述講述的回滾還針對RR和RC級別設計,不單單是事務內回滾。通過這里我們也能知道,一個事務提交了之后,它的歷史記錄并不會馬上刪除。
回滾操作總的來說就是逆向執行語句。 如delete一個數據,回滾操作就是mysqld執行insert,update后的數據回滾就是根據版本鏈的歷史數據update回來。
d.讀視圖(Read View)
版本鏈形成了,那RR和RC是如何利用版本鏈來確定當前讀到的數據應該是什么樣的呢? 這就是讀視圖干的事了。
①快照讀和當前讀
快照讀:讀取數據快照(歷史版本),而非數據的最新版本。讀取歷史版本可以并行執行
當前讀:讀取數據的最新版本,并對讀取的行加鎖,防止其他事務并發修改。由于加鎖,當前讀是串行的。 這難道不就是前面版本鏈中遇到的嗎?所以涉及到增刪改操作的都是當前讀,它們的執行也都是串行的。我們要知道修改的第一步就是讀到數據,所以說增刪改是當前讀并不意外,它們會在當前讀后進行下一步操作。
至于select是當前讀還是快照讀,需要根據隔離級別確定。
②Read View的設計
快照讀是讀取歷史數據的,但是讀取哪一個歷史數據呢?這就是Read View要解決的問題。
當執行快照讀操作時,MySQL會為當前事務生成一個讀視圖的對象,為我們判斷當前能讀到哪些數據。
下面是讀視圖對應類設計的簡化版。
class ReadView
{
private:
int m_low_limit_id; // 高水位,大于等于這個ID的事務均不可見
int m_up_limit_id; // 低水位:小于這個ID的事務均可見
int m_creator_trx_id; // 創建該 Read View 的事務ID
int m_ids[100]; // 創建視圖時的活躍事務id列表,這里int[100]只是方便理解
// 配合purge,標識該視圖不需要小于m_low_limit_no的undo log,
// 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的undo log
int m_low_limit_no;
bool m_closed; // 標記視圖是否被關閉
};
下面是對其中重要字段的講解:
m_ids是一張列表,用來維護Read View生成時系統正在活躍的事務ID,說明當前事務執行select時列表里的事務仍未commit,這張表是核心。
m_up_limit_id記錄m_ids列表中事務ID最小的ID,注意是最小的事務ID。
m_low_limit_id記錄了ReadView生成時系統尚未分配的下一個事務ID,也就是目前正在執行的事務ID的最大值+1。
m_creator_trx_id指的是創建該ReadView的事務ID,也是執行select語句的事務ID。
③確定快照讀讀到的歷史版本的邏輯
讀取版本鏈的時候,在undo log中我們能找到每個版本及其對應修改的事務ID。根據當前快照讀創建的ReadView中的當前事務ID和版本鏈中記錄修改事務ID,再結合事務ID遞增的特點,我們能夠很好的解決快照讀應該讀哪個歷史版本的問題。
當執行快照讀時,先創建ReadView并獲取最大最小事務ID,之后去找要訪問的數據對應的版本鏈,從最新的開始往前回溯,通過節點ID和當前活躍ID進行比較找到能夠訪問的最新的歷史記錄。
其中節點ID位于正在運行的事務ID區間內但不在m_ids的原因是有的事務執行時間長,導致m_up_limit_id偏低,有部分事務提前退出,這部分事務又實實在在地先于ReadView提交,所以我們還是要讀。
e.RC和RR的本質區別
快照讀會先生成ReadView,判斷后返回可以讀到的歷史版本。那么RR和RC的區別到底在哪呢?
RR級別下ReadView只會在第一次調用快照讀(select)的地方創建(注意不是begin開始處),第二次第三次都是用同一張ReadView。這意味著早于ReadView創建的事務所做的修改均是可見的,所以RR級別可能看到ID比自己還大的事務做出的修改。只要這個事務晚于begin時創建,但又早于ReadView創建時提交就會遇到這種情況。
而RC級別下的事務每次快照讀都會生成一個新的ReadView,因此能一直獲取最新的commit后的結果,這也是它不可重復讀的根本原因。
ReadView是限制能看見哪些事務的根本,RR會在調用時生成一次并用到結束,RC卻會在調用出時刻更新,這就是RC和RR的底層區別。