1 事務(wù)介紹
在MySQL中的事務(wù)是由存儲引擎實現(xiàn)的,而且支持事務(wù)的存儲引擎不多,我們主要講解InnoDB存儲引擎中的事務(wù)。
事務(wù)處理可以用來維護數(shù)據(jù)庫的完整性,保證成批的 SQL 語句要么全部執(zhí)行,要么全部不執(zhí)行。
事務(wù)用來管理 DDL、DML、DCL 操作,比如 insert,update,delete 語句,默認(rèn)是自動提交的。
2 事務(wù)四大特性(ACID)
Atomicity(原子性):構(gòu)成事務(wù)的的所有操作必須是一個邏輯單元,要么全部執(zhí)行,要么全部不執(zhí)行。
Consistency(一致性):數(shù)據(jù)庫在事務(wù)執(zhí)行前后狀態(tài)都必須是穩(wěn)定的或者是一致的。
Isolation(隔離性):事務(wù)之間不會相互影響。由鎖機制和MVCC機制來實現(xiàn)的。MVCC(多版本并發(fā)控制):優(yōu)化讀寫性能(讀不加鎖、讀寫不沖突)
Durability(持久性):事務(wù)執(zhí)行成功后必須全部寫入磁盤。
3 事務(wù)開啟
① BEGIN 或 START TRANSACTION;顯式地開啟一個事務(wù);
② COMMIT 或 COMMIT WORK ,不過二者是等價的。COMMIT會提交事務(wù),并使已對數(shù)據(jù)庫進行的所有修改稱為永久性的;
③ ROLLBACK 或ROLLBACK WORK,不過二者是等價的?;貪L會結(jié)束用戶的事務(wù),并撤銷正在進行的所有未提交的修改;
4 InnoDB架構(gòu)圖


上圖詳細(xì)顯示了InnoDB存儲引擎的體系架構(gòu)。從圖中可見,InnoDB存儲引擎由內(nèi)存池、后臺線程和磁盤文件三大部分組成。接下來我們就來簡單了解一下內(nèi)存相關(guān)的概念和原理。
4.1 InnoDB內(nèi)存結(jié)構(gòu)
4.1.1Buffer Pool緩沖池
InnoDB存儲引擎是基于磁盤存儲的,并將其中的記錄按照頁的方式進行管理。但是由于CPU速度和磁盤速度之間的鴻溝,基于磁盤的數(shù)據(jù)庫系統(tǒng)通常使用緩沖池記錄來提高數(shù)據(jù)庫的的整體性能。
在數(shù)據(jù)庫中進行讀取操作,首先將從磁盤中讀到的頁放在緩沖池中,下次再讀相同的頁中時,首先判斷該頁是否在緩沖池中。若在緩沖池中,稱該頁在緩沖池中被命中,直接讀取該頁。否則,讀取磁盤上的頁。
對于數(shù)據(jù)庫中頁的修改操作,則首先修改在緩沖池中的頁,然后再以一定的頻率刷新到磁盤上。頁從緩沖池刷新回磁盤的操作并不是在每次頁發(fā)生更新時觸發(fā),而是通過一種稱為CheckPoint的機制刷新回磁盤。
所以,緩沖池的大小直接影響著數(shù)據(jù)庫的整體性能,可以通過配置參數(shù)innodb_buffer_pool_size來設(shè)置。
具體來看,緩沖池中緩存的數(shù)據(jù)頁類型有:索引頁、數(shù)據(jù)頁、undo頁、插入緩沖(insert buffer)、自適應(yīng)哈希索引(adaptive hash index)、InnoDB存儲的鎖信息(lock info)和數(shù)據(jù)字典信息(data dictionary)。
在架構(gòu)圖上可以看到,InnoDB存儲引擎的內(nèi)存區(qū)域除了有緩沖池之外,還有重做日志緩沖和額外內(nèi)存池。InnoDB存儲引擎首先將重做日志信息先放到重做日志緩沖區(qū)中,然后按照一定頻率將其刷新到重做日志文件中。重做日志緩沖一般不需要設(shè)置的很大,該值可由配置參數(shù)innodb_log_buffer_size控制。
4.1.1.1 數(shù)據(jù)頁和索引頁
Page是Innodb存儲的最基本結(jié)構(gòu),也是Innodb磁盤管理的最小單位。與數(shù)據(jù)庫相關(guān)的所有內(nèi)容都存儲在Page結(jié)構(gòu)里。Page分為幾種類型,數(shù)據(jù)頁和索引頁就是其中最為重要的兩種類型。當(dāng)做增刪改時,緩存里的數(shù)據(jù)頁和磁盤里的數(shù)據(jù)頁不一致,該數(shù)據(jù)頁為臟頁
4.1.1.2 插入緩沖頁(Insert Buffer Page)
我們都知道,在InnoDB引擎上進行插入操作時,一般需要按照主鍵順序進行插入,這樣才能獲得較高的插入性能。當(dāng)一張表中存在非聚簇的且不唯一的索引時,在插入時,數(shù)據(jù)頁的存放還是按照主鍵進行順序存放,但是對于非聚簇索引葉節(jié)點的插入不再是順序的了,這時就需要離散的訪問非聚簇索引頁,由于隨機讀取的存在導(dǎo)致插入操作性能下降。
InnoDB為此設(shè)計了Insert Buffer來進行插入優(yōu)化。對于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引頁中,而是先判斷插入的非聚集索引是否在緩沖池中,若在,則直接插入;若不在,則先放入到一個Insert Buffer中。看似數(shù)據(jù)庫這個非聚集的索引已經(jīng)查到葉節(jié)點,而實際沒有,這時存放在另外一個位置。然后再以一定的頻率和情況進行Insert Buffer和非聚簇索引頁子節(jié)點的合并操作。這時通常能夠?qū)⒍鄠€插入合并到一個操作中,這樣就大大提高了對于非聚簇索引的插入性能。
4.1.1.3 自適應(yīng)哈希索引(Adaptive Hash Index)
InnoDB會根據(jù)訪問的頻率和模式,為熱點頁建立哈希索引,來提高查詢效率。InnoDB存儲引擎會監(jiān)控對表上各個索引頁的查詢,如果觀察到建立哈希索引可以帶來速度上的提升,則建立哈希索引,所以叫做自適應(yīng)哈希索引。
自適應(yīng)哈希索引是通過緩沖池的B+樹頁構(gòu)建而來,因此建立速度很快,而且不需要對整張數(shù)據(jù)表建立哈希索引。其有一個要求,即對這個頁的連續(xù)訪問模式必須是一樣的,也就是說其查詢的條件(WHERE)必須完全一樣,而且必須是連續(xù)的。
4.1.1.4 鎖信息(lock info)
我們都知道,InnoDB存儲引擎會在行級別上對表數(shù)據(jù)進行上鎖。不過InnoDB也會在數(shù)據(jù)庫內(nèi)部其他很多地方使用鎖,從而允許對多種不同資源提供并發(fā)訪問。數(shù)據(jù)庫系統(tǒng)使用鎖是為了支持對共享資源進行并發(fā)訪問,提供數(shù)據(jù)的完整性和一致性。關(guān)于鎖的具體知識我們之后再進行詳細(xì)學(xué)習(xí)。
4.1.1.5 數(shù)據(jù)字典信息(Data Dictionary)
InnoDB有自己的表緩存,可以稱為表定義緩存或者數(shù)據(jù)字典。當(dāng)InnoDB打開一張表,就增加一個對應(yīng)的對象到數(shù)據(jù)字典。
數(shù)據(jù)字典是對數(shù)據(jù)庫中的數(shù)據(jù)庫對象、表對象等的元信息的集合。在MySQL中,數(shù)據(jù)字典信息內(nèi)容就包括數(shù)據(jù)庫名、表名、表結(jié)構(gòu)、表字段信息、視圖、索引、存儲過程、觸發(fā)器等內(nèi)容。MySQL INFORMATION_SCHEMA庫提供了數(shù)據(jù)庫元數(shù)據(jù)、統(tǒng)計信息、以及有關(guān)MySQL server的訪問信息(例如:數(shù)據(jù)庫名或表名,字段的數(shù)據(jù)類型和訪問權(quán)限等)。該庫中保存的信息也可以稱為MySQL的數(shù)據(jù)字典。
4.1.2 Double Write雙寫
介紹double write之前我們有必要了解partial page write 問題 :
InnoDB 的Page Size一般是16KB,其數(shù)據(jù)校驗也是針對這16KB來計算的,將數(shù)據(jù)寫入到磁盤是以Page為單位進行操作的。而計算機硬件和操作系統(tǒng),在極端情況下(比如斷電)往往并不能保證這一操作的原子性,16K的數(shù)據(jù),寫入4K 時,發(fā)生了系統(tǒng)斷電/os crash ,只有一部分寫是成功的,這種情況下就是 partial page write 問題。很多DBA 會想到系統(tǒng)恢復(fù)后,MySQL 可以根據(jù)redolog 進行恢復(fù),而mysql在恢復(fù)的過程中是檢查page的checksum(checksum就是pgae的最后事務(wù)號),發(fā)生partial page write 問題時,page已經(jīng)損壞,找不到該page中的事務(wù)號,就無法恢復(fù)。

