這步中,查詢管理器正在執行查詢並須要從表和索引中獲取數據。它這會要求數據管理器給它數據,但這有兩個問題:html
在這部分,咱們會看到關係數據庫是如何解決這兩個問題。我不會談及數據管理器獲取數據的方式,由於這不過重要(這篇文章已經夠長了)
mysql
就像我以前說的,數據庫的主要瓶頸是磁盤 I/O。爲了提升性能,現代數據庫使用緩存管理器。算法
查詢管理器不會直接從系統中拿數據,而是去緩存管理器請求數據。緩存管理器有個叫緩衝池(buffer pool)的內存緩存。從內存中獲取數據會大大加快數據庫的速度 。但這很難給出一個具體的數量級,由於這取決於你須要的是哪一種操做:sql
數據庫用的是什麼磁盤數據庫
但我仍是要說內存比磁盤快100到100k倍。 這又致使另外一個問題的出現(數據庫老是這樣。。。),緩存管理器須要在查詢執行器使用數以前,從內存中獲取數據;因此查詢管理器須要等待數據從慢磁盤中獲取緩存
這個問題叫預讀取。查詢管理器知道將會須要數據了,由於它知道查詢的完整流程和磁盤上的數據的統計信息。構思以下:服務器
緩存管理器會在緩衝區中存儲全部數據。爲了知道數據是否仍然須要,緩存管理器會爲數據添加了一個緩存日期(叫閂latch) 有時查詢執行器不知道須要什麼數據,有時候數據庫也不提供功能。相反他們會用推測預讀(例如:若是查詢執行器要數據 1,3,5,它可能在不久的未來會要數據 7,9,11) 又或者一個順序的預讀取(在這種狀況下,緩存管理器在一次請求後,簡單地加載下一個連續的數據)網絡
注意:緩存命中率不高並不老是意味着緩存不正常。有關更多信息,請閱讀 Oracle文檔併發
但,緩衝是的內存是有限的。所以,它須要將一些數據移走並加載新的數據。加載和清理緩存須要一點磁盤和網絡的I/O 成本。若是你有一個查詢要常常執行,使用這查詢的時候老是要加載數據清理數據,這也未免太沒效率的。爲了解決這個問題,現代數據庫使用一種緩衝區替換策略mvc
LRU是指(Least Recently Used)最近用得最少的。這個算法背後的構想是在緩存中保留最近使用,這些數據更有可能會再次使用 下面是個直觀的例子
爲了便於理解,我會設計這些在緩衝區的數據沒有被閂(latch)鎖住(因此能被移除)。在這個簡單的例子中,這個緩衝區能夠存儲3個元素
1) 緩衝管理器用了數據1,而後把數據放到一個空的緩衝區
2) 緩衝管理器用了數據4,而後把數據放半滿載緩衝區
3) 緩衝管理器用了數據3,而後把數據放到半滿載緩衝
4) 緩衝管理器用了數據9,緩衝區已滿,* 因而將數據1移除,由於它是最先使用的數據* 。而後把數據9加入到緩衝區 5) 緩衝管理器用了數據4,而數據4以前已經在緩衝區存在了,全部數據3成了緩衝區最先使用的數據
6) 緩衝管理器用了數據1,緩衝區已滿,因而數據3被清除由於它是最先使用的數據,數據1 被添加到緩衝區中。 這算法能很好地工做,但也存在一些侷限性。若是在一個大表中進行全局搜索呢?換句話說,若是表/索引的大小比緩衝區還大會發生什麼事呢?使用這算法會把以前在緩衝中的值所有移走,可是全局掃描可能只會使用一次
爲了防止上述的狀況,某些數據庫會添加特定的規則。若是根據Oracle 文檔所言
對於很大的表,數據庫會直接用路徑讀,這直接加載塊... 以免填滿緩衝區緩存。對於中等大小的表,數據庫可使用直接讀取或者是讀緩存。若是它決定要讀緩存,數據庫會把這塊放到 LRU列表的最後,來防止掃描有效地清除緩存區緩存
也有不少其餘的辦法像是一個LRU的高級版本叫 LRU-K。像 SQL Server 就用了 LRU-k 而 K = 2 這算法背後的思想是要考慮更多的歷史。使用簡單的 LRU(K=1時的LRU-K),算法只用考慮上次使用數據的時間。而LRU-K:
而計算權重的成本是很大的,這就是 SQL Server 只用到 K = 2。這個值在可接受的成本範圍內性能不錯。 關於 LRU-K 的更深刻的學習,你能夠閱讀最原始的研究論文(1993):《用於數據庫磁盤緩衝與的LRU-K頁替換算法》
固然啦,還有不少其餘的用於管理緩存的算法,像是:
有些數據庫可能容許使用其餘的算法而不是默認算法
我只講過在去緩衝區要在使用前先加載。但在數據庫中,有寫緩衝區的操做,這用來存儲數據,把數據串聯起來刷新磁盤數據。而不是逐個逐個地寫數據,產生不少的單次磁盤訪問。
請記住,buffer 存儲的是頁(page,數據的最小單元)而不是 row(邏輯上/人性化觀察數據的)。一個頁在緩衝池被修改但沒有寫入到磁盤是骯髒的。有不少算法能決定髒頁寫入磁盤的最佳時間,它和事務概念關係很密切,那是下一部分的內容。
最後,但也很重要,這部分會講事務管理器。咱們將看到進程是如何確保每一個查詢都在本身的事務中執行。在此以前,咱們須要明白事務的
一個ACID事務是一個工做單元,它要保證4個屬性:
在同一事務期間,您能夠運行多個SQL查詢來讀取,建立,更新和刪除數據。當兩個事務使用相同的數據時,開始混亂了。典型的例子是從帳戶A到帳戶B的匯款。想象一下,您有2筆事務:
若是咱們回到ACID屬性:
[若是你願意,能夠跳到下一部分,我要說的對於文章的其他部分並不重要]
許多現代數據庫不使用純隔離做爲默認行爲,由於它帶來了巨大的性能開銷。 SQL規範定義了4個級別的隔離:
例如,若是事務A執行 「TABLE_X中的SELECT count(1)」,而後由事務B在TABLE_X中添加並提交新數據,若是事務A再次執行count(1),則該值將不是相同。 這稱爲幽靈讀取(phantom read)
多數數據庫添加了本身的自定義的隔離級別(好比 PostgreSQL、Oracle、SQL Server的使用快照隔離),並且並無實現SQL規範裏的全部級別(尤爲是讀取未提交級別)。
默認的隔離級別能夠由用戶/開發者在創建鏈接時覆蓋(只須要增長很簡單的一行代碼)。
確保隔離性,一致性和原子性的真正問題是對相同數據(添加,更新和刪除)的寫操做:
這種問題叫 併發控制
解決問題的最簡單的方式是每一個事務逐一運行(按順序)。但這根本就沒有伸縮性的,一個多進程/多核心的服務器上只有一個核,這太沒效率了
解決這個問題的方法是,每次建立或取消事務:
更正規地說,這是一個調度衝突的問題。更具體地講,這是個很是難的且CPU開銷大的優化的問題。企業級數據庫沒法負擔等待數小時,爲新的事務找尋最佳的調度。所以,他們用不太理想的方法,它會讓更多的時間花費在處理事務衝突上。
爲了解決這個問題,大部分數據庫使用 鎖 和/或 數據版本控制。因爲這是個大話題,我關注點會在鎖的部分,而後我會說一小點數據版本控制
這鎖背後的思想是:
這種叫排他鎖(exclusive lock) 但對事務只是要讀取數據,使用排他鎖就很昂貴了。由於它強制讓那些只想讀一些數據的事務去等待。 這就是爲何會有另一種鎖,共享鎖(share lock) 共享鎖是這樣的:
可是,若是數據在用排它鎖,而事務只須要讀數據,也不得不等到排他鎖結束才能用共享鎖鎖住數據
鎖管理器是提供和釋放鎖的進程。在內部,它用哈希表(key是被鎖的數據)存儲了鎖,而且知道每一個數據
可是使用鎖可能致使2個事務永遠等待數據的狀況:
在這圖中:
這叫作 死鎖 。 在死鎖中,鎖管理器選擇要取消(回滾)事務來刪除死鎖,這個決定也不太容易啊
但在作出這個選擇以前,須要檢查是否存在死鎖。 哈希表能夠當作是一張圖表(像前面的那張圖)。若是圖中有個循環就會出現死鎖。因爲檢查循環(由於全部鎖的圖標是至關的大)是成本是很昂貴的,因此一個更簡單的方法會被常用:使用 時間超時(timeout) 。 若是在給定超時範圍內未能鎖定,就說明事務進入了死鎖狀態。 鎖管理器也能夠在加鎖以前檢查該鎖會不會變成死鎖,但要完美作到這點成本也是很昂貴的。所以這些預檢常常設置一些基本規則。
確保純粹的隔離的 最簡單方式 是在事務開始的時候加鎖,在事務結束的時候釋放鎖。這意味着事務在開始前不得不等待它的全部鎖,而後爲事務持有鎖,當結束時釋放鎖。它能夠工做的,可是在等待全部鎖的時候回浪費不少的時間
一個更快的方法是 兩段鎖協議(由DB2和SQL Server使用),其中事務分爲兩個階段:
這兩條簡單規則背後的思想是:
這協議能很好地工做,除非是那個事務修改後的數據並釋放鎖後,事務被取消或者回滾了。你可能遇到一種狀況是,一個事務讀了另外一個事務修改後的值,而這個事務要被回滾的。要避免此問題,必須在事務結束時釋放全部獨佔鎖。
固然,真正的數據庫會用更復雜的系統,涉及更多類型的鎖(如意向鎖 intention lock )和更多粒度(行級鎖,頁級鎖,分區鎖,表鎖,表空間鎖)可是這個道理都是同樣的。 我只探討純粹基於鎖的方法,數據版本控制是解決這個問題的另外一個方法。 版本控制背後的思想是:
它提升了性能,由於:
一切都比鎖更好,除了兩個事務寫入相同的數據(由於總有一個被回滾)。只是,你的磁盤空間會被快速增大。
數據版本控制和鎖定是兩種不一樣的簡介:樂觀鎖定與悲觀鎖定。他們都有利有弊;它實際上取決於應用場景(更多讀取與更多寫入)。有關數據版本控制的演示文稿,我推薦這篇關於PostgreSQL如何實現多版本併發控制,是很是好的演示文稿。
某些數據庫(如DB2(直到DB2 9.7)和SQL Server(快照隔離除外))僅使用鎖。其餘像PostgreSQL,MySQL和Oracle使用涉及鎖和數據版本控制的混合方法。我不知道只使用數據版本控制的數據庫(若是您知道基於純數據版本的數據庫,請隨時告訴我)。
[2015年8月20日更新]讀者告訴我: Firebird和Interbase使用沒有鎖的版本控制。 版本控制對索引有一個有趣的影響:有時一個惟一索引包含重複項,索引能夠有比表有行更多的條目,等等。
若是你在不一樣的隔離級別上讀過那部分,你會發現增長隔離級別時,會增長鎖的數量,從而增長事務等待鎖定所浪費的時間。這就是大多數數據庫默認狀況下不使用最高隔離級別(Serializable)的緣由。
與往常同樣,您能夠本身檢查主數據庫的文檔(例如 MySQL,PostgreSQL或Oracle)。
咱們已經看到,爲了提升性能,數據庫將數據存儲在內存緩衝區中。可是若是服務器在提交事務時崩潰,那麼在崩潰期間你將丟失在內存中的數據,這會破壞事務的持久性。
你能夠在磁盤上寫入全部內容,但若是服務器崩潰,你最終會將數據可能只有部分寫入磁盤,這會破壞事務的原子性。
任何事務的修改都只有撤銷和已完成兩個狀態 要解決這個問題,
有兩種方法:
在涉及許多事務的大型數據庫上使用時,影子副本/頁面會產生巨大的磁盤開銷。 這就是現代數據庫使用事務日誌的緣由。事務日誌必須存儲在穩定的存儲中。我不會深刻研究存儲技術,但必須使用(至少)RAID磁盤來防止磁盤故障。
大多數數據庫(至少Oracle,SQL Server,[DB2,PostgreSQL,MySQL和 SQLite)使用Write-Ahead Logging協議(WAL)處理事務日誌。
WAL協議是一組3條規則:
1)數據庫的每次修改都會生成一條日誌記錄,而且 必須在將數據寫入磁盤以前將日誌記錄寫入事務日誌。
2)日誌記錄必須按順序寫入;日誌記錄A在日誌記錄B以前發生就必須在B以前寫入
3)提交事務時,必須在事務成功結束以前,在事務日誌中寫入提交順序。
這個工做由日誌管理器完成。一種簡單的方法是在緩存管理器和數據訪問管理器(在磁盤上寫入數據)之間,日誌管理器在將事務日誌寫入磁盤以前將每一個更新/刪除/建立/提交/回滾寫入事務日誌。容易,對嗎? 錯誤的答案!都講了這麼多了,你應該知道與數據庫相關的全部內容都受到「數據庫效應」的詛咒。認真地是,問題是找到一種在保持良好性能的同時編寫日誌的方法。若是事務日誌上的寫入速度太慢,則會下降全部內容的速度。
1992年,IBM研究人員「發明了」一種名爲ARIES的WAL加強版。ARIES或多或少地被大多數現代數據庫使用。邏輯可能不同,但ARIES背後的理念隨處可見。我給發明加了引號是由於,是由於根據麻省理工學院的這門課程,IBM的研究人員「只不過是編寫事務恢復的良好實踐」。自從我5歲時ARIES論文發表以來,我並不關心來自辛酸研究者的這個古老八卦。事實上,在咱們開始這個最後的技術部分以前,我只是把這些信息給你一個休息時間。 我已經閱讀了關於ARIES的大量研究論文,我發現它很是有趣!在這部分中,我將僅向你概述ARIES,但若是你須要真正的知識,我強烈建議您閱讀本文。 ARIES 表示的是恢復和利用語義隔離算法(Algorithms for Recovery and Isolation Exploiting Semantics)。 這項技術的目的是有兩個的:
1) 寫日誌時有良好的性能
2) 有快速可靠的恢復 數據庫必須回滾事務有多種緣由:
有時候(好比網絡出現故障),數據庫能夠恢復事務。 怎麼可能?要回答這個問題,咱們須要瞭解日誌記錄中的存儲的信息。
事務期間的每一個 *操做(添加/刪除/修改)都會生成一個日誌* 。該日誌記錄包括:
好比,若是操做是更新,UNDO將會回到元素更新前的值或狀態(物理UNDO),或者回到原來狀態的反向狀態(邏輯UNDO)
此外,磁盤上的每一個頁面(存儲數據,而不是日誌)具備修改數據的最後一個操做的日誌記錄(LSN)的id。
給出LSN的方式更復雜,由於它與日誌的存儲方式有關。但這個背後的思想仍然是同樣的。 ARIES僅使用邏輯UNDO,由於處理物理UNDO真是一團糟。
注意:據我所知,只有PostgreSQL沒有使用UNDO。它使用垃圾收集器守護程序來刪除舊版本的數據。這與PostgreSQL中數據版本控制的實現有關。
爲了更好地說明這點,這裏是查詢「UPDATE FROM PERSON SET AGE = 18;」生成的日誌記錄的可視化和簡化示例。假設此查詢在事務18中執行。
每一個日誌都有一個惟一的LSN。鏈接的日誌屬於同一事務。日誌按時間順序連接(連接列表的最後一個日誌是最後一個操做的日誌)。
爲避免日誌寫入成爲主要瓶頸,使用 日誌緩衝區 。
當查詢執行程序要求修改時:
1) 緩存管理器將修改存儲其緩衝區中
2) 日誌管理器將關聯的日誌存儲在其緩衝區中
3) 到了這一步,查詢執行器認爲操做完成了(所以能夠請求作另外一次修改);
4)而後(稍後)日誌管理器將日誌寫入事務日誌。什麼時候寫日誌的決定是由算法完成的。
5)而後(稍後)緩存管理器將修改寫入磁盤。什麼時候在磁盤上寫入數據是由算法完成的。 當事務被提交,這意味着對於事務中的每一個操做,步驟1,2,3,4,5也作完了。 在事務日誌中寫入很快,由於它只是「在事務日誌中的某處添加日誌」,而在磁盤上寫入數據則更復雜,由於它要 「以能快速讀取數據的方式寫入數據」。
出於性能緣由,步驟5可能在提交以後完成 ,由於在崩潰的狀況下,仍然可使用REDO日誌恢復事務。這稱爲NO-FORCE政策 。 數據庫能夠選擇FORCE策略(即必須在提交以前完成步驟5)以下降恢復期間的工做負載。 另外一個問題是選擇是否在磁盤上逐步寫入數據(STEAL策略),或者緩衝區管理器是否須要等到提交順序一次寫入全部內容(NO-STEAL)。STEAL和NO-STEAL之間的選擇取決於您的需求:使用UNDO日誌快速寫入長時間恢復或快速恢復? 如下是這些影響恢復策略摘要:
注意:我在多篇研究論文和課程中讀到了這個事實,但我沒有(明確地)在官方文件中找到它。
好的,咱們有很好的日誌,讓咱們使用它們! 假設新實習生讓數據庫崩潰了(規則1:永遠是實習生的錯誤)。你重啓數據庫並開始恢復進程 ARIES在3個關卡中讓崩潰恢復過來:
1) 分析關卡:恢復進程讀所有的事務日誌去建立奔潰期間發生的事情的時間線。它會肯定哪些事務要回滾(全部事務沒有提交的都會回滾)和哪些在奔潰期間的數據須要寫入到磁盤
2) redo關卡:這關從分析期間肯定一條日誌記錄開始,並使用 REDO 來將數據庫更新到崩潰以前的狀態。
在 REDO 階段,REDO日誌按時間順序處理(使用LSN)。 對於每一個日誌,恢復過程將讀取包含要修改數據的磁盤每頁上的 LSN 若是 LSN(磁盤的頁)>= LSN(日誌記錄),則代表數據在奔潰以前已經寫入磁盤了(值已經被日誌以後、奔潰以前的某個操做覆蓋)因此不用作什麼 若是LSN(磁盤的頁)< LSN(日誌記錄),那麼磁盤上的頁將被更新。 即便對於要回滾的事務,重作也會完成,由於它簡化了恢復過程(但我確信現代數據庫不會這樣作)。
3) undo關卡: 此過程將回滾崩潰時未完成的全部事務。回滾從每一個事務的最後日誌開始,並按照反時間順序處理UNDO日誌(使用日誌記錄的PrevLSN)。
在恢復期間,事務日誌必須留意中恢復過程的操做,以便寫入磁盤上的數據與事務日誌中寫入的數據同步。解決方案多是移除被 undone的事務日誌記錄,這是很困難的。相反,ARIES在事務日誌中寫入補償日誌,邏輯上刪除被取消的事務日誌記錄。 當事務被手動取消,或者被鎖管理器取消(爲了消除死鎖),或僅僅由於網絡故障而取消,那麼分析階段就不須要了。實際上,有關 REDO 和 UNDO 的信息在 2 個內存表中:
當新的事務產生時,這兩個表由緩存管理器和事務管理器更新。由於它們是在內存中,當數據庫崩潰時它們也被破壞掉了。 分析階段的任務就是在崩潰以後,用事務日誌中的信息重建上述的兩個表。爲了加快分析階段,ARIES提出了一個概念:檢查點(check point),就是不時地把事務表和髒頁表的內容,還有此時最後一條LSN寫入磁盤。那麼在分析階段當中,只須要分析這個LSN以後的日誌便可。