1. 程式人生 > >MySQL InnoDB中髒讀引發的select查詢原理思考

MySQL InnoDB中髒讀引發的select查詢原理思考

一、MySQL InnoDB事務隔離級別髒讀、可重複讀、幻讀

MySQL InnoDB事務的隔離級別有四級,預設是“可重複讀”(REPEATABLE READ)

·        1)未提交讀(READUNCOMMITTED)。另一個事務修改了資料,但尚未提交,而本事務中的SELECT會讀到這些未被提交的資料(髒讀)( 隔離級別最低,併發效能高 )。

·        2)提交讀(READCOMMITTED)。本事務讀取到的是最新的資料(其他事務提交後的)。問題是,在同一個事務裡,前後兩次相同的SELECT會讀到不同的結果(不重複讀)。會出現不可重複讀、幻讀問題(鎖定正在讀取的行)

·       3)可重複讀(REPEATABLEREAD)。在同一個事務裡,SELECT的結果是事務開始時時間點的狀態,因此,同樣的SELECT操作讀到的結果會是一致的。會出幻讀現象(鎖定所讀取的所有行)。

·        4)序列化(SERIALIZABLE)。讀操作會隱式獲取共享鎖,可以保證不同事務間的互斥(鎖表)。

併發操作帶來的資料庫不一致性可以分為四類:

         1)丟失或覆蓋更新。當兩個或多個事務選擇同一資料,並且基於最初選定的值更新該資料時,會發生丟失更新問題。每個事務都不知道其它事務的存在。最後的更新將重寫由其它事務所做的更新,這將導致資料丟失。上面預定飛機票的例子就屬於這種併發問題。事務1與事務2先後讀入同一資料A=16,事務1執行A-1,並將結果A=15寫回,事務2執行A-1,並將結果A=15寫回。事務2提交的結果覆蓋了事務1對資料庫的修改,從而使事務1對資料庫的修改丟失了。

·        2)髒讀。另一個事務修改了資料,但尚未提交,而本事務中的SELECT會讀到這些未被提交的資料。

·        3)不重複讀。解決了髒讀後,會遇到,同一個事務執行過程中,另外一個事務提交了新資料,因此本事務先後兩次讀到的資料結果會不一致。

·        4)幻讀。解決了不重複讀,保證了同一個事務裡,查詢的結果都是事務開始時的狀態(一致性)。但是,如果另一個事務同時提交了新資料,本事務再更新時,就會“驚奇的”發現了這些新資料,貌似之前讀到的資料是“鬼影”一樣的幻覺。

具體地:

    1) 髒讀 

      首先區分髒頁和髒資料

      髒頁是記憶體的緩衝池中已經修改的page,未及時flush到硬碟,但已經寫到redo log中。讀取和修改緩衝池的page很正常,可以提高效率,flush即可同步。髒資料是指事務對緩衝池中的行記錄record進行了修改,但是還沒提交!!!,如果這時讀取緩衝池中未提交的行資料就叫髒讀,違反了事務的隔離性。髒讀就是指,當事務1修改某一資料,並將其寫回磁碟,事務2讀取同一資料後,事務1由於某種原因被撤銷,這時事務1已修改過的資料恢復原值,事務2讀到的資料就與資料庫中的資料不一致,是不正確的資料,稱為髒讀。