如上圖所示,Double Write由兩部分組成:
①內(nèi)存中的double write buffer,大小為2MB;
②物理磁盤上共享表空間連續(xù)的128個頁,大小也為2MB。
為了解決 partial page write 問題 ,當(dāng)mysql將臟數(shù)據(jù)flush到data file的時候, 并不直接寫磁盤,而是先使用memcopy 將臟數(shù)據(jù)復(fù)制到內(nèi)存中的double write buffer ,之后通過double write buffer再分兩次,每次寫入1MB到共享表空間的物理磁盤上,然后馬上調(diào)用fsync函數(shù),同步到磁盤上,避免操作系統(tǒng)緩沖寫帶來的問題。在這個過程中,doublewrite是順序?qū)懀_銷并不大。在完成doublewrite寫入后,再將double write buffer寫入各表空間文件,這時是離散寫入。
如果發(fā)生了極端情況(斷電)操作系統(tǒng)在將頁寫入磁盤的過程中發(fā)生了崩潰,InnoDB再次啟動后在恢復(fù)過程中,發(fā)現(xiàn)了一個Page數(shù)據(jù)已經(jīng)損壞,那么此時就可以從共享表空間中的double write中找到該頁的一個副本,將其復(fù)制到表空間文件中,再應(yīng)用重做日志進行恢復(fù)。
如果說Insert Buffer給InnoDB存儲引擎帶來了性能上的提升,那么Double Write帶給InnoDB存儲引擎的是數(shù)據(jù)頁的可靠性。
4.1.3 Redo log Buffer 重做日志緩沖
當(dāng)緩沖池中的頁的版本比磁盤要新時,數(shù)據(jù)庫需要將新版本的頁從緩沖池刷新到磁盤。但是如果每次一個頁發(fā)生變化就進行刷新,那么性能開銷是非常大的,于是InnoDB采用了Write Ahead Log策略,即當(dāng)事務(wù)提交時,先寫重做日志,然后再擇時將臟頁寫入磁盤。如果發(fā)生宕機導(dǎo)致數(shù)據(jù)丟失,就通過重做日志進行數(shù)據(jù)恢復(fù)。
InnoDB存儲引擎會首先將重做日志信息先放入重做日志緩沖中,然后再按照一定頻率將其刷新到重做日志文件。重做日志緩沖一般不需要設(shè)置得很大,因為一般情況每一秒鐘都會將重做日志緩沖刷新到日志文件中??赏ㄟ^配置參數(shù)innodb_log_buffer_size控制,默認(rèn)為8MB。
除了每秒刷新機制之外,每次事務(wù)提交時重做日志緩沖也會刷新到日志中。InnoDB是事務(wù)的存儲引擎,其通過Force Log at Commit機制實現(xiàn)事務(wù)的持久性,即當(dāng)事務(wù)提交時,必須先將該事務(wù)的所有日志寫入到重做日志文件進行持久化,然后事務(wù)的提交操作完成才算完成。InnoDB的寫入機制大致入下圖所示。

