前面有多篇文章介紹過MySQL InnoDB的相關知識,今天咱們要更深刻一些,看看它們的內部原理和機制是如何實現的。html
咱們知道,MySQl是一個存儲系統,數據最後都寫在磁盤上。咱們之前也提到過,磁盤的速度特別是大容量的磁盤受磁頭臂的影響,速度相對內存慢不少。因此Innodb實現了本身的緩存機制。前端
首先咱們先看下Innodb對內存是如何使用和劃分的,而後咱們再看看它是如何保存熱數據的。算法
預分配的內存池sql
Buffer Pool的最小單位緩存
空閒Page組成的鏈表安全
髒頁鏈表併發
維護內存Page和文件Page的映射關係運維
內存淘汰算法 post
以上三種鏈表LRU list、Free list、Flush list 和內存池、Page hash 以及磁盤文件之間的映射關係以下圖所示: 性能
LRU,Least Recent Used,最近最少使用。每次將剛使用過的頁面插到LRU隊列的最前端,那麼最少使用的排在尾端,當緩存不夠時,淘汰尾端的頁。
不少文件系統和開源庫的內存淘汰算法都用到了LRU,之前有很多文章都提到過。
可是LRU的缺陷是,有時會沒法淘汰真正的冷數據,尾端的數據可能暫時沒使用而已,不表明不使用頻繁,不表明不是熱數據。因此不少系統對LRU進行了優化。
好比Redis加了LFU(least frequently used最不常用)配合LRU一塊兒使用。
那麼InnoDB存儲引擎是如何改進的呢?以下圖,它將LRU分紅兩部分,中間的分割點叫作midpoint,新讀取的頁再也不是加入到最頭部,而是midpoint後面的位置,即後半截的頭部。
那麼midpoint的位置是如何計算的呢,在默認配置下,離LRU整個頭部的5/8處。固然這個比例是能夠根據實際業務進行設置的。但總之,能夠真正將冷熱數據分離,熱數據在前,冷數據在後。
那麼這兩個區的數據如何移動的呢,即冷熱數據如何切換的呢?
上面咱們提到了,剛插入的頁放在old區的頭部,那麼若是該頁確實訪問頻繁,不能一直呆在該位置吧。
InnoDB引入了參數innodb_old_blocks_time,若是old區的數據在該時間範圍內沒有被淘汰出去,就能夠移到new區,加入到new區的頭部。這也叫作made young。
而若是在old呆的時間不夠innodb_old_blocks_time,並且緩存不夠時,就會面臨直接淘汰,這就叫作made not young。這種狀況,能夠發生在全表掃描的時候,保證了new區的數據纔是真正的熱數據!
固然數據也有可能從new區移動到old區,只是相對比較簡單了,直接移動midpoint指向的位置便可。即new區的尾巴變成了old區的頭部。
事務特性
A(Atomicity原子性):所有成功或所有失敗
I(Isolation隔離性):並行事務之間互不干擾
D(Durability持久性):事務提交後,永久生效
C(Consistency一致性):經過AID保證
併發問題
髒讀(Drity Read):讀取到未提交的數據。中間全部變化的值均可能讀到。
不可重複讀(Non-repeatable read):兩次讀取結果不一樣。讀取已提交的(不同的值),讀到的值變化數量比髒讀要少。
幻讀(Phantom Read):select 操做獲得的結果所表徵的數據狀態影響(沒法支撐)後續的業務操做。
網上有人這樣區分,髒讀是讀取修改的數據,幻讀是讀取新提交的數據。我認爲也能夠,或許phantom表示虛幻的新數據(因此沒法支撐後續操做),而drity表明了修改的意思呢?
因此,不可重複讀重點在於update和delete,而幻讀的重點在於insert。
隔離級別
Read Uncommitted(讀取未提交內容):最低隔離級別,會讀取到其餘事務未提交的數據;存在髒讀的問題。
Read Committed(讀取提交內容):事務過程當中能夠讀取到其餘事務已提交的數據;存在不可重複讀的問題。
Repeatable Read(可重複讀):每次讀取相同結果集,無論其餘事務是否提交;存在幻讀的問題。
Serializable(串行化):事務排隊,隔離級別最高,性能最差。
從上咱們能夠看出事務有ACID四大特性,而「I」隔離性是經過鎖來實現的,咱們下一節講述。那麼其餘三個特性主要經過undo/redo日誌的機制來實現,這個知識點在前面有一篇文章中介紹和對比過。如今咱們站在事務實現的角度再來看看。
回滾日誌,顧名思義,是對事務rollback時使用。這是它核心的功能之一,可是它還有另外一個很是重要的功能,MVCC。因此今天這裏主要介紹它是如何在事務中發揮做用的。
MVCC
Multiversion concurrency control,多版本併發控制。當用戶讀取一行時,若是該記錄已經被其餘事務佔用,當前事務能夠經過undo讀取以前的行版本信息(快照數據),以此實現非鎖定讀。因此實現了非阻塞的讀操做,寫操做也只鎖定必要的行。即解決讀-寫衝突。
快照數據就是當前行數據的歷史版本,每行記錄可能含有多個版本。那該讀取哪一個版本呢?
首先,InnoDB的每行記錄或者說每條數據,除了記錄用戶定義的列以外,還有兩個隱藏的列:事務ID列DB_TRX_ID和回滾指針DB_ROLL_PTR。若是該表沒有定義主鍵,每行還會增長一個rowid列。DB_TRX_ID是當時執行這條sql的事務id,DB_ROLL_PTR指向的就是undo log中修改前的行DB_ROW_ID。因此對同一條數據的修改,經過roll_pointer就造成了undo log版本鏈。
而後咱們再來介紹下Read View快照讀。
通常狀況下讀取數據時會生成一個Read View,對當前該行的可能正在進行的事務進行一個快照。
Read View中主要包含4個比較重要的內容:
m_ids:表示在生成Read View時當前系統中活躍的讀寫事務的事務id列表,簡稱活躍列表。
min_trx_id:表示在生成Read View時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。
max_trx_id:表示生成Read View時系統中應該分配給下一個事務的id值,,也就是m_ids中的最大值。
creator_trx_id:表示生成該Read View的事務的事務id。
有了這些信息,這樣在訪問某條記錄時,只須要依次判斷undo log版本鏈中節點的事務ID是否可見,若是可見即找到了所須要的行記錄。
最後,READ COMMITTED和REPEATABLE READ兩種隔離級別對於快照數據生成的時機不同。
對於RC,在每次查詢語句執行的過程當中,都關閉Read View, 再建立當前的一份Read View。這樣就會產生不可重複讀現象。
對於RR,建立事務trx結構的時候,就生成了當前的global Read View,一直維持到事務結束。在事務結束這段時間內每一次查詢都不會從新重建Read View,從而實現了可重複讀。
undo log分爲兩種格式,處理不同。
insert undo log:用於回滾,提交即清理;不須要進行purge操做。
update undo log:用於回滾,同時實現快照讀,不能隨便刪除,因此須要等待purge線程來判斷什麼時候刪除。它記錄的是對delete和update的操做產生的undo log。
注:以上來自書上的說法,網上有人把第一種說成delete undo log,包括insert和delete操做,供參考。
還須要補充一點的是,update undo log怎樣去清理,應該是根據系統活躍的Read view中最小的活躍事務ID以前的便可清除。
redo日誌其實在《MySQL的undo/redo日誌和binlog日誌,以及2PC》文章中介紹比較多,也提到了XA事務的2PC。咱們這裏簡單介紹下普通事務的流程。
寫入流程仍然能夠分爲兩步,相似二階段提交:
記錄頁的修改,狀態爲prepare
事務提交,講事務記錄爲commit狀態
共享鎖(S)
讀鎖,能夠同時被多個事務獲取,阻止其餘事務對記錄的修改。
排他鎖(X)
寫鎖,只能被一個事務獲取,容許得到鎖的事務修改數據。
而讀其實又能夠分爲當前讀(鎖定讀)和快照讀(非鎖定讀),而快照讀經過上一節描述的MVCC來實現。
當前讀, 讀取的是最新版本,因此須要對讀取的記錄加鎖,阻塞其餘事務改動該記錄。當前讀又分爲兩種方式:
select...for update,對讀取的行加X鎖;
select...lock in share mode,對讀取的行加S鎖。
行級鎖
Record Lock,單個記錄上的鎖。
鎖直接加在索引記錄上面,鎖住的是key,因此必須是聚簇索引或者二級索引是惟一索引。
間隙鎖
Gap Lock,間隙鎖,鎖定一個範圍,但不包含記錄自己。
InnoDB存儲引擎的隔離級別默認是Repeatable Read,因此引入了間隙鎖解決可重複讀模式下的幻讀問題。
GAP鎖不是加在記錄上,鎖住的位置是兩條記錄之間的GAP;保證兩次當前讀返回一致的記錄。
因此兩次當前讀以前,其餘的事務不會插入新的知足條件的記錄。
咱們來整理下着二者的關係和區別。
Record Lock針對的是索引必須具有惟一性;而GAP鎖針對的是索引不具有惟一性但須要保證可重複讀,也就是說若是發現數據有被其餘事務修改的可能,那就把先後間隙都加上鎖。
好比說以下圖,有個用戶表,uid爲主鍵,那麼就只須要103這條記錄加上行鎖便可。
可是若是咱們變化下查詢條件(phone列上創建了二級索引),則除了對於這兩條記錄加鎖外,對先後的間隙也須要加鎖。固然這種狀況是針對RR的隔離級別,若是隔離級別是RC或者更低,安全性就沒有這麼高,系統會自動降級到行鎖。
Next-Key Lock,是Record Lock與Gap Lock的一個結合。理解了上述兩種鎖的原理,對於它而言就很容易了。
表級鎖
Table Lock,鎖定整張表。
主要用在運維的時候,對錶格進行操做好比MDL或者元數據的操做(meta data lock)等等。
固然有些狀況下會觸發鎖升級:全表掃描。全表掃描的觸發通常狀況下是當前被查詢的字段沒有創建任何索引。
而表級鎖事實上是對全部記錄和全部的間隙都加上鎖。
因此全表掃描的效率很是低,要儘可能避免。
以下圖,當咱們更新多條數據時,是一行一行的加鎖。
因此當同時出現對多條記錄交叉查詢時,很容易出現AB-BA死鎖,以下圖操做。
附錄
分庫分表的建議:
是否分表
建議單表不超過1KW
分表方式
取模:存儲均勻&訪問均勻
按時間:冷熱庫
分庫
按業務垂直分
水平查分多個庫
參考:
《MySQL技術內幕InnoDB存儲引擎》
內部培訓資料