例如,事務1將C值修改為200,事務2讀到C為200,而事務1由於某種原因撤銷,其修改作廢,C恢復原值100,這時事務2讀到的就是不正確的“髒“資料 

    2). 不可重複讀 

     是指在一個事務內,多次讀同一資料。在這個事務還沒有結束時,另外一個事務也訪問該同一資料,由於第二個事務的修改,並且第二個事務提交。那麼第一個事務兩次讀到的的資料可能是不一樣的。這樣就發生了在一個事務內兩次讀到的資料是不一樣的,因此稱為是不可重複讀。即事務1讀取某一資料後,事務2對其做了修改,當事務1再次讀資料時,得到的與第一次不同的值。 例如,事務1讀取B=100進行運算,事務2讀取同一資料B,對其進行修改後將B=200寫回資料庫。事務1為了對讀取值校對重讀B,B已為200,與第一次讀取值不一致。

    3). 幻讀 :

     當某個事務讀取某個範圍內的記錄時,另外一個事務又在該範圍內插入了新的記錄.當之前的事務再次讀取該範圍時,會產生幻行.(與不可重複讀的區別是,是由於新事務插入資料,導致的舊事務兩次查詢不一致,那為什麼要將不可重複讀和幻讀區分呢?這因為兩種併發問題所產生的原理不一樣,具體可以檢視另一篇博文 Innodb中的事務隔離級別實現原理。例如,事務1按一定條件從資料庫中讀取某些資料記錄後未提交查詢結果,事務2刪除了其中部分記錄,事務1再次按相同條件讀取資料時,發現某些記錄神祕地消失了;或者事務1按一定條件從資料庫中讀取某些資料記錄後未提交查詢結果,事務2插入了一些記錄,當事務1再次按相同條件讀取資料時,發現多了一些記錄。

MVCC是如何解決幻讀的:

      MySQL的大多數事務型儲存引擎實現的都不是簡單的行級鎖。基於提升併發效能的考慮,它們一般都同時實現了多版本併發控制(MVCC)。不僅是MySQL,包括Oracle、PostgreSQL等其他資料庫系統也都實現了MVCC,但各自的實現機制不盡相同,因為MVCC沒有一個統一的實現標準。

      可以認為MVCC是行級鎖的一個變種,但是它在很多情況下避免了加鎖操作,因此開銷更低。雖然實現機制有所不同,但大都實現了非阻塞的讀操作,寫操作也只鎖定必要的行。

      MVCC的實現,是通過儲存資料在某個時間點的快照來實現的。也就是說,不管需要執行多長時間,每個事務看到的資料都是一致的。根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的資料可能是不一樣的。如果之前沒有這方面的概念,這句話聽起來就有點迷惑。熟悉了以後會發現,這句話其實還是很容易理解的。

      前面說到不同儲存引擎的MVCC實現是不同的,典型的有樂觀(optimistic)併發控制控制和悲觀(pessimistic)併發控制。

下面我們通過InnoDB的簡化版行為來說明MVCC是如何工作的。

       InnoDB的MVCC,是通過在每行記錄後面儲存兩個隱藏的列來實現的。這兩個列,一個儲存了行的建立時間,一個儲存行的過期時間(或刪除時間)。當然儲存的並不是實際的時間值,而是系統版本號(systemversionnumber)。每開始一個新的事務,系統版本號都會自動遞增。事務開始時刻的系統版本號會作為事務的版本號,用來和查詢到的每行記錄的版本號進行比較。下面看一下在REPEATABLEREAD隔離級別下,MVCC具體是如何操作的。

SELECT    InnoDB會根據以下兩個條件檢查每行記錄:InnoDB只查詢版本早於當前事務版本的資料行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要麼是在事務開始前已經存在的,要麼是事務自身插入或者修改過的。行的刪除版本要麼未定義,要麼大於當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。只有符合上述兩個條件的記錄,才能返回作為查詢結果。

INSERT    InnoDB為新插入的每一行儲存當前系統版本號作為行版本號。

DELETE    InnoDB為刪除的每一行儲存當前系統版本號作為行刪除標識。

UPDATE   InnoDB為插入一行新記錄,儲存當前系統版本號作為行版本號,同時儲存當前系統版本號到原來的行作為行刪除標識。儲存這兩個額外系統版本號,使大多數讀操作都可以不用加鎖。這樣設計使得讀資料操作很簡單,效能很好,並且也能保證只會讀取到符合標準的行。不足之處是每行記錄都需要額外的儲存空間,需要做更多的行檢查工作,以及一些額外的維護工作。MVCC只在REPEATABLEREAD和READCOMMITTED兩個隔離級別下工作。其他兩個隔離級別都和MVCC不相容(4),因為READUNCOMMITTED總是讀取最新的資料行,而不是符合當前事務版本的資料行。而SERIALIZABLE則會對所有讀取的行都加鎖。

二、select查詢原理

第一步:應用程式把查詢SQL語句發給伺服器端執行

我們在資料層執行SQL語句時,應用程式會連線到相應的資料庫伺服器,把SQL語句傳送給伺服器處理。

第二步:伺服器解析請求的SQL語句

1.SQL計劃快取,經常用查詢分析器的朋友大概都知道這樣一個事實,往往一個查詢語句在第一次執行的時候需要執行特別長的時間,但是如果你馬上或者在一定時間內運行同樣的語句,會在很短的時間內返回查詢結果。

原因:

  • 伺服器在接收到查詢請求後,並不會馬上去資料庫查詢,而是在資料庫中的計劃快取中找是否有相對應的執行計劃,如果存在,就直接呼叫已經編譯好的執行計劃,節省了執行計劃的編譯時間。
  • 如果所查詢的行已經存在於資料緩衝儲存區中,就不用查詢物理檔案了,而是從快取中取資料,這樣從記憶體中取資料就會比從硬碟上讀取資料快很多,提高了查詢效率.資料緩衝儲存區會在後面提到,這就是髒讀的原因,沒有控制快取和物理儲存介質之間的資料一致性,其實mysql使用innoDB儲存引擎的話是不存在髒讀的問題,因為innoDB預設的事務隔離策略是readCommited

注:如果使用計劃快取必須是在同一次資料庫連線中

2.如果在SQL計劃快取中沒有對應的執行計劃,伺服器首先會對使用者請求的SQL語句進行語法效驗,如果有語法錯誤,伺服器會結束查詢操作,並用返回相應的錯誤資訊給呼叫它的應用程式。

注意:此時返回的錯誤資訊中,只會包含基本的語法錯誤資訊,例如select寫成selec等,錯誤資訊中如果包含一列表中本沒有的列,此時伺服器是不會檢查出來的,因為只是語法驗證,語義是否正確放在下一步進行。

3.語法符合後,就開始驗證它的語義是否正確,例如,表名,列名,儲存過程等等資料庫物件是否真正存在,如果發現有不存在的,就會報錯給應用程式,同時結束查詢。

4.接下來就是獲得物件的解析鎖,我們在查詢一個表時,首先伺服器會對這個物件加鎖,這是為了保證資料的統一性,如果不加鎖,此時有資料插入,但因為沒有加鎖的原因,查詢已經將這條記錄讀入,而有的插入會因為事務的失敗會回滾,就會形成髒讀的現象。

5.接下來就是對資料庫使用者許可權的驗證,SQL語句語法,語義都正確,此時並不一定能夠得到查詢結果,如果資料庫使用者沒有相應的訪問許可權,伺服器會報出許可權不足的錯誤給應用程式,在稍大的專案中,往往一個專案裡面會包含好幾個資料庫連線串,這些資料庫使用者具有不同的許可權,有的是隻讀許可權,有的是隻寫許可權,有的是可讀可寫,根據不同的操作選取不同的使用者來執行,稍微不注意,無論你的SQL語句寫的多麼完善,完美無缺都沒用。

6.解析的最後一步,就是確定最終的執行計劃。當語法,語義,許可權都驗證後,伺服器並不會馬上給你返回結果,而是會針對你的SQL進行優化,選擇不同的查詢演算法以最高效的形式返回給應用程式。例如在做表聯合查詢時,伺服器會根據開銷成本來最終決定採用hashjoin,mergejoin,還是loopjoin,採用哪一個索引會更高效等等,不過它的自動化優化是有限的,要想寫出高效的查詢SQL還是要優化自己的SQL查詢語句。

當確定好執行計劃後,就會把這個執行計劃儲存到SQL計劃快取中,下次在有相同的執行請求時,就直接從計劃快取中取,避免重新編譯執行計劃。

第三步:語句執行

伺服器對SQL語句解析完成後,伺服器才會知道這條語句到底代表了什麼意思,接下來才會真正的執行SQL語句。

這時分兩種情況:

  • 如果查詢語句所包含的資料行已經讀取到資料緩衝儲存區的話,伺服器會直接從資料緩衝儲存區中讀取資料返回給應用程式,避免了從物理檔案中讀取,提高查詢速度。
  • 如果資料行沒有在資料緩衝儲存區中,則會從物理檔案中讀取記錄返回給應用程式,同時把資料行寫入資料緩衝儲存區中,供下次使用。

說明:SQL快取分好幾種,這裡有興趣的朋友可以去搜索一下,有時因為快取的存在,使得我們很難馬上看出優化的結果,因為第二次執行因為有快取的存在,會特別快速,所以一般都是先消除快取,然後比較優化前後的效能表現,這裡有幾個常用的方法:

DBCCDROPCLEANBUFFERS

從緩衝池中刪除所有清除緩衝區。

DBCCFREEPROCCACHE

從過程快取中刪除所有元素。

DBCCFREESYSTEMCACHE

從所有快取中釋放所有未使用的快取條目。SQLServer2005資料庫引擎會事先在後臺清理未使用的快取條目,以使記憶體可用於當前條目。但是,可以使用此命令從所有快取中手動刪除未使用的條目。

這隻能基本消除SQL快取的影響,目前好像沒有完全消除快取的方案,如果大家有,請指教。

結論:只有知道了服務執行應用程式提交的SQL的操作流程才能很好的除錯我們的應用程式。

  1. 確保SQL語法正確;
  2. 確保SQL語義上的正確性,即物件是否存在;
  3. 資料庫使用者是否具有相應的訪問許可權。

支援事務的的資料庫系統都需要有一套機制來保證事務更新的一致性和永續性,innoDB與Oracle等支援事務的關係資料庫一樣,也是採用redo log機制來保證事務更新的一致性和永續性的。

三、MySQL中有六種日誌檔案

重做日誌(redo log) 回滾日誌(undo log) 二進位制日誌(binlog) 錯誤日誌(errorlog) 慢查詢日誌(slow query log) 一般查詢日誌(general log) 中繼日誌(relay log)。

其中重做日誌和回滾日誌與事務操作息息相關,二進位制日誌也與事務操作有一定的關係,這三種日誌,對理解MySQL中的事務操作有著重要的意義。

這裡簡單總結一下這三者具有一定相關性的日誌。

一、重做日誌(redo log)

作用:

確保事務的永續性。防止在發生故障的時間點,尚有髒頁未寫入磁碟,在重啟mysql服務的時候,根據redo log進行重做,從而達到事務的永續性這一特性。

內容:

物理格式的日誌,記錄的是物理資料頁面的修改的資訊,其redo log是順序寫入redo log file的物理檔案中去的。

什麼時候產生:

事務開始之後就產生redo log,redo log的落盤並不是隨著事務的提交才寫入的,而是在事務的執行過程中,便開始寫入redo log檔案中。對於寫入redo log檔案的操作不是直接寫,而是先寫入一個redo log buffer中,然後按照一定的條件寫入日誌檔案。

dirtypage既然是在Bufferpool中,那麼如果系統突然斷電Dirtypage中的資料修改是否會丟失?這個擔心是很有必要的,例如如果一個使用者完成一個操作(資料庫完成了一個事務,page已經在bufferpool中修改,但dirtypage尚未flush),這時系統斷電,bufferpool資料全部消失。那麼,這個使用者完成的操作(導致的資料庫修改)是否會丟失呢?

答案是不會,innodb_flush_log_at_trx_commit=0時,主執行緒每秒會將redo log buffer寫入磁碟的redo log檔案中,無論事務是否已經提交。觸發寫入過程是由innodb_flush_log_at_trx_commit控制,表示在commit操作時,處理redo log的方式。引數innodb_flush_log_at_trx_commit可設定的值有0、1、2。

0代表當前事務提交時,並不會立即觸發將事務的redo log寫入磁碟上的日誌檔案,而是等待主執行緒每秒觸發一次快取日誌回寫磁碟操作,並呼叫作業系統fsync重新整理io快取。

1代表當前事務提交時,會立即觸發將事務的redo log寫入磁碟上的日誌檔案,並呼叫作業系統fsync重新整理io快取。

2代表當前事務提交時,會立即觸發將事務的redo log寫入磁碟上的日誌檔案,但不會立即呼叫作業系統fsync重新整理io快取,而是每秒只做一次磁碟io快取重新整理操作。

什麼時候釋放:

當對應事務的髒頁寫入到磁碟之後,redo log的使命也就完成了,重做日誌佔用的空間就可以重用(被覆蓋)。

對應的物理檔案:

預設情況下,對應的物理檔案位於資料庫的data目錄下的ib_logfile1&ib_logfile2

innodb_log_group_home_dir 指定日誌檔案組所在的路徑,預設./ ,表示在資料庫的資料目錄下。

innodb_log_files_in_group 指定重做日誌檔案組中檔案的數量,預設2

關於檔案的大小和數量,由以下兩個引數配置:

innodb_log_file_size 重做日誌檔案的大小。

innodb_mirrored_log_groups 指定了日誌映象檔案組的數量,預設1

其他:

很重要一點,redo log是什麼時候寫盤的?前面說了是在事物開始之後逐步寫盤的。

之所以說重做日誌是在事務開始之後逐步寫入重做日誌檔案,而不一定是事務提交才寫入重做日誌快取,原因就是,重做日誌有一個快取區Innodb_log_buffer,Innodb_log_buffer的預設大小為8M(這裡設定的16M),Innodb儲存引擎先將重做日誌寫入innodb_log_buffer中。

這裡寫圖片描述

然後會通過以下三種方式將innodb日誌緩衝區的日誌重新整理到磁碟

Master Thread 每秒一次執行重新整理Innodb_log_buffer到重做日誌檔案。

每個事務提交時會將重做日誌重新整理到重做日誌檔案。

當重做日誌快取可用空間 少於一半時,重做日誌快取被重新整理到重做日誌檔案

由此可以看出,重做日誌通過不止一種方式寫入到磁碟,尤其是對於第一種方式,Innodb_log_buffer到重做日誌檔案是Master Thread執行緒的定時任務。

因此重做日誌的寫盤,並不一定是隨著事務的提交才寫入重做日誌檔案的,而是隨著事務的開始,逐步開始的。

另外引用《MySQL技術內幕 Innodb 儲存引擎》(page37)上的原話:

即使某個事務還沒有提交,Innodb儲存引擎仍然每秒會將重做日誌快取重新整理到重做日誌檔案。

這一點是必須要知道的,因為這可以很好地解釋再大的事務的提交(commit)的時間也是很短暫的。

當更新資料時,InnoDB內部的操作流程大致是:

1、將資料讀入innoDB buffer pool,並對相關記錄加獨佔鎖;

2、將undo資訊寫入undo表空間的回滾段中;

3、更改快取頁中的資料,並將更新記錄寫入redo buffer中;

4、提交commit時,根據innodb_flush_log_at_trx_commit的設定,用不同的方式將redo buffer中的更新記錄重新整理到innoDB redo log file中,然後釋放獨佔鎖;

5、最後,後臺io執行緒根據需要擇機將快取中更新過的資料重新整理到磁碟檔案中

二、回滾日誌(undo log)

作用:

儲存了事務發生之前的資料的一個版本,可以用於回滾,同時可以提供多版本併發控制下的讀(MVCC),也即非鎖定讀

內容:

邏輯格式的日誌,在執行undo的時候,僅僅是將資料從邏輯上恢復至事務之前的狀態,而不是從物理頁面上操作實現的,這一點是不同於redo log的。

什麼時候產生:

事務開始之前,將當前是的版本生成undo log,undo 也會產生 redo 來保證undo log的可靠性

什麼時候釋放:

當事務提交之後,undo log並不能立馬被刪除,而是放入待清理的連結串列,由purge執行緒判斷是否由其他事務在使用undo段中表的上一個事務之前的版本資訊,決定是否可以清理undo log的日誌空間。

對應的物理檔案:

MySQL5.6之前,undo表空間位於共享表空間的回滾段中,共享表空間的預設的名稱是ibdata,位於資料檔案目錄中。

MySQL5.6之後,undo表空間可以配置成獨立的檔案,但是提前需要在配置檔案中配置,完成資料庫初始化後生效且不可改變undo log檔案的個數

如果初始化資料庫之前沒有進行相關配置,那麼就無法配置成獨立的表空間了。

關於MySQL5.7之後的獨立undo 表空間配置引數如下:

innodb_undo_directory = /data/undospace/ –undo獨立表空間的存放目錄 innodb_undo_logs = 128 –回滾段為128KB innodb_undo_tablespaces = 4 –指定有4個undo log檔案

如果undo使用的共享表空間,這個共享表空間中又不僅僅是儲存了undo的資訊,共享表空間的預設為與MySQL的資料目錄下面,其屬性由引數innodb_data_file_path配置。

這裡寫圖片描述

其他:

undo是在事務開始之前儲存的被修改資料的一個版本,產生undo日誌的時候,同樣會伴隨類似於保護事務持久化機制的redolog的產生。

預設情況下undo檔案是保持在共享表空間的,也即ibdatafile檔案中,當資料庫中發生一些大的事務性操作的時候,要生成大量的undo資訊,全部儲存在共享表空間中的。

因此共享表空間可能會變的很大,預設情況下,也就是undo 日誌使用共享表空間的時候,被“撐大”的共享表空間是不會也不能自動收縮的。

因此,mysql5.7之後的“獨立undo 表空間”的配置就顯得很有必要了。

三、二進位制日誌(binlog):

作用:

用於複製,在主從複製中,從庫利用主庫上的binlog進行重播,實現主從同步。

用於資料庫的基於時間點的還原。

內容:

邏輯格式的日誌,可以簡單認為就是執行過的事務中的sql語句。

但又不完全是sql語句這麼簡單,而是包括了執行的sql語句(增刪改)反向的資訊,也就意味著delete對應著delete本身和其反向的insert;update對應著update執行前後的版本的資訊;insert對應著delete和insert本身的資訊。

在使用mysqlbinlog解析binlog之後一些都會真相大白。

因此可以基於binlog做到類似於oracle的閃回功能,其實都是依賴於binlog中的日誌記錄。

什麼時候產生:

事務提交的時候,一次性將事務中的sql語句(一個事物可能對應多個sql語句)按照一定的格式記錄到binlog中。

這裡與redo log很明顯的差異就是redo log並不一定是在事務提交的時候重新整理到磁碟,redo log是在事務開始之後就開始逐步寫入磁碟。

因此對於事務的提交,即便是較大的事務,提交(commit)都是很快的,但是在開啟了bin_log的情況下,對於較大事務的提交,可能會變得比較慢一些。

這是因為binlog是在事務提交的時候一次性寫入的造成的,這些可以通過測試驗證。

什麼時候釋放:

binlog的預設是保持時間由引數expire_logs_days配置,也就是說對於非活動的日誌檔案,在生成時間超過expire_logs_days配置的天數之後,會被自動刪除。

這裡寫圖片描述

對應的物理檔案:

配置檔案的路徑為log_bin_basename,binlog日誌檔案按照指定大小,當日志文件達到指定的最大的大小之後,進行滾動更新,生成新的日誌檔案。

對於每個binlog日誌檔案,通過一個統一的index檔案來組織。

這裡寫圖片描述

其他:

二進位制日誌的作用之一是還原資料庫的,這與redo log很類似,很多人混淆過,但是兩者有本質的不同

作用不同:redo log是保證事務的永續性的,是事務層面的,binlog作為還原的功能,是資料庫層面的(當然也可以精確到事務層面的),雖然都有還原的意思,但是其保護資料的層次是不一樣的。

內容不同:redo log是物理日誌,是資料頁面的修改之後的物理記錄,binlog是邏輯日誌,可以簡單認為記錄的就是sql語句

另外,兩者日誌產生的時間,可以釋放的時間,在可釋放的情況下清理機制,都是完全不同的。

恢復資料時候的效率,基於物理日誌的redo log恢復資料的效率要高於語句邏輯日誌的binlog

關於事務提交時,redo log和binlog的寫入順序,為了保證主從複製時候的主從一致(當然也包括使用binlog進行基於時間點還原的情況),是要嚴格一致的,MySQL通過兩階段提交過程來完成事務的一致性的,也即redo log和binlog的一致性的,理論上是先寫redo log,再寫binlog,兩個日誌都提交成功(刷入磁碟),事務才算真正的完成。