為了確保每次日志都寫入到重做日志文件,在每次將重做日志緩沖寫入重做日志后,必須調(diào)用一次fsync操作,將緩沖文件從文件系統(tǒng)緩存中真正寫入磁盤。
可以通過innodb_flush_log_at_trx_commit來控制重做日志刷新到磁盤的策略。
該參數(shù)默認(rèn)值為1,表示事務(wù)提交必須進行一次fsync操作,還可以設(shè)置為0和2。
0表示事務(wù)提交時不進行寫入重做日志操作,該操作只在主線程中完成。
2表示提交時寫入重做日志,但是只寫入文件系統(tǒng)緩存,不進行fsync操作。
由此可見,設(shè)置為0時,性能最高,但是喪失了事務(wù)的一致性。
4.1.5 CheckPoint: 檢查點
對于內(nèi)存中的臟頁,什么時候,什么情況下,將多少臟頁寫入磁盤,是由多方面因素決定的。checkpoint的工作之一,就是在一定條件下將內(nèi)存中的臟頁刷新到磁盤。
4.1.5.1 checkpoint的目的
①縮短數(shù)據(jù)庫的恢復(fù)時間
②buffer pool空間不夠用時,臟頁刷新到磁盤,釋放buffer pool空間。
③redo log buffer空間不夠用時,刷新臟頁到磁盤,釋放redo log buffer空間。
4.1.5.2 檢查點分類
按照checkpoint刷新的方式,MySQL中的checkpoint分為兩種,也即sharp checkpoint和fuzzy checkpoint。
①sharp checkpoint(完全檢查點):在關(guān)閉數(shù)據(jù)庫的時候,將buffer pool中的臟頁全部刷新到磁盤中。
②fuzzy checkpoint(模糊檢查點):數(shù)據(jù)庫正常運行時,在不同的時機,將部分臟頁寫入磁盤。將部分臟頁刷新到磁盤,也是為了避免一次性刷新全部的臟頁造成的性能問題。
4.1.5.3 checkpoint發(fā)生的時機
checkpoint都是將buffer pool中的臟頁刷新到磁盤,但是在不同的情況下,checkpoint會被以不同的方式觸發(fā),同時寫入到磁盤的臟頁的數(shù)量也不同。
①master thread checkpoint :在Master Thread中, 以每秒或每十秒的速度從緩沖池的臟頁列表中刷新一定比例的頁回磁盤,這個過程是異步的。正常的用戶線程對數(shù)據(jù)的操作不會被阻塞。
②flush_lru_list checkpoint : flush_lru_list checkpoint是在單獨的page cleaner線程中執(zhí)行的。MySQL對緩存的管理是通過buffer pool中的LRU列表實現(xiàn)的,LRU 空閑列表中要保留一定數(shù)量的空閑頁面,來保證buffer pool中有足夠的空閑頁面來相應(yīng)外界對數(shù)據(jù)庫的請求。當(dāng)這個空間頁面數(shù)量不足的時候,發(fā)生flush_lru_list checkpoint??臻e頁的數(shù)量由innodb_lru_scan_depth參數(shù)表來控制的,因此在空閑列表頁面數(shù)量少于配置的值的時候,會發(fā)生checkpoint,剔除部分LRU列表尾端的頁面。
③async/sync flush checkpoint :Async/Sync Flush checkpoint 發(fā)生在重做日志不可用的時候,將buffer pool中的一部分臟頁刷新到磁盤中,在臟頁寫入磁盤之后,事務(wù)對應(yīng)的重做日志也就可以釋放了。
redo log剩余空間超過25%的時候,無需執(zhí)行Async/Sync Flush checkpoint。
redo log剩余空間不足25%,但是大于10%的時候,執(zhí)行Async Flush checkpoint,刷新到滿足條件1。
redo log剩余空間不足10%的時候,執(zhí)行Sync Flush checkpoint,刷新到滿足條件1。
在mysql 5.6之后,不管是Async Flush checkpoint還是Sync Flush checkpoint,都不會阻塞用戶的查詢進程。
④dirty page too much checkpoint : dirty page too much checkpoint 是在Master Thread 線程中每秒一次的頻率實現(xiàn)的。dirty page too much 意味著buffer pool中的臟頁過多,執(zhí)行checkpoint臟頁刷入磁盤,保證buffer pool中有足夠的可用頁面。dirty page 由innodb_max_dirty_pages_pct配置,innodb_max_dirty_pages_pct 的默認(rèn)值在innodb 1.0之前是90%,之后是75%
4.2 InnoDB磁盤文件
InnoDB的磁盤文件主要分為三大塊:一是系統(tǒng)表空間,二是用戶表空間,三是redo日志文件和歸檔文件。二進制文件(binlog)等文件是MySQL Server層維護的文件,所以未列入InnoDB的磁盤文件中。
系統(tǒng)表空間和用戶表空間
InnoDB系統(tǒng)表空間包含InnoDB數(shù)據(jù)字典(元數(shù)據(jù)以及相關(guān)對象)以及double write buffer,change buffer,undo logs的存儲區(qū)域。
系統(tǒng)表空間也默認(rèn)包含任何用戶在系統(tǒng)表空間創(chuàng)建的表數(shù)據(jù)和索引數(shù)據(jù)。系統(tǒng)表空間是一個共享的表空間因為它是被多個表共享的。
系統(tǒng)表空間是由一個或者多個數(shù)據(jù)文件組成。默認(rèn)情況下,一個初始大小為10MB,名為ibdata1的系統(tǒng)數(shù)據(jù)文件在MySQL的data目錄下被創(chuàng)建。用戶可以使用 innodb_data_file_path 對數(shù)據(jù)文件的大小和數(shù)量進行配置。
innodb_data_file_path 的格式如下:
innodb_data_file_path=datafile1[,datafile2]...
用戶可以通過多個文件組成一個表空間,同時制定文件的屬性:
innodb_data_file_path = /db/ibdata1:1000M;/dr2/db/ibdata2:1000M:autoextend
這里使用/db/ibdata1和/dr2/db/ibdata2兩個文件組成系統(tǒng)表空間。
設(shè)置innodb_data_file_path參數(shù)之后,所有基于InnoDB存儲引擎的表的數(shù)據(jù)都會記錄到該系統(tǒng)表空間中,如果設(shè)置了參數(shù)innodb_file_per_table,則用戶可以將每個基于InnoDB存儲引擎的表產(chǎn)生一個獨立的用戶表空間。用戶表空間的命名規(guī)則為:表名.ibd。
通過這種方式,用戶不用將所有數(shù)據(jù)都存放于默認(rèn)的系統(tǒng)表空間中,但是用戶表空間只存儲該表的數(shù)據(jù)、索引和插入緩沖BITMAP等信息,其余信息還是存放在默認(rèn)的系統(tǒng)表空間中。
下圖顯示InnoDB存儲引擎對于文件的存儲方式,其中frm文件是表結(jié)構(gòu)定義文件,記錄每個表的表結(jié)構(gòu)定義。

