MySQL innodb引擎的事務執行過程
通過這篇文章可以了解到下面幾個問題
問題1:
MySQL innodb引擎的update的流程;
問題2:以及寫redo,undo,binlog的順序,然后刷盤的順序又是什么呢?
問題3:以及刷新redo和臟數據的相關進程;
總結以上的三個問題,其實就是關于MySQL innodb事務的流程;那么接下來,我將詳細總結下一一一:MySQL innodb的事務流程:
1.接下來我就以update為例,講解下MySQL5.6的innodb的事務流程,總結起來就是:
鎮對update he set name='liuwenhe' where id=5;
1)事務開始
2)對id=5這條數據上排他鎖,并且給5兩邊的臨近范圍加gap鎖,防止別的事務insert新數據;
3)將需要修改的數據頁PIN到innodb_buffer_cache中;
4)記錄id=5的數據到undo log.
5)記錄修改id=5的信息到redo log.
6)修改id=5的name='liuwenhe'.
7)刷新innodb_buffer_cache中臟數據到底層磁盤,這個過程和commit無關;
8)commit,觸發page cleaner線程把redo從redo buffer cache中刷新到底層磁盤,并且刷新innodb_buffer_cache中臟數據到底層磁盤也會觸發對redo的刷新;
9)記錄binlog (記錄到binlog_buffer_cache中)
10)事務結束;
2.關于事務的四大特性ACID
事務的原子性(Atomicity):事務中的所有操作,要么全部完成,要么不做任何操作,不能只做部分操作。如果在執行的過程中發生了錯誤,要回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過。
事務的持久性(Durability):事務一旦完成,該事務對數據庫所做的所有修改都會持久的保存到數據庫中。為了保證持久性,數據庫系統會將修改后的數據完全的記錄到持久的存儲上。
事務的隔離性:多個事務并發訪問時,事務之間是隔離的,一個事務不應該影響其它事務運行效果
事務的一致性:一致性是指在事務開始之前和事務結束以后,數據庫的完整性約束沒有被破壞。這是說數據庫事務不能破壞關系數據的完整性以及業務邏輯上的一致性。
二:redo和undo保證MySQL innodb事務的原子性和持久性:
總起來概述可以認為:
undo用來保存數據更改之前的數據;保證原子性
redo用來保存數據更改之后的數據(注意是物理的修改信息),保證持久性
1)首先介紹Undo Log
Undo Log 主要是為了實現事務的原子性,在MySQL數據庫InnoDB存儲引擎中,還用Undo Log來實現多版本并發控制(簡稱:MVCC),之后的文章將會介紹mvcc;
Undo Log的原理很簡單,為了滿足事務的原子性,在操作任何數據之前,首先將數據備份到一個地方
也就是 Undo Log,然后進行數據的修改。如果出現了錯誤或者用戶執行了ROLLBACK語句,系統可以利用Undo Log中的備份將數據恢復到事務開始之前的狀態。
需要注意在MySQL 5.6之前,undo log是放在了共享表空間 ibdata1中的,MySQL5.6中開始支持把undo log分離到獨立的表空間,并放到單獨的文件目錄下;采用獨立undo表空間,再也不用擔心undo會把 ibdata1 文件搞大。
undo log是為回滾而用,具體內容就是copy事務前的數據庫內容(行)到innodb_buffer_pool中的undo buffer(或者叫undo page),在適合的時間把undo buffer中的內容刷新到磁盤。undo buffer與redo buffer一樣,也是環形緩沖,但當緩沖滿的時候,undo buffer中的內容也會被刷新到磁盤;并且innodb_purge_threads后臺線程會清空undo頁、清理“deleted”page,InnoDB將Undo Log看作數據,因此記錄Undo Log的操作也會記錄到redo log中。這樣undo log就可以象數據一樣緩存起來
2)接下來介紹 Redo Log,注意是先寫redo,然后才修改buffer cache中的頁,因為修改是以頁為單位的,所以先寫redo才能保證一個大事務commit的時候,redo已經刷新的差不多了。反過來說假如是先改buffer cache中的頁,然后再寫redo,就可能會有很多的redo需要寫,因為一個頁可能有很多數據行;而很多數據行產生的redo也可能比較多,那么commit的時候,就可能會有很多redo需要寫;
和Undo Log相反,Redo Log記錄的是新數據的備份。在事務提交前,只要將Redo Log持久化即可,
不需要將數據持久化。當系統崩潰時,雖然數據沒有持久化,但是Redo Log已經持久化。系統可以根據Redo Log的內容,將所有數據恢復到最新的狀態。需要注意的是,事務過程中,先把redo寫進redo log buffer中,然后MySQL后臺進程page cleaner thread適當的去刷新redo到低層磁盤永久保存;
因為刷新buffer pool的臟數據之前,必須要先刷新redo(從redo log buffer到磁盤),所以觸發刷新臟數據buffer pool的臟數據的條件也同時會觸發刷新redo。還需要注意:MySQL 5.6版本之前都是master thread來完成刷臟數據的任務(包括buffer pool中的臟數據以及redo log buffer中的redo),MySQL 5.6版本,刷新操作放入到了單獨的Page Cleaner Thread中;
Checkpoint(檢查點)技術目的是解決以下幾個問題:1、縮短數據庫的恢復時間;2、緩沖池不夠用時,將臟頁刷新到磁盤;3、重做日志不可用時,刷新臟頁。
在InnoDB存儲引擎內部,有兩種Checkpoint
分別為:Sharp Checkpoint、Fuzzy Checkpoint
Sharp Checkpoint發生在數據庫關閉時將所有的臟頁都刷新回磁盤,這是默認的工作方式,即參數innodb_fast_shutdown=1。但是若數據庫在運行時也使用Sharp Checkpoint,那么數據庫的可用性就會受到很大的影響。故在InnoDB存儲引擎內部使用Fuzzy Checkpoint進行頁的刷新,即只刷新一部分臟頁,而不是刷新所有的臟頁回磁盤。
Fuzzy Checkpoint:
1、Master Thread Checkpoint;
2、FLUSH_LRU_LIST Checkpoint;
3、Async/Sync Flush Checkpoint;
4、Dirty Page too much Checkpoint
1、Master Thread Checkpoint
以每秒或每十秒的速度從緩沖池的臟頁列表中刷新一定比例的頁回磁盤,這個過程是異步的,此時InnoDB存儲引擎可以進行其他的操作,用戶查詢線程不會阻塞。
2、FLUSH_LRU_LIST Checkpoint
因為InnoDB存儲引擎需要保證LRU列表中需要有差不多100個空閑頁可供使用。在InnoDB1.1.x版本之前,需要檢查LRU列表中是否有足夠的可用空間操作發生在用戶查詢線程中,顯然這會阻塞用戶的查詢操作。倘若沒有100個可用空閑頁,那么InnoDB存儲引擎會將LRU列表尾端的頁移除。如果這些頁中有臟頁,那么需要進行Checkpoint,而這些頁是來自LRU列表的,因此稱為FLUSH_LRU_LIST Checkpoint。
而從MySQL 5.6版本,也就是InnoDB1.2.x版本開始,這個檢查被放在了一個單獨的Page Cleaner線程中進行,并且用戶可以通過參數innodb_lru_scan_depth控制LRU列表中可用頁的數量,該值默認為1024,如:
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_lru_scan_depth';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_lru_scan_depth | 1024 |
+-----------------------+-------+
3、Async/Sync Flush Checkpoint
指的是重做日志文件不可用的情況,這時需要強制將一些頁刷新回磁盤,而此時臟頁是從臟頁列表中選取的。若將已經寫入到重做日志的LSN記為redo_lsn,將已經刷新回磁盤最新頁的LSN記為checkpoint_lsn,則可定義:
checkpoint_age(可以理解臟頁,或者待刷新的臟頁) = redo_lsn - checkpoint_lsn
再定義以下的變量:
async_water_mark = 75% * total_redo_log_file_size
sync_water_mark = 90% * total_redo_log_file_size
若每個重做日志文件的大小為1GB,并且定義了兩個重做日志文件,則重做日志文件的總大小為2GB。那么async_water_mark=1.5GB,sync_water_mark=1.8GB。則:
當checkpoint_age<async_water_mark時,不需要刷新任何臟頁到磁盤; </async_water_mark時,不需要刷新任何臟頁到磁盤;<>
當async_water_mark<checkpoint_age<sync_water_mark時觸發async flush,從flush列表中刷新足夠的臟頁回磁盤,使得刷新后滿足checkpoint_age<async_water_mark; </checkpoint_age
checkpoint_age>sync_water_mark這種情況一般很少發生,除非設置的重做日志文件太小,并且在進行類似LOAD DATA的BULK INSERT操作。此時觸發Sync Flush操作,從Flush列表中刷新足夠的臟頁回磁盤,使得刷新后滿足checkpoint_age<async_water_mark。 </async_water_mark。<>
可見,Async/Sync Flush Checkpoint是為了保證重做日志的循環使用的可用性。在InnoDB 1.2.x版本之前,Async Flush Checkpoint會阻塞發現問題的用戶查詢線程,而Sync Flush Checkpoint會阻塞所有的用戶查詢線程,并且等待臟頁刷新完成。從InnoDB 1.2.x版本開始——也就是MySQL 5.6版本,這部分的刷新操作同樣放入到了單獨的Page Cleaner Thread中,故不會阻塞用戶查詢線程。
解釋下為什么重做日志文件不可用時,這時需要強制將一些臟頁刷新回磁盤?
因為我們知道redo的作用是保證數據庫的一致性,當數據庫異常停機時,需要借助redo+undo進行實例恢復,redo前滾---恢復出buffer pool中的臟數據(包括已經commit還沒有刷新到磁盤的,也可能包括沒有commit,但是已經刷新到磁盤的,)然后借助undo完成回滾---將沒有commit,但是已經刷新到磁盤的數據,回滾到之前的狀態。那么為啥重做日志文件不可用時,這時需要強制將一些臟頁刷新回磁盤?原因就在于,redo 是循環覆寫的,當redo log 文件不可用,也就是說此時所有的redo 文件里面的redo都是實例恢復需要的,也就是不能被覆蓋的redo, 那么什么是實例恢復需要的redo呢?就是buffer pool中的的臟數據,還沒有刷新到磁盤,而這些臟數據相關的redo是不能被覆蓋的,這些redo就是實例恢復需要的redo,所以沒有可用的重做日志文件,需要強制將一些臟頁刷新回磁盤,這樣就會有一些redo是實例恢復不需要的了,也就可以被覆蓋了。
4、Dirty Page too much
即臟頁的數量太多,導致InnoDB存儲引擎強制進行Checkpoint。其目的總的來說還是為了保證緩沖池中有足夠可用的頁。其可由參數innodb_max_dirty_pages_pct控制:
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_max_dirty_pages_pct' ;
+----------------------------+-------+
| Variable_name | Value |
+----------------------------+-------+
| innodb_max_dirty_pages_pct | 75 |
+----------------------------+-------+
innodb_max_dirty_pages_pct值為75表示,當緩沖池中臟頁的數量占據75%時,強制進行Checkpoint,刷新一部分的臟頁到磁盤。在InnoDB 1.0.x版本之前,該參數默認值為90,之后的版本都為75,其可以通過參數innodb_max_dirty_pages_pct來設置;
總結下redo刷新的條件(因為刷新innodb_buffer_pool中的臟數據之前需要刷新redo,所以觸發刷新buffer_pool會同時觸發刷新redo):
1)當redo log buffer達到一定比值后,
2)刷新innodb_buffer_pool中的臟數據之前,
3)redo log buffer滿的時候,沒有可用buffer;
4)每秒刷新一次;
5)commit的時候;
6)數據庫關閉時發生harp Checkpoint,觸發將所有臟頁刷回磁盤
7)手工flush logs;
8)重做日志不可用時,觸發刷新innodb_buffer_pool中的臟數據,進而觸發redo刷新;
三:MySQL binlog: 主從同步 主庫binlog先寫入到 binlog_buffer中,然后刷新到磁層磁盤也就是binlog文件,主庫dump進程讀取的binlog文件,發送給從庫;
binlog日志是針對整個MySQL server而言的,前面介紹的redo和undo是針對innodb引擎而言的,binlog的存在就是方便那些不支持事務的引擎表來同步數據到slave;
那么到底是先刷新redo還是先寫binlog呢?
伴隨著這個問題,我重點說下,MySQL innodb 引擎事務commit的過程:
MySQL為了保證master和slave的數據一致性,就必須保證binlog和InnoDB redo日志的一致性,為此MySQL引入二階段提交(two phase commit or 2pc),MySQL通過兩階段提交(內部XA的兩階段提交)很好地解決了這一問題,兩階段提交關鍵在于保證redo刷盤之后才能刷新binlog到底層文件,以 binlog 的寫入與否作為事務提交成功與否的標志,最后判斷 binlog中是否有 redo里的xid,MySQL5.6以前,為了保證數據庫上層二進制日志的寫入順序和InnoDB層的事務提交順序一致,MySQL數據庫內部使用了prepare_commit_mutex鎖。但是持有這把鎖之后,會導致組提交失敗;直到MySQL5.6之后,才解決了這個問題,借助序列來保證binlog刷新也可以組提交;關于redo 和binlog組提交,請看下一篇文章,
事務崩潰恢復過程如下:
1.崩潰恢復時,掃描最后一個Binlog文件,提取其中的xid;
2.InnoDB維持了狀態為Prepare的事務鏈表(commit兩階段提交中的第一階段,為Prepare階段,會把事務設置為Prepare狀態)將這些事務的xid和Binlog中記錄的xid做比較,如果在Binlog中存在,則提交,否則回滾事務。
通過這種方式,可以讓InnoDB redo 和Binlog中的事務狀態保持一致。
四:簡單介紹下MySQL的后臺進程:
InnoDB存儲引擎是多線程模型,因此其后臺有多個不同的后臺線程,負責處理不同的任務:
1)Master Thread
Master Thread是一個非常核心的后臺線程,主要負責將緩沖池中的數據異步刷新到磁盤,保證數據的一致性。
2)IO Thread
InnoDB存儲引擎中大量使用了Async IO來處理寫IO請求,這樣可以極大提高數據庫的性能,而IO Thread的主要工作是負責這些IO請求的回調處理,可以使用show engine innodb status命令查看InnoDB存儲引擎中的IO進程:
mysql> show engine innodb status\g;
I/O thread 0 state: waiting for completed aio requests (insert buffer thread)
I/O thread 1 state: waiting for completed aio requests (log thread)
I/O thread 2 state: waiting for completed aio requests (read thread)
I/O thread 3 state: waiting for completed aio requests (read thread)
I/O thread 4 state: waiting for completed aio requests (read thread)
I/O thread 5 state: waiting for completed aio requests (read thread)
I/O thread 6 state: waiting for completed aio requests (read thread)
I/O thread 7 state: waiting for completed aio requests (read thread)
I/O thread 8 state: waiting for completed aio requests (write thread)
I/O thread 9 state: waiting for completed aio requests (write thread)
I/O thread 10 state: waiting for completed aio requests (write thread)
I/O thread 11 state: waiting for completed aio requests (write thread)
Pending normal aio reads: [0, 0, 0, 0] , aio writes: [0, 0, 0, 0] ,
ibuf aio reads:, log i/o’s:, sync i/o’s:
Pending flushes (fsync) log: 0; buffer pool: 0
451 OS file reads, 54 OS file writes, 7 OS fsyncs
3.77 reads/s, 16384 avg bytes/read, 1.05 writes/s, 0.13 fsyncs/s
如上顯示的是6個io read thread和4個io write thread,但是關于log的io thread 和insert buffer thread的io thread 只有一個;從MySQL 5.6開始默認是四個io read thread和4個io write thread,并且可以通過innodb_read_io_threads 和innodb_write_io_threads 參數進行設置:
mysql> show variables like '%io_threads%';
+-------------------------+-------+
| Variable_name | Value |
+-------------------------+-------+
| innodb_read_io_threads | 6 |
| innodb_write_io_threads | 4 |
+-------------------------+-------+
2 rows in set (0.00 sec)
3)Purge Thread
事務被提交后,其所使用的undo log可能不再需要,因此需要PurgeThread來回收已經使用并分配的undo頁。從InnoDB1.1版本開始,purge操作可以獨立到單獨的線程中進行,以此來減輕Master Thread的工作,從而提高CPU的使用率、提升存儲引擎的性能。可以通過在MySQL數據庫的配置文件中添加相關的命令來啟用獨立的Purge Thread,如下參數:
mysql> show variables like 'innodb_purge_threads';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| innodb_purge_threads | 1 |
+----------------------+-------+
1 row in set (0.00 sec)
Page Cleaner Thread
4)Page Cleaner Thread
是在InnoDB 1.2.x版本中引入的,其作用是將之前版本中的臟頁的刷新操作都放入到單獨的進程中來完成,目的就是為了減輕原Master Thread的工作及對于用戶查詢線程的阻塞,進一步提高InnoDB存儲引擎的性能。
然后回答最開始的問題:
在內存中先寫undo,然后寫redo,至于redo和binlog順序不確定, 刷盤是先刷undo,然后刷redo,最后刷新binlog;