相信工做了一段時間的同窗確定都用過事務,也都據說過事務的4大特性ACID。ACID表示原子性、一致性、隔離性和持久性。一個很好的事務處理系統,必須具有這些標準特性:html
而咱們最常說的隔離性其實有對應的隔離級別,MySQL規定的隔離級別有4種,分別是:mysql
能夠看到隔離級別裏最重要的只有兩個隔離級別:RC和RR。那麼問題來了,咱們知道上面說的ACID以及隔離級別的實現原理嗎?不管是平時工做仍是面試,這部分的問題都重中之重,接下來,我會拋出幾個問題,你們能夠帶着問題來看此文:面試
ACID問題:sql
隔離性裏隔離級別的問題:數據庫
解決這些問題以前,咱們要首先知道Redo log、Undo log以及MVCC都是什麼。設計模式
redo log(重作日誌)用來實現事務的持久性,即事務ACID中的D。其由兩部分組成,一是內存中的重作日誌緩衝(redo log buffer),其實易失的。二是重作日誌文件(redo log file),其是持久的。緩存
在一個事務中的每一次SQL操做以後都會寫入一個redo log到buffer中,在最後COMMIT的時候,必須先將該事務的全部日誌寫入到redo log file進行持久化(這裏的寫入是順序寫的),待事務的COMMIT操做完成纔算完成。數據結構
因爲重作日誌文件打開沒有使用O_DIRECT選項,所以重作日誌緩衝先寫入文件系統緩存。爲了確保重作日誌寫入磁盤,必須進行一次fsync操做。因爲fsync的效率取決於磁盤的性能,所以磁盤的性能決定了事務提交的性能,也就是數據庫的性能。由此咱們能夠得出在進行批量操做的時候,不要for循環裏面嵌套事務。併發
參數 innodb_flush_log_at_trx_commit
用來控制重作日誌刷新到磁盤的策略,該參數有3個值:0、1和2。異步
咱們能夠看到0和2的設置都比1的效率要高,可是破壞了數據庫的ACID特性,不建議使用!
在MySQL數據庫中還有一種二進制日誌(binlog),從表面上來看它和redo log很類似,都是記錄了對數據庫操做的日誌,可是,它們有着很是大的不一樣。
首先,redo log是在MySQL的InnoDB引擎層產生,而binlog則是在MySQL的上層產生,它不只針對InnoDB引擎,其餘任何引擎對於數據庫的更改都會產生binlog。
其次,兩種日誌記錄的內容形式不一樣,binlog是一種邏輯日誌,其記錄的是對應的SQL語句。而redo log則是記錄的物理格式日誌,其記錄的是對於每一個頁的修改。
此外,兩種日誌記錄寫入磁盤的時間點不一樣,binlog只在事務提交完成後一次性寫入,而redo log在上面也說了是在事務進行中不斷被寫入,這表現爲日誌並非隨事務提交的順序進行寫入的。
在InnoDB引擎中,redo log都是以512字節進行存儲的(和磁盤扇區的大小同樣,所以redo log寫入能夠保證原子性,不須要double write),也就是重作日誌緩存和文件都是以塊的方式進行保存的,稱爲redo log block,每一個block佔512字節。
重作日誌除了日誌自己以外,還由日誌塊頭(log block header)及日誌塊尾(log block tailer)兩部分組成。
下面我來解釋一下組成Log Block header的4個部分各自的含義:
Log Block tailer只包含一個LOG_BLOCK_TRL_NO,它的值和LOG_BLOCK_HDR_NO相同,並在函數log_block_init中被初始化。
前面提到了redo log是用來實現ACID的持久性的,也就是隻要事務提交成功後,事務內的全部修改都會保存到數據庫,哪怕這時候數據庫crash了,也要有辦法來進行恢復。也就是Crash Recovery。
說到恢復,咱們先來了解一個概念:什麼是LSN?
LSN(log sequence number) 用於記錄日誌序號,它是一個不斷遞增的 unsigned long long 類型整數,佔用8字節。它表明的含義有:
checkpoint:它是redo log中的一個檢查點,這個點以前的全部數據都已經刷新回磁盤,當DB crash後,經過對checkpoint以後的redo log進行恢復就能夠了。
咱們能夠經過命令show engine innodb status來觀察LSN的狀況:
--- LOG --- Log sequence number 33646077360 Log flushed up to 33646077360 Last checkpoint at 33646077360 0 pending log writes, 0 pending chkp writes 49687445 log i/o's done, 1.25 log i/o's/second
Log sequence number表示當前的LSN,Log flushed up to表示刷新到redo log文件的LSN,Last checkpoint at表示刷新到磁盤的LSN。若是把它們三個簡寫爲 A、B、C
的話,它們的值的大小確定爲 A>=B>=C
。
InnoDB引擎在啓動時無論上次數據庫運行時是否正常關閉,都會進行恢復操做。由於重作日誌記錄的是物理日誌,所以恢復的速度比邏輯日誌,如二進制日誌要快不少。恢復的時候只須要找到redo log的checkpoint進行恢復便可。
重作日誌記錄了事務的行爲,能夠很好的經過其對頁進行「重作」操做。可是事務有時候還須要進行回滾操做,也就是ACID中的A(原子性),這時就須要Undo log了。所以在數據庫進行修改時,InnoDB存儲引擎不但會產生Redo,還會產生必定量的Undo。這樣若是用戶執行的事務或語句因爲某種緣由失敗了,又或者用戶一條ROLLBACK語句請求回滾,就能夠利用這些Undo信息將數據庫回滾到修改以前的樣子。
Undo log是InnoDB MVCC事務特性的重要組成部分。當咱們對記錄作了變動操做時就會產生Undo記錄,Undo記錄默認被記錄到系統表空間(ibdata)中,但從5.6開始,也可使用獨立的Undo 表空間。
Undo記錄中存儲的是老版本數據,當一箇舊的事務須要讀取數據時,爲了能讀取到老版本的數據,須要順着undo鏈找到知足其可見性的記錄。當版本鏈很長時,一般能夠認爲這是個比較耗時的操做。
爲了保證事務併發操做時,在寫各自的undo log時不產生衝突,InnoDB採用回滾段(Rollback Segment,簡稱Rseg)的方式來維護undo log的併發寫入和持久化。回滾段其實是一種 Undo 文件組織方式,每一個回滾段又有多個undo log slot。具體的文件組織方式以下圖所示:
上圖展現了基本的Undo回滾段佈局結構,其中:
innodb_undo_logs
設置)存放到獨立undo表空間中(若是沒有打開獨立Undo表空間,則存放於ibdata中,獨立表空間能夠經過參數 innodb_undo_directory
設置),用於普通事務的undo。如圖所示,每一個回滾段維護了一個段頭頁,在該page中又劃分了1024個slot(TRX_RSEG_N_SLOTS),每一個slot又對應到一個undo log對象,所以理論上InnoDB最多支持 96 * 1024個普通事務。
在InnoDB引擎中,undo log分爲:
insert undo log是指在insert操做中產生的undo log,由於insert操做的記錄,只對事務自己可見,對其餘事務不可見(這是事務隔離性的要求),故該undo log能夠在事務提交後直接刪除,不須要進行purge操做。而update undo log記錄的是delete和update操做產生的undo log。該undo log可能須要提供MVCC機制,所以不能在事務提交時就進行刪除,提交時放入undo log鏈表,等待purge線程進行最後的刪除。下面是兩種undo log的結構圖。
對於一條delete語句 delete from t where a = 1
,若是列a有彙集索引,則不會進行真正的刪除,而只是在主鍵列等於1的記錄delete flag設置爲1,即記錄仍是存在在B+樹中。而對於update操做,不是直接對記錄進行更新,而是標識舊記錄爲刪除狀態,而後新產生一條記錄。那這些舊版本標識位刪除的記錄什麼時候真正的刪除?怎麼刪除?
其實InnoDB是經過undo日誌來進行舊版本的刪除操做的,在InnoDB內部,這個操做被稱之爲purge操做,原來在srv_master_thread主線程中完成,後來進行優化,開闢了purge線程進行purge操做,而且能夠設置purge線程的數量。purge操做每10s進行一次。
爲了節省存儲空間,InnoDB存儲引擎的undo log設計是這樣的:一個頁上容許多個事務的undo log存在。雖然這不表明事務在全局過程當中提交的順序,可是後面的事務產生的undo log總在最後。此外,InnoDB存儲引擎還有一個history列表,它根據事務提交的順序,將undo log進行鏈接,以下面的一種狀況:
在執行purge過程當中,InnoDB存儲引擎首先從history list中找到第一個須要被清理的記錄,這裏爲trx1,清理以後InnoDB存儲引擎會在trx1所在的Undo page中繼續尋找是否存在能夠被清理的記錄,這裏會找到事務trx3,接着找到trx5,可是發現trx5被其餘事務所引用而不能清理,故再去history list中取查找,發現最尾端的記錄時trx2,接着找到trx2所在的Undo page,依次把trx六、trx4清理,因爲Undo page2中全部的記錄都被清理了,所以該Undo page能夠進行重用。
InnoDB存儲引擎這種先從history list中找undo log,而後再從Undo page中找undo log的設計模式是爲了不大量隨機讀操做,從而提升purge的效率。
MVCC 多版本併發控制技術,用於多事務環境下,對數據讀寫在不加讀寫鎖的狀況下實現互不干擾,從而實現數據庫的隔離性,在事務隔離級別爲Read Commit 和 Repeatable Read中使用到,今天咱們就用最簡單的方式,來分析下MVCC具體的原理,先解釋幾個概念。
InnoDB表數據的組織方式爲主鍵聚簇索引,二級索引中採用的是(索引鍵值, 主鍵鍵值)的組合來惟一肯定一條記錄。
InnoDB表數據爲主鍵聚簇索引,mysql默認爲每一個索引行添加了4個隱藏的字段,分別是:
整個MVCC的機制都是經過DB_TRX_ID
,DB_ROLL_PTR
這2個隱藏字段來實現的。
當一個事務開始的時候,會將當前數據庫中正在活躍的全部事務(執行begin,可是尚未commit的事務)保存到一個叫trx_sys
的事務鏈表中,事務鏈表中保存的都是未提交的事務,當事務提交以後會從其中刪除。
有了前面隱藏列和事務鏈表的基礎,接下去就能夠構造MySQL實現MVCC的關鍵——ReadView。
ReadView說白了就是一個數據結構,在事務開始的時候會根據上面的事務鏈表構造一個ReadView,初始化方法以下:
// readview 初始化 // m_low_limit_id = trx_sys->max_trx_id; // m_up_limit_id = !m_ids.empty() ? m_ids.front() : m_low_limit_id; ReadView::ReadView() : m_low_limit_id(), m_up_limit_id(), m_creator_trx_id(), m_ids(), m_low_limit_no() { ut_d(::memset(&m_view_list, 0x0, sizeof(m_view_list))); }
總共作了如下幾件事:
trx_sys
)中事務id最大的值被賦值給m_low_limit_id
。m_up_limit_id
。m_ids
爲事務鏈表。經過該ReadView,新的事務能夠根據查詢到的全部活躍事務記錄的事務ID來匹配可以看見該記錄,從而實現數據庫的事務隔離,主要邏輯以下:
那麼問題來了,怎麼來判斷可見性呢?咱們來經過源碼一探究竟:
// 判斷數據對應的聚簇索引中的事務id在這個readview中是否可見 bool changes_visible( trx_id_t id, // 記錄的id const table_name_t& name) const MY_ATTRIBUTE((warn_unused_result)) { ut_ad(id > 0); // 若是當前記錄id < 事務鏈表的最小值或者等於建立該readview的id就是它本身,那麼是可見的 if (id < m_up_limit_id || id == m_creator_trx_id) { return(true); } check_trx_id_sanity(id, name); // 若是該記錄的事務id大於事務鏈表中的最大值,那麼不可見 if (id >= m_low_limit_id) { return(false); // 若是事務鏈表是空的,那也是可見的 } else if (m_ids.empty()) { return(true); } const ids_t::value_type* p = m_ids.data(); //判斷是否在ReadView中,若是在說明在建立ReadView時 此條記錄還處於活躍狀態則不該該查詢到,不然說明建立ReadView是此條記錄已是不活躍狀態則能夠查詢到 return(!std::binary_search(p, p + m_ids.size(), id)); }
總結一下可見性判斷邏輯:
trx_id_current
,而後從步驟1從新開始判斷,這樣總能最後找到一個可用的記錄。咱們知道,RC隔離級別是能看到其餘事務提交後的修改記錄的,也就是不可重複讀,可是RR隔離級別完美的避免了,可是它們都是使用的MVCC機制,那又爲什麼有兩種大相徑庭的結果呢?其實咱們看一下他們建立ReadView的區別就知道了。
上面的總結英文版爲:With REPEATABLE READ isolation level, the snapshot is based on the time when the first read operation is performed. With READ COMMITTEDisolation level, the snapshot is reset to the time of each consistent read operation.
來源自MySQL官網:MySQL Glossary-glos_consistent_read
由於RC每次查詢語句都建立一個新的ReadView,因此活躍的事務列表一直在變,也就致使若是事務B update提交了後事務A才進行查詢,查詢的結果就是最新的行,也就是不可重複讀咯。而RR則一直用的事務開始時建立的ReadView。
還記得開頭提到的問題嗎?如今應該可以所有解決了。
其實這個在上面Undo log中已經說起了。在事務裏任何對數據的修改都會寫一個Undo log,而後進行數據的修改,若是出現錯誤或者用戶須要回滾的時候能夠利用Undo log的備份數據恢復到事務開始以前的狀態。
這個在上面Redo log中已經說起了。在一個事務中的每一次SQL操做以後都會寫入一個redo log到buffer中,在最後COMMIT的時候,必須先將該事務的全部日誌寫入到redo log file進行持久化(這裏的寫入是順序寫的),待事務的COMMIT操做完成纔算完成。即便COMMIT後數據庫有任何的問題,在下次重啓後依然可以經過redo log的checkpoint進行恢復。也就是上面提到的crash recovery。
在事務處理的ACID屬性中,一致性是最基本的屬性,其它的三個屬性都爲了保證一致性而存在的。
首先回顧一下一致性的定義。所謂一致性,指的是數據處於一種有意義的狀態,這種狀態是語義上的而不是語法上的。最多見的例子是轉賬。例如從賬戶A轉一筆錢到賬戶B上,若是賬戶A上的錢減小了,而賬戶B上的錢卻沒有增長,那麼咱們認爲此時數據處於不一致的狀態。
在數據庫實現的場景中,一致性能夠分爲數據庫外部的一致性和數據庫內部的一致性。前者由外部應用的編碼來保證,即某個應用在執行轉賬的數據庫操做時,必須在同一個事務內部調用對賬戶A和賬戶B的操做。若是在這個層次出現錯誤,這不是數據庫自己可以解決的,也不屬於咱們須要討論的範圍。後者由數據庫來保證,即在同一個事務內部的一組操做必須所有執行成功(或者所有失敗)。這就是事務處理的原子性。(上面說過了是用Undo log來保證的)
可是,原子性並不能徹底保證一致性。在多個事務並行進行的狀況下,即便保證了每個事務的原子性,仍然可能致使數據不一致的結果,好比丟失更新問題。
爲了保證併發狀況下的一致性,引入了隔離性,即保證每個事務可以看到的數據老是一致的,就好象其它併發事務並不存在同樣。用術語來講,就是多個事務併發執行後的狀態,和它們串行執行後的狀態是等價的。
RU級別的操做其實就是對事務內的每一條更新語句對應的行記錄加上讀寫鎖來操做,而不把一個事務當成一個總體來加鎖,因此會致使髒讀。可是RC和RR可以經過MVCC來保證記錄只有在最後COMMIT後纔會讓別的事務看到。
這個在上面的MVCC的最後說到了,在RC事務隔離級別下,每次語句執行都關閉ReadView,而後從新建立一份ReadView。而在RR下,事務開始後第一個讀操做建立ReadView,一直到事務結束關閉。
這個是由於RR隔離級別使用了Next-key Lock這麼個東東,也就是Gap Lock+Record Lock的方式來進行間隙鎖定,具體原理本章不深刻討論,能夠參考個人另外一篇文章。