4.2.1 系統(tǒng)表空間(共享表空間)
①數(shù)據(jù)字典(data dictionary):記錄數(shù)據(jù)庫相關(guān)信息
②double write buffer:解決部分寫失?。摂嗔眩?br>
③insert buffer:內(nèi)存insert buffer數(shù)據(jù),周期寫入共享表空間,防止意外宕機
④回滾段(rollback segments)
⑤undo空間:undo頁
4.2.1 用戶表空間(獨立表空間)
①每個表的數(shù)據(jù)和索引都會存在自已的表空間中
②每個表的結(jié)構(gòu)
③undo空間:undo頁 (需要設(shè)置)
④double write buffer
4.2.3 重做日志文件和歸檔文件
默認(rèn)情況下,在InnoDB存儲引擎的數(shù)據(jù)目錄下會有兩個名為ib_logfile0和ib_logfile1的文件,這就是InnoDB的重做日志文件(redo log file),它記錄了對于InnoDB存儲引擎的事務(wù)日志。 當(dāng)InnoDB的數(shù)據(jù)存儲文件發(fā)生錯誤時,重做日志文件就能派上用場。InnoDB存儲引擎可以使用重做日志文件將數(shù)據(jù)恢復(fù)為正確狀態(tài),以此來保證數(shù)據(jù)的正確性和完整性。
每個InnoDB存儲引擎至少有1個重做日志文件組(group),每個文件組下至少有2個重做日志文件,如默認(rèn)的ib_logfile0和ib_logfile1。為了得到更高的可靠性,用戶可以設(shè)置多個鏡像日志組,將不同的文件組放在不同的磁盤上,以此來提高重做日志的高可用性。在日志組中每個重做日志文件的大小一致,并以【循環(huán)寫入】的方式運行。InnoDB存儲引擎先寫入重做日志文件1,當(dāng)文件被寫滿時,會切換到重做日志文件2,再當(dāng)重做日志文件2也被寫滿時,再切換到重做日志文件1。

用戶可以使用innodb_log_file_size來設(shè)置重做日志文件的大小,這對InnoDB存儲引擎的性能有著非常大的影響。如果重做日志文件設(shè)置的太大,數(shù)據(jù)丟失時,恢復(fù)時可能需要很長的時間;另一方面,如果設(shè)置的太小,重做日志文件太小會導(dǎo)致依據(jù)checkpoint的檢查需要頻繁刷新臟頁到磁盤中,導(dǎo)致性能的抖動。
5 事務(wù)的ACID特性的具體實現(xiàn)原理
總結(jié)來說,事務(wù)的隔離性由多版本控制機制和鎖實現(xiàn),而原子性、一致性和持久性通過InnoDB的redo log、undo log和Force Log at Commit機制來實現(xiàn)。
5.1 原子性,持久性和一致性
原子性,持久性和一致性主要是通過redo log、undo log和Force Log at Commit機制機制來完成的。
redo log用于在崩潰時恢復(fù)數(shù)據(jù),undo log用于對事務(wù)的影響進行撤銷,也可以用于多版本控制。而Force Log at Commit機制保證事務(wù)提交后redo log日志都已經(jīng)持久化
5.1 .1 redo log
InnoDB對于數(shù)據(jù)文件和日志文件的刷盤遵守WAL(Write ahead redo log) 和Force-log-at-commit兩種規(guī)則,二者保證了事務(wù)的持久性。
WAL要求數(shù)據(jù)的變更寫入到磁盤前,首先必須將內(nèi)存中的日志寫入到磁盤;
Force-log-at-commit要求當(dāng)一 個事務(wù)提交時,所有產(chǎn)生的日志都必須刷新到磁盤上,如果日志刷新成功后,緩沖池中的數(shù)據(jù)刷新到磁盤前數(shù)據(jù)庫發(fā)生了宕機,那么重啟時,數(shù)據(jù)庫可以從日志中恢復(fù)數(shù)據(jù)。

如上圖所示,InnoDB在緩沖池中變更數(shù)據(jù)時,會首先將相關(guān)變更寫入重做日志緩沖中,然后再按時或者當(dāng)事務(wù)提交時寫入磁盤,這符合Force-log-at-commit原則;當(dāng)重做日志寫入磁盤后,緩沖池中的變更數(shù)據(jù)才會依據(jù)checkpoint機制擇時寫入到磁盤中,這符合WAL原則。
checkpoint(檢查點)表示臟頁寫入到磁盤的時機,所以檢查點也就意味著臟數(shù)據(jù)的寫入。
在checkpoint擇時機制中,就有重做日志文件寫滿的判斷。如果重做日志文件太小經(jīng)常被寫滿,就會頻繁導(dǎo)致checkpoint將更改的數(shù)據(jù)寫入磁盤,導(dǎo)致性能抖動。
操作系統(tǒng)的文件系統(tǒng)是帶有緩存的,當(dāng)InnoDB向磁盤寫入數(shù)據(jù)時,有可能只是寫入到了文件系統(tǒng)的緩存中,沒有真正的“落袋為安”。
InnoDB的innodb_flush_log_at_trx_commit屬性可以控制每次事務(wù)提交時InnoDB的行為,該參數(shù)默認(rèn)值為1。
當(dāng)屬性值為0時,事務(wù)提交時,不會對重做日志進行寫入操作,而是等待主線程按時寫入;
當(dāng)屬性值為1時,事務(wù)提交時,會將重做日志寫入文件系統(tǒng)緩存,并且調(diào)用文件系統(tǒng)的fsync,將文件系統(tǒng)緩沖中的數(shù)據(jù)真正寫入磁盤存儲,確保不會出現(xiàn)數(shù)據(jù)丟失;
當(dāng)屬性值為2時,事務(wù)提交時,也會將日志文件寫入文件系統(tǒng)緩存,但是不會調(diào)用fsync,而是讓文件系統(tǒng)自己去判斷何時將緩存寫入磁盤。
日志的刷盤機制如下圖所示:

innodb_flush_log_at_commit是InnoDB性能調(diào)優(yōu)的一個基礎(chǔ)參數(shù),涉及InnoDB的寫入效率和數(shù)據(jù)安全。
當(dāng)參數(shù)值為0時,寫入效率最高,但是數(shù)據(jù)安全最低;
參數(shù)值為1時,寫入效率最低,但是數(shù)據(jù)安全最高;
參數(shù)值為2時,二者都是中等水平。
一般建議將該屬性值設(shè)置為1,以獲得較高的數(shù)據(jù)安全性,而且也只有設(shè)置為1,才能保證事務(wù)的持久性。
5.1.2 undoLog
數(shù)據(jù)庫崩潰重啟后需要從redo log中把未落盤的臟頁數(shù)據(jù)恢復(fù)出來,重新寫入磁盤,保證用戶的數(shù)據(jù)不丟失。當(dāng)然,在崩潰恢復(fù)中還需要回滾沒有提交的事務(wù)。由于回滾操作需要undo日志的支持,undo日志的完整性和可靠性需要redo日志來保證,所以崩潰恢復(fù)先做redo恢復(fù)數(shù)據(jù),然后做undo回滾。

在事務(wù)執(zhí)行的過程中,除了記錄redo log,還會記錄一定量的undo log。undo log記錄了數(shù)據(jù)在每個操作前的狀態(tài),如果事務(wù)執(zhí)行過程中需要回滾,就可以根據(jù)undo log進行回滾操作。

數(shù)據(jù)和回滾日志的邏輯存儲結(jié)構(gòu)

undo log的存儲不同于redo log,它存放在數(shù)據(jù)庫內(nèi)部的一個特殊的段(segment)中,這個段稱為回滾段。回滾段位于共享表空間中。undo段中的以undo page為更小的組織單位。undo page和存儲數(shù)據(jù)庫數(shù)據(jù)和索引的頁類似。因為redo log是物理日志,記錄的是數(shù)據(jù)庫頁的物理修改操作。所以undolog(也看成數(shù)據(jù)庫數(shù)據(jù))的寫入也會產(chǎn)生redo log,也就是undo log的產(chǎn)生會伴隨著redo log的產(chǎn)生,這是因為undo log也需要持久性的保護。如上圖所示,表空間中有回滾段和葉節(jié)點段和非葉節(jié)點段,而三者都有對應(yīng)的頁結(jié)構(gòu)。
我們再來總結(jié)一下數(shù)據(jù)庫事務(wù)的整個流程,如下圖所示。

事務(wù)進行過程中,每次sql語句執(zhí)行,都會記錄undo log和redo log,然后更新數(shù)據(jù)形成臟頁,然后redo log按照時間或者空間等條件進行落盤,undo log和臟頁按照checkpoint進行落盤,落盤后相應(yīng)的redo log就可以刪除了。此時,事務(wù)還未COMMIT,如果發(fā)生崩潰,則首先檢查checkpoint記錄,使用相應(yīng)的redo log進行數(shù)據(jù)和undo log的恢復(fù),然后查看undo log的狀態(tài)發(fā)現(xiàn)事務(wù)尚未提交,然后就使用undo log進行事務(wù)回滾。事務(wù)執(zhí)行COMMIT操作時,會將本事務(wù)相關(guān)的所有redo log都進行落盤,只有所有redo log落盤成功,才算COMMIT成功。然后內(nèi)存中的數(shù)據(jù)臟頁繼續(xù)按照checkpoint進行落盤。如果此時發(fā)生了崩潰,則只使用redo log恢復(fù)數(shù)據(jù)。

5.2 隔離性
5.2.1 事務(wù)隔離級別
四種隔離級別(SQL92標(biāo)準(zhǔn)):
現(xiàn)在來看看MySQL數(shù)據(jù)庫為我們提供的四種隔離級別(由低到高):
① Read uncommitted (讀未提交):最低級別,任何情況都無法保證。
② Read committed (RC,讀已提交):可避免臟讀的發(fā)生。
③ Repeatable read (RR,可重復(fù)讀):可避免臟讀、不可重復(fù)讀的發(fā)生。(注意事項:InnoDB的RR還可以解決幻讀,主要原因是Next-Key鎖,只有RR才能使用Next-Key鎖)
④ Serializable (串行化):可避免臟讀、不可重復(fù)讀、幻讀的發(fā)生。(由MVCC降級為Locking-Base CC)
5.2.2 事務(wù)并發(fā)問題
在事務(wù)的并發(fā)操作中可能會出現(xiàn)一些問題:
①丟失更新:兩個事務(wù)針對同一數(shù)據(jù)都發(fā)生修改操作時,會存在丟失更新問題。
②臟讀:一個事務(wù)讀取到另一個事務(wù)未提交的數(shù)據(jù)。
③不可重復(fù)讀:一個事務(wù)因讀取到另一個事務(wù)已提交的update或者delete數(shù)據(jù)。導(dǎo)致對同一條記錄讀取兩次以上的結(jié)果不一致。
④幻讀:一個事務(wù)因讀取到另一個事務(wù)已提交的insert數(shù)據(jù)。導(dǎo)致對同一張表讀取兩次以上的結(jié)果不一致。
以上是并發(fā)事務(wù)過程中會存在的問題,解決更新丟失可以交給應(yīng)用,但是后三者需要數(shù)據(jù)庫提供事務(wù)間的隔離機制來解決。實現(xiàn)隔離機制的方法主要有兩種:
a.加讀寫鎖
b.一致性快照讀,即 MVCC
5.2.3 現(xiàn)實場景
管理者要查詢所有用戶的存款總額,假設(shè)除了用戶A和用戶B之外,其他用戶的存款總額都為0,A、B用戶各有存1000,所以所有用戶的存款總額為2000。但是在查詢過程中,用戶A會向用戶B進行轉(zhuǎn)賬操作。轉(zhuǎn)賬操作和查詢總額操作的時序圖如下圖所示。
轉(zhuǎn)賬和查詢的時序圖:

如果沒有任何的并發(fā)控制機制,查詢總額事務(wù)先讀取了用戶A的賬戶存款,然后轉(zhuǎn)賬事務(wù)改變了用戶A和用戶B的賬戶存款,最后查詢總額事務(wù)繼續(xù)讀取了轉(zhuǎn)賬后的用戶B的賬號存款,導(dǎo)致最終統(tǒng)計的存款總額多了100元,發(fā)生錯誤。
--創(chuàng)建賬戶表并初始化數(shù)據(jù)
create table tacount(id int , aname varchar(100),acount int , primary key(id));
alter table tacount add index idx_name(aname);
insert into tacount values(1,'a',1000);
insert into tacount values(2,'b',1000);
--設(shè)置隔離級讀未提交(read-uncommitted)
mysql> set session transaction isolation level read uncommitted;
--session 1
mysql> start transaction ;
select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
--session 2
mysql> start transaction;
update tacount set acount=1100 where aname='b';
--session 1
mysql> select * from tacount where aname='b';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 2 | b | 1100 |
+----+-------+--------+
5.2.4 使用鎖機制(LBCC)可以解決上述的問題。
查詢總額事務(wù)會對讀取的行加鎖,等到操作結(jié)束后再釋放所有行上的鎖。因為用戶A的存款被鎖,導(dǎo)致轉(zhuǎn)賬操作被阻塞,直到查詢總額事務(wù)提交并將所有鎖都釋放。
使用鎖機制:

但是這時可能會引入新的問題,當(dāng)轉(zhuǎn)賬操作是從用戶B向用戶A進行轉(zhuǎn)賬時會導(dǎo)致死鎖。轉(zhuǎn)賬事務(wù)會先鎖住用戶B的數(shù)據(jù),等待用戶A數(shù)據(jù)上的鎖,但是查詢總額的事務(wù)卻先鎖住了用戶A數(shù)據(jù),等待用戶B的數(shù)據(jù)上的鎖。
--設(shè)置隔離級別為串行化(serializable) 死鎖演示
mysql> set session transaction isolation level serializable;
--session 1
mysql> start transaction;
select * from tacount where aname='a';
--session 2
mysql> start transaction ;
update tacount set acount=900 where aname='b';
-- session 1
mysql> select * from tacount where aname='b';
-- session 2
mysql> update tacount set acount=1100 where aname='a';
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
5.2.5 使用MVCC機制可以解決這個問題。
查詢總額事務(wù)先讀取了用戶A的賬戶存款,然后轉(zhuǎn)賬事務(wù)會修改用戶A和用戶B賬戶存款,查詢總額事務(wù)讀取用戶B存款時不會讀取轉(zhuǎn)賬事務(wù)修改后的數(shù)據(jù),而是讀取本事務(wù)開始時的數(shù)據(jù)副本(在RR隔離等級下)。
使用MVCC機制(RR隔離級別下的演示情況):

MVCC使得數(shù)據(jù)庫讀不會對數(shù)據(jù)加鎖,普通的SELECT請求不會加鎖,提高了數(shù)據(jù)庫的并發(fā)處理能力。借助MVCC,數(shù)據(jù)庫可以實現(xiàn)READ COMMITTED,REPEATABLE READ等隔離級別,用戶可以查看當(dāng)前數(shù)據(jù)的前一個或者前幾個歷史版本,保證了ACID中的I特性(隔離性)。
-- 顯示當(dāng)前隔離級別為 REPEATABLE-READ MySQL默認(rèn)隔離級別
mysql> select @@tx_isolation;
-- session 1
mysql> start transaction ;
select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2
mysql> start transaction;
update tacount set acount=1100 where aname='a';
-- session 1
mysql> select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2 提交事務(wù)
mysql> commit;
-- session 1 顯示在session 1 事務(wù)開始時的數(shù)據(jù)
mysql> select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- 設(shè)置事務(wù)隔離級別為REPEATABLE-COMMITTED 讀已提交
-- session 1
mysql> set session transaction isolation level read committed;
mysql> start transaction ;
select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2
mysql> set session transaction isolation level read committed;
mysql> start transaction;
update tacount set acount=1100 where aname='a';
-- session 1
mysql> select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1000 |
+----+-------+--------+
-- session 2 提交事務(wù)
mysql> commit;
-- session 1 顯示最新事務(wù)提交后的數(shù)據(jù)
mysql> select * from tacount where aname='a';
+----+-------+--------+
| id | aname | acount |
+----+-------+--------+
| 1 | a | 1100 |
+----+-------+--------+
5.2.6 InnoDB的MVCC實現(xiàn)
5.2.6.1 什么是MVCC
MVCC,Multi-Version Concurrency Control,多版本并發(fā)控制。MVCC 是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫管理系統(tǒng)中,實現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問。
如果有人從數(shù)據(jù)庫中讀數(shù)據(jù)的同時,有另外的人寫入數(shù)據(jù),有可能讀數(shù)據(jù)的人會看到『半寫』或者不一致的數(shù)據(jù)。有很多種方法來解決這個問題,叫做并發(fā)控制方法。最簡單的方法,通過加鎖,讓所有的讀者等待寫者工作完成,但是這樣效率會很差。MVCC 使用了一種不同的手段,每個連接到數(shù)據(jù)庫的讀者,在某個瞬間看到的是數(shù)據(jù)庫的一個快照,寫者寫操作造成的變化在寫操作完成之前(或者數(shù)據(jù)庫事務(wù)提交之前)對于其他的讀者來說是不可見的。
基于提升并發(fā)性能的考慮,各大數(shù)據(jù)庫廠商的事務(wù)型存儲引擎一般都同時實現(xiàn)了多版本并發(fā)控制(MVCC)。不僅是MySQL,包括Oracle、PostgreSQL等其他數(shù)據(jù)庫系統(tǒng)也都實現(xiàn)了。MVCC就像是Java語言中的接口,各個數(shù)據(jù)庫廠商的實現(xiàn)機制不盡相同??梢哉J(rèn)為MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現(xiàn)機制有所不同,但大都實現(xiàn)了非阻塞的讀操作,寫操作也只是鎖定必要的行。MVCC會保存某個時間點上的數(shù)據(jù)快照。這意味著事務(wù)可以看到一個一致的數(shù)據(jù)視圖,不管他們需要跑多久。這同時也意味著不同的事務(wù)在同一個時間點看到的同一個表的數(shù)據(jù)可能是不同的。前面說到不同的存儲引擎的MVCC實現(xiàn)是不同的,典型的有樂觀并發(fā)控制和悲觀并發(fā)控制。
MVCC實現(xiàn)的讀寫不阻塞正如其名:多版本并發(fā)控制,通過一定機制生成一個數(shù)據(jù)請求時間點的一致性數(shù)據(jù)快照(Snapshot),并用這個快照來提供一定級別(語句級或事務(wù)級)的一致性讀取。從用戶的角度來看,好像是數(shù)據(jù)庫可以提供同一數(shù)據(jù)的多個版本
5.2.6.1 一致性非鎖定讀
一致性非鎖定讀(consistent nonlocking read)是指InnoDB存儲引擎通過多版本控制(MVCC)讀取當(dāng)前數(shù)據(jù)庫中行數(shù)據(jù)的方式。如果讀取的行正在執(zhí)行DELETE或UPDATE操作,這時讀取操作不會因此去等待行上鎖的釋放,相反地,InnoDB會去讀取行的一個快照。

上圖直觀地展現(xiàn)了InnoDB一致性非鎖定讀的機制。之所以稱其為非鎖定讀,是因為不需要等待行上排他鎖的釋放??煺諗?shù)據(jù)是指該行的之前版本的數(shù)據(jù),每一條行記錄可能有多個版本,一般稱這種技術(shù)為行多版本技術(shù)。由此帶來的并發(fā)控制,稱之為多版本并發(fā)控制(Multi Version Concurrency Control, MVVC)。InnoDB是通過undo log來實現(xiàn)MVVC(undo log的兩個作用①回滾②讓mvcc讀歷史版本)。undo log本身用來在事務(wù)中回滾數(shù)據(jù),因此快照數(shù)據(jù)本身是沒有額外開銷。此外,讀取快照數(shù)據(jù)是不需要上鎖的,因為沒有事務(wù)需要對歷史的數(shù)據(jù)進行修改操作。
一致性非鎖定讀是InnoDB默認(rèn)的讀取方式,即讀取不會占用和等待行上的鎖。但是并不是在每個事務(wù)隔離級別下都是采用此種方式。此外,即使都是使用一致性非鎖定讀,但是對于快照數(shù)據(jù)的定義也各不相同。
在事務(wù)隔離級別RC和RR,InnoDB使用一致性非鎖定讀。然而,對于快照數(shù)據(jù)的定義卻不同。在RC事務(wù)隔離級別下,一致性非鎖定讀總是讀取被鎖定行的最新一份快照數(shù)據(jù),即每次讀取數(shù)據(jù)前都生成一個ReadView。而在RR事務(wù)隔離級別下,則讀取事務(wù)開始時的行數(shù)據(jù)版本,即在第一次讀取數(shù)據(jù)前生成一個ReadView。
我們下面舉個例子來詳細(xì)說明一下上述的情況。
# session A
mysql> BEGIN;
mysql> SELECT * FROM test WHERE id = 1;
我們首先在會話A中顯示地開啟一個事務(wù),然后讀取test表中的id為1的數(shù)據(jù),但是事務(wù)并沒有結(jié)束。于此同時,用戶再開啟另一個會話B,這樣可以模擬并發(fā)的操作,然后對會話B做出如下的操作:
# session B
mysql> BEGIN;
mysql> UPDATE test SET id = 3 WHERE id = 1;
在會話B的事務(wù)中,將test表中id為1的記錄修改為id=3,但是事務(wù)同樣也沒有提交,這樣id=1的行其實加了一個排他鎖。由于InnoDB在RC和RR事務(wù)隔離級別下使用一致性非鎖定讀,這時如果會話A再次讀取id為1的記錄,仍然能夠讀取到相同的數(shù)據(jù)。此時,RC和RR事務(wù)隔離級別沒有任何區(qū)別。

如上圖所示,當(dāng)會話B提交事務(wù)后,會話A再次運行 SELECT * FROM test WHERE id = 1 的SQL語句時,兩個事務(wù)隔離級別下得到的結(jié)果就不一樣了。
對于RC的事務(wù)隔離級別,它總是讀取行的最新版本,如果行被鎖定了,則讀取該行版本的最新一個快照。因為會話B的事務(wù)已經(jīng)提交,所以在該隔離級別下上述SQL語句的結(jié)果集是空的。
對于RR的事務(wù)隔離級別,總是讀取事務(wù)開始時的行數(shù)據(jù),因此,在該隔離級別下,上述SQL語句仍然會獲得相同的數(shù)據(jù)。
5.2.6.2 當(dāng)前讀和快照讀
在MVCC并發(fā)控制中,讀操作可以分成兩類:快照讀 (snapshot read)與當(dāng)前讀 (current read)。
- 快照讀,就是讀取數(shù)據(jù)的時候會根據(jù)一定規(guī)則讀取事務(wù)可見版本的數(shù)據(jù)(可能是過期的數(shù)據(jù)),不用加鎖。
- 當(dāng)前讀,讀取的是最新版本, 并且對讀取的記錄加鎖,保證其他事務(wù)不會再并發(fā)的修改這條記錄,避免出現(xiàn)安全問題。
在一個支持MVCC并發(fā)控制的系統(tǒng)中,哪些讀操作是快照讀?哪些操作又是當(dāng)前讀呢?
以MySQL InnoDB為例:
使用當(dāng)前讀的場景:
①select…lock in share mode (共享讀鎖)
②select…for update
③update
④delete
⑤insert
使用快照讀的場景:
單純的select操作,不包括上述 select … lock in share mode、select … for update

5.2.6.3 MVCC 在mysql 中的實現(xiàn)依賴的是 undo log 與 read view
多版本并發(fā)控制僅僅是一種技術(shù)概念,并沒有統(tǒng)一的實現(xiàn)標(biāo)準(zhǔn), 其的核心理念就是數(shù)據(jù)快照,不同的事務(wù)訪問不同版本的數(shù)據(jù)快照,從而實現(xiàn)不同的事務(wù)隔離級別。雖然字面上是說具有多個版本的數(shù)據(jù)快照,但這并不意味著數(shù)據(jù)庫必須拷貝數(shù)據(jù),保存多份數(shù)據(jù)文件,這樣會浪費大量的存儲空間。InnoDB通過事務(wù)的undo日志巧妙地實現(xiàn)了多版本的數(shù)據(jù)快照。
數(shù)據(jù)庫的事務(wù)有時需要進行回滾操作,這時就需要對之前的操作進行undo。因此,在對數(shù)據(jù)進行修改時,InnoDB會產(chǎn)生undo log。當(dāng)事務(wù)需要進行回滾時,InnoDB可以利用這些undo log將數(shù)據(jù)回滾到修改之前的樣子。
5.2.6.3.1 Undo Log
根據(jù)行為的不同 undo log 分為兩種 insert undo log和update undo log。
insert undo log 是在 insert 操作中產(chǎn)生的 undo log。因為 insert 操作的記錄只對事務(wù)本身可見,對于其它事務(wù)此記錄是不可見的,所以 insert undo log 可以在事務(wù)提交后直接刪除而不需要進行 purge 操作。
update undo log 是 update 或 delete 操作中產(chǎn)生的 undo log,因為會對已經(jīng)存在的記錄產(chǎn)生影響,為了提供 MVCC機制,因此 update undo log 不能在事務(wù)提交時就進行刪除,而是將事務(wù)提交時放到入 history list 上,等待 purge 線程進行最后的刪除操作。

為了保證事務(wù)并發(fā)操作時在寫各自的undo log時不產(chǎn)生沖突,InnoDB采用回滾段的方式來維護undo log的并發(fā)寫入和持久化?;貪L段實際上是一種 Undo 文件組織方式。
InnoDB行記錄有三個隱藏字段:行號rowid、事務(wù)號db_trx_id和回滾指針db_roll_pt,其中db_trx_id表示最近修改的事務(wù)的id,db_roll_pt指向回滾段中的undo log。
如下圖所示(初始狀態(tài)):

當(dāng)事務(wù)2使用UPDATE語句修改該行數(shù)據(jù)時,會首先使用排他鎖鎖定該行,將該行當(dāng)前的值復(fù)制到undo log中,然后再真正地修改當(dāng)前行的值,最后填寫事務(wù)ID,使用回滾指針指向undo log中修改前的行。
如下圖所示(第一次修改):

當(dāng)事務(wù)3進行修改與事務(wù)2的處理過程類似。
如下圖所示(第二次修改):

5.2.6.3.2 事務(wù)鏈表
Mysql中的事務(wù)在開始到提交過程中,都會被保存到一個叫trx_sys鏈表中。
事務(wù)鏈表中保存的都是還未提交的事務(wù),事務(wù)一旦被提交,則會從事務(wù)鏈表中摘除。
RR隔離級別下,在每個事務(wù)開始的時候,會將當(dāng)前系統(tǒng)中所有活躍事務(wù)拷貝到一個列表中(read view)。
RC隔離級別下,在每條語句開始的時候,會將當(dāng)前系統(tǒng)中活躍事務(wù)拷貝到一個列表中(read view)。
查看事務(wù)列表:
show engine innodb status;
5.2.6.3.3 read view
Read View是事務(wù)開啟時,當(dāng)前所有事務(wù)的一個集合,這個數(shù)據(jù)結(jié)構(gòu)中存儲了當(dāng)前Read View中最大的ID及最小的ID。
這就是當(dāng)前活躍事務(wù)列表,如下所示:
ct-trx --> trx11 --> trx9 --> trx6 --> trx5 --> trx3;
ct-trx 表示當(dāng)前事務(wù)的id,對應(yīng)上面的read_view數(shù)據(jù)結(jié)構(gòu)如下
read_view->creator_trx_id = ct-trx;
read_view->up_limit_id = trx3;
read_view->low_limit_id = trx11;
read_view->trx_ids = [trx11, trx9, trx6, trx5, trx3];
low_limit_id是“高水位”,即當(dāng)時活躍事務(wù)的最大id,如果讀到row的db_trx_id>=low_limit_id,說明這些id在此之前的數(shù)據(jù)都沒有提交,這些數(shù)據(jù)都不可見。
if (trx_id >= view->low_limit_id) {
return(FALSE);
}
注:readview 部分源碼
up_limit_id是“低水位”,即當(dāng)時活躍事務(wù)的最小事務(wù)id,如果row的db_trx_id<up_limit_id,說明這些數(shù)據(jù)在事務(wù)創(chuàng)建id的時都已經(jīng)提交,這些數(shù)據(jù)均可見。
if (trx_id < view->up_limit_id) {
return(TRUE);
}
row的db_trx_id在low_limit_id和up_limit_id之間,則查找該記錄的db_trx_id是否在自己事務(wù)的read_view->trx_ids列表中,如果在則該記錄的當(dāng)前版本不可見,否則該記錄的當(dāng)前版本可見。

不同隔離級別ReadView實現(xiàn)方式
read-commited:
函數(shù):ha_innobase::external_lock
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED && trx->global_read_view) {
/ At low transaction isolation levels we let each consistent read set its own snapshot /
read_view_close_for_mysql(trx);
}
即:在每次語句執(zhí)行的過程中,都關(guān)閉read_view, 重新在row_search_for_mysql函數(shù)中創(chuàng)建當(dāng)前的一份read_view。這樣就會產(chǎn)生不可重復(fù)讀現(xiàn)象發(fā)生。
repeatable read:
在repeatable read的隔離級別下,創(chuàng)建事務(wù)trx結(jié)構(gòu)的時候,就生成了當(dāng)前的global read view。使用trx_assign_read_view函數(shù)創(chuàng)建,一直維持到事務(wù)結(jié)束。在事務(wù)結(jié)束這段時間內(nèi) 每一次查詢都不會重新重建Read View , 從而實現(xiàn)了可重復(fù)讀。