Buffer Cache 是 Oracle 中最重要的內存池之一,本文將對其進行深刻講解。算法
若要更好地理解本文,須要對 Oracle 體系結構中的內存結構有基本的瞭解。若是還不知道Buffer Cache的做用、SGA各個部分的基礎知識,可先行閱讀文章《Oracle 體系結構詳解》。數據結構
在正式介紹 Buffer Cache 內容前,先介紹兩個名詞,按照 IT 界的慣常叫法,對於一個塊,在磁盤中叫 Block(塊);在內存中一般稱爲 Buffer 。本文將沿用這個習慣叫法。併發
在Buffer Cache中最重要、最難理解的部分就是它的各類鏈表,包括HASH鏈表、LRU鏈表、檢查點隊列鏈表等。所以本文將以鏈表爲主線,挖掘Buffer Cache的工做原理。測試
小問答:
假設進程要訪問 5 號數據文件中的第 1234 號塊,Oracle 如何知道這個塊是否在 BufferCache 中呢?若是在的話,它的具體地址是多少呢?
答:使用HASH算法。優化
1)HASH 鏈表與邏輯讀
在 Oracle 中,幾乎全部在內存中搜索數據的算法都採用 HASH 算法。ui
HASH 算法中有一個重要的概念:Bucket。Buffer Cache 中的 HASH Bucket 數量,由_db_block_hash_buckets 參數設置。.net
全部的 HASH Bucket 結構如圖 1 所示。
圖 1 HASH 表結構指針
當進程要讀取某一個數據塊時,好比,要讀取 5 號文件中的第 1234 號塊,它將根據文件號、塊號來計算 HASH 值。假設此處計算出的 HASH 值爲 X,那麼進程將根據此值,直接定位到 Bucket X ,如圖 2 所示。
對象
圖 2 HASH 表定位blog
定位到這個 Bucket 後,就能夠讀取它裏面的內容。它裏面的內容如圖 3 所示。
圖 3 HASH 表和 Cache Buffers Cache 鏈表
簡單地說,在每一個 Bucket 中,都只保存一個指向 Cache Buffers Cache 鏈表(簡稱 CBC 鏈表)的鏈表頭。
先要簡單說一下什麼是鏈表。
好比有 3 我的,張3、李四和王五,他們分別住在不一樣的地方。假設張三的住址是 「中山路123號」 ,李四的住址是 「延安路456號」 ,王五的住址是 「長安街789號」 。
找一張紙,寫上李四的住址 「延安路456號」 ,放在張三家中。這張紙,就是指針,它指向李四。這張紙放在張三家中,能夠說張三指向李四。同理,將王五的地址放在李四家中,即 「李四指向王五」 。
這樣一來,張三指向李4、李四指向王五。這就是單向鏈表。
在數據結構中,張3、李4、王五被統一稱爲鏈表的Node(節點),本文後面也會使用這個統一的稱呼:Node。
除單向鏈表外,固然還有雙向鏈表。張三指向李四,李四指向張三;李四指向王五,王五指向李四,這就是雙向鏈表。Oracle中全部的鏈表都是雙向鏈表。
張3、李4、王五一旦造成鏈表,之後再找這 3 我的就方便了,不須要記住他們 3 我的的地址,只需記住一我的的地址就能夠了。將張三的地址 「中山路123號」 ,寫到一張紙上,因爲張三是鏈表中開頭第一我的,所以這張紙就叫 「指向鏈表頭的指針」 。Oracle HASH 表的 Bucket 中存放的是什麼呢,就是 「指向鏈表頭的指針」 。
假設張3、李4、王五他們幾個都是記錄 Buffer Cache 中 Buffer 的位置、狀態等信息的,他們被稱爲 Buffer Header(簡稱爲BH)。
Buffer Cache 中每個 Buffer 都有一個 Buffer Header,可是碰巧張3、李4、王五他們的 HASH 值同樣(HASH衝突),所以,他們幾我的被組織成了鏈表,這個鏈表稱爲 CBC 鏈表。Buffer Cache 的 HASH 表的 Bucket中,存放的就是 CBC 鏈表的鏈表頭。
全部的 HASH 算法,都無可避免 HASH 衝突的問題。解決 HASH 衝突問題的辦法,就是在每一個 HASH Bucket 後面創建一個鏈表。
如今既然已經找到 Bucket X 了,下一步固然就是讀取它裏面存放的 CBC 鏈表頭,再接下來則是搜索鏈表了。進程將逐個比較每一個 BH 中記錄的文件號、塊號,直到找到須要的爲止。在圖 3 中, Bucket X 中第二個 BH 就是目標了。
接下來,再看一張圖,如圖 4 所示。
圖 4 邏輯讀過程
能夠看到,在 BH 中有一項信息很重要,即 BA(Buffer Address)。它是 5 號文件第 1234 號塊在 Buffer Cache 中的地址。進程根據這個地址,直接訪問 Buffer 就能夠了。
經過這幾張圖,將進程在 Buffer Cache 中搜索 Buffer 的過程描述以下。
這就是 Oracle 邏輯讀的過程。若是搜索 Bucket 後的BH鏈表,沒有找到包含目標文件號、塊號的 BH,那就證實 uffer Cache 中不包含目標塊,就只能物理讀了。
2)Cache Buffers Chain Latch與Buffer Pin鎖
SGA 中是公共內存,哪怕要訪問公共內存中的一個字節,都須要有某種鎖機制保護。Oracle 採用的鎖機制就是 Latch 和 Mutex 。
在以上邏輯讀的過程當中,搜索 Bucket 後的鏈表,還有訪問 BH 中的 BA,都須要 Latch 的保護。這個 Latch 就是 Cache Buffers Chain Latch(簡稱CBC Latch)。
爲了讓你們對 CBC Latch 有更形象的理解,這裏用一幅圖來表示,如圖 5 所示。
圖 5 CBC Latch示意圖
在圖 5 中,Oracle 在鏈表前加了一把鎖,若是想訪問鏈表,必須先申請得到這把鎖。這個鎖就是 CBC Latch 。
它不單保護對鏈表的訪問,當在鏈表中找到目標 BH 時,有時還要對 BH 進行修改,修改的目的是爲了加鎖,此處的鎖能夠稱爲 Buffer Pin 鎖。在修改完 BH 中 Buffer Pin 鎖的狀態後,CBC Latch 就能夠釋放了。
以後,進程將在 Buffer Pin 鎖的保護下訪問 Buffer 。
總結一下,得到 CBC Latch 後,進程要完成兩個工做:
Buffer Pin 鎖有多種模式,最多見的有共享(用字母S表示)、獨佔(用字母X表示)兩種模式。在沒有加鎖的時候,Buffer Pin 鎖的值將是 0 。
說明:Buffer Pin 鎖還有一種模式也較爲常見,就是爲 「當前讀」 加鎖,後文會有介紹。
若是隻是邏輯讀,進程會將 Buffer Pin 鎖的狀態設置爲 S 模式;若是是 DML 操做,要修改 Buffer,進程將把 Buffer Pin 鎖設置爲X模式。
Buffer Pin 鎖的詳情後面再描述,先回到 CBC Latch 上來。事實上,圖 5 並非十分準確,從該圖中很容易得出,每一個 HASH Bucket 都有一個專門的 CBC Latch 保護,但其實不是。事實上,一個 CBC Latch 要保護好幾個 Bucket ,如圖 6 所示。
圖 6 一個 CBC Latch 保護幾個 Bucket
在圖 6 中,Bucket X、Bucket Y、Bucket Z 三個 Bucket 都和同一個 CBC Latch 相關。也就是用同一個 CBC Latch 保護了 3 個 Bucket 。
但要注意的是,每一個 Bucket 後仍是有各自的鏈表(圖 6 未畫出 Bucket Y 和 Bucket Z 後面的鏈表 ),只是這些鏈表都用同一個 CBCLatch 保護而已 。
Oracle 這樣作是爲了節約內存。由於每一個 Latch 都會佔用一部份內存,若是每一個 Bucket 都對應一個 CBC Latch,那麼就會多佔用一些內存。
在圖 6 中,在 5 號文件 1234 號塊的 BH 中,添加了一行:Buffer Pin 。它其實只是 BH 中的幾個字節,但它同時也是一個鎖。只有將它成功地修改爲 S 或 X,才能進一步訪問 Buffer 。
再強調一下,開始訪問 Buffer 的時候,CBC Latch 就釋放了,Buffer 的訪問將在 Buffer Pin 鎖的保護下完成。
訪問完 Buffer 後,固然還要釋放 Buffer Pin 鎖,也就是說,還要修改 BH 中的 BufferPin 鎖。修改這個鎖時,一樣還須要 CBC Latch 的保護。事實上,CBC Latch 一共也就這麼兩個做用,即保護鏈表和保護 BH 。
CBC Latch 也有兩種持有模式:共享和獨佔。但要注意的是,不一樣於 Buffer Pin 鎖用讀、寫形式來決定鎖的模式,就算爲了 「讀」 而持有 CBC Latch,有時會是獨佔模式,而有時則會是共享模式。CBC Latch 的持有模式取決如下 4 個要素:
除有惟一索引外,在大多數狀況下,不管是讀仍是寫,訪問表塊都將以獨佔模式得到 CBC Latch 。另外,索引的根塊、枝塊只要不修改,都是以共享模式得到 CBC Latch 的。
注意,前面說過,CBC Latch 的目的有兩個:保護鏈表和保護 BH 。其實 Oracle 全部鎖的根本原理就是,只要不修改,都以共享模式得到。若是涉及修改,固然就要以獨佔模式得到了。
對於 Buffer,就算只是讀其中的數據,仍然要修改 BH 中 Buffer Pin 鎖的狀態。而修改 BH,天然就要以獨佔方式請求 CBC Latch 了。具體步驟以下:
步驟1:以獨佔方式得到 CBC Latch,如圖 7 所示。
圖 7 邏輯讀時獨佔CBC Latch
步驟2:在獨佔 CBC Latch 的保護下,修改 BH 中的 Buffer Pin 鎖,將鎖狀態改成 S(原來是0),如圖 8 所示。
圖 8 修改鎖狀態
步驟3:BH 中的 Buffer Pin 鎖狀態修改完畢,釋放獨佔的 CBC Latch,如圖 9 所示。
圖 9 釋放 CBC Latch
步驟4:在共享 Buffer Pin 鎖的保護下,到 BH 中的 BA 找到 Buffer 的地址,讀取 Buffer 中的數據,如圖 10 所示。
圖 10 讀取Buffer中的數據
這是通常邏輯讀的流程。
可是對於索引的根塊、枝塊等這些塊的查詢頻度,要遠高於葉塊和表塊。這點應該很容易理解,若是你的索引有 100 萬個塊,但根塊只有一個。不管訪問這 100 萬個塊中的哪個,都要先訪問根塊。能夠想象,根塊、枝塊的查詢頻度要比葉塊和表塊高得多。但根、枝塊的修改頻度,則又會遠低於葉塊、表塊。
在這種狀況下,若是每次查詢根塊、枝塊都以獨佔模式得到 CBC Latch ,再以共享模式獲得 Buffer Pin 鎖,而後查詢 Buffer 數據 。雖然這在 Buffer Pin 鎖相關步驟不會產生等待,但因爲 CBC Latch 是獨佔的,必然致使那裏產生激烈的競爭。
如何進行優化調整呢?Oracle 公司的開發人員仍是很聰明的,他們清楚,獨佔 CBCLatch 的目的,是爲了保護修改 BH 中 Buffer Pin 鎖狀態的過程。若是不去修改 Buffer Pin 鎖,那麼對於 BH ,就只剩下讀操做了。既然只有讀,也就不必以獨佔模式得到 CBC Latch 了。所以優化調整後的具體步驟以下。
步驟1:以共享方式得到CBC Latch(在前面方法中,是以獨佔方式得到Latch的),如圖 11 所示。
圖 11 共享 CBC Latch
步驟2:在共享 CBC Latch 保護下,搜索鏈表。查詢 BH 中的 BA ,如圖 12 所示。在此步驟中,不須要對 Buffer Pin 鎖的狀態進行修改。
圖 12 搜索鏈表
步驟3:在共享 CBC Latch 的保護下,根據 BH 中 BA 地址,查詢 Buffer中的 數據,如圖 13 所示。
圖 13 查詢 Buffer 中的數據
步驟4:查詢 Buffer 數據完畢,釋放 CBC Latch,如圖 14 所示。
圖 14 釋放 CBC Latch
看到和剛纔的區別沒?在此過程當中,共享 CBC Latch 一直持續到讀取 Buffer 數據結束,沒有修改 Buffer Pin 鎖的過程。
雖然 CBC Latch 加載的時間長了些,但因爲是共享模式,在大量讀取操做的環境中,能夠有效下降競爭。索引根塊、枝塊的絕大多數操做都是讀,除非發生了索引分裂。
除了普通索引的根塊、枝塊外,在有惟一索引、索引惟一掃描時,索引的根塊、枝塊,還有葉塊、表塊都將以共享 CBC Latch 的方式保護。
3)Cache Buffers Chain Latch 的競爭
在圖 6 中已經有描述,一個 CBC Latch 可保護多個鏈表,並且它除了保護鏈表的訪問之外,還要保護 BH 的讀和修改操做。而一個鏈表中,可能會有多個 BH,這樣算下來,一個 CBC Latch 除了操做多個鏈表之外,還要保護數目更多的 BH 。所以,當 CBC Latch 出現競爭時,多是以下兩種狀況:
在這兩種狀況中,第一種狀況被稱爲熱鏈競爭,第二種狀況被稱爲熱塊競爭。
熱鏈競爭最容易解決。多個進程其實訪問的是不一樣的BH,只不過剛好這些 BH 在同一 CBC Latch 保護下(這種巧合的狀況固然比較少見,但偶爾也會遇到),這時,解決方案很簡單,可對兩個隱藏參數中的一個進行修改,即 _db_block_hash_buckets 和 _db_block_hash_latches ,它們分別控制 HASHBucket 的數量和 CBC Latch 的數量。這樣一來,BH 和 HASH Bucket 的對應關係就會被從新計算。本來在同一鏈表中的 BH,從新計算後極可能就不在同一鏈表中了。
除了以上所說的熱鏈競爭外,第二種狀況就屬於「熱塊」競爭了。
熱塊競爭比較難解決,但好在這種競爭通常不會出現。之前索引的根塊、枝塊查詢頻度高,很容易致使熱塊競爭,但在 Oracle 9iR2 以後,Oracle 已經對這一部分進行了優化(前文已有過詳細討論),根塊、枝塊如今使用的都是共享模式的 CBCLatch 。於是,如今出現 CBC Latch 競爭的大多數狀況,都是 SQL 語句執行計劃不合理致使的,只須要調優 SQL 便可。但也有些狀況是 SQL 調優所不能解決的。好比,某個應用中有一個很小的表,只有十幾行數據,如此小的表沒有爲它創建索引,每次訪問都是全表掃描。後來當查詢壓力逐漸加大時,這個表的塊上就出現了大量的 CBC Latch 競爭。查看這個表的 SQL 查詢,發現有不少 SQL 是等值查詢,所以,這個問題的解決方案很簡單,即創建一個惟一索引。
只要有鎖,就有競爭。邏輯讀時的鎖,除了 CBC Latch ,還有 Buffer Pin 鎖。Buffer Pin 鎖的相關等待事件就是 Buffer Busy Waits 。
Buffer Pin 鎖有兩種模式:共享和獨佔,分別對應讀、寫兩種操做。若是要讀一個 Buffer,先要得到共享的 Buffer Pin 鎖,這樣才能讀這個 Buffer。寫則是要先得到獨佔 Buffer Pin 鎖,而後才能修改 Buffer。
至於 Buffer Pin 鎖的阻塞模式,寫與寫互相阻塞,這是毋庸置疑的。但讀與寫之間的阻塞模式,Oracle 10g 後的版本中就未再簡單地讓它們阻塞了事,而是很巧妙地對併發的讀、寫操做進行了優化。下面就一塊兒來看看。假設 A 進程先發起讀操做,B 進程後然後發起寫操做,這樣一來,B 進程的寫會被 A 進程的讀阻塞。可是,根據測試,B 並不會被 A 阻塞。過程以下:
步驟1:A 進程在 BH 中成功地設置了共享 Buffer Pin 鎖。注意,此時 A 進程已經釋放了 CBC Latch 。CBC Latch 的目的之一就是保護 Buffer Pin 鎖的設置過程,這在前文中已經重點描述。Buffer Pin 鎖設置成功了,固然就要釋放 CBC Latch 。A 進程會在 Buffer Pin 鎖 的保護下讀 Buffer 中的數據,如圖 15 所示。
圖 15 設置 Buffer Pin 鎖
步驟2:B 進程想修改 Buffer,它首先得到 CBC Latch,如圖 16 所示。
圖 16 B 進程得到 CBC Latch
步驟3:B 進程查看 BH 中 Buffer Pin 鎖的狀態,發現其餘進程留下的 S 鎖,如圖 17 所示。B 進程會等待嗎?固然不會。
圖 17 B 進程查看 Buffer Pin 鎖狀態
步驟4:在這一步中 B 進程會作不少工做。它會在原來的 BH 中也留下一個共享的 Buffer Pin 鎖,而後釋放 CBC Latch 。在共享 Buffer Pin 鎖的保護下,將原來的 Buffer 複製到 Buffer Cache 裏另外的 Buffer 中(這個過程 Oracle 稱爲 Buffer 克隆,即 Buffer Clone),如圖 18 所示。而後還要增長一個新的 BH,這一過程又要在 CBC Latch 的保護下進行。新的 BH 創建完畢後,釋放 CBC Latch 。這一步完成後的最終結果如圖 18 所示。HASH 鏈中多了一個如出一轍的 BH,下面的 BufferCache 池中則多了一個如出一轍的 Buffer 。
圖 18 B 進程 Buffer 克隆
注意,在圖 18 中 BH 中多了一行:STATUS 。顧名思義,STATUS 表示 Buffer 的狀態。在 Buffer 克隆開始時,原 Buffer 的 BH 中 STATUS 值爲 XCUR 。Buffer 克隆的目標 Buffer BH 中的 STATUS 沒有值。
對於這個狀態列的意義,可經過圖 19 來介紹。
步驟5:在源 Buffer 已經成功複製到另外的 Buffer 中、原來的 CBC 鏈中增長一個新的 BH 後,從本步驟開始,首先得到 CBC Latch,修改源 BH 中的 STATUS 爲 CR,新 BH 中的 STATUS 爲 XCUR。同時,在新 BH 中,設置獨佔 Buffer Pin 鎖,準備開始修改 Buffer 。到此爲止,Buffer 克隆纔算結束。
Buffer 克隆結束後,Buffer Cache 中多了一個新的 STATUS 爲 XCUR 的 Buffer,原 Buffer 的 STATUS 則被改成 CR 。
CR 的本意爲一致讀(consist read),在這裏,咱們只須要知道,它不是當前塊。XCUR 是當前塊,表明它裏面包含用戶全部最新的修改。
圖 19 STATUS 的意義
Oracle之因此能作到讀不阻塞寫,奧祕就在此。將 A 進程正在讀的 Buffer 轉爲 CR 塊,另外複製一個 Buffer 做爲 XCUR 塊。A 進程正在讀 Buffer 數據時,它只持有 Buffer Pin 鎖,並不持有 CBC Latch,所以 B 進程能夠申請 CBC Latch,修改 A 進程正在讀的 Buffer 的 BH,將它的狀態改成 CR 。這對 A 進程的讀操做並不會產生影響,由於 A 進程正在讀的是 Buffer 數據,而 B 進程修改的是 BH 。
步驟6:釋放 CBC Latch 。在獨佔 Buffer Pin 鎖的保護下,修改對應 Buffer,如圖 20 所示。
圖 20 B 進程修改對應 Buffer
若這個時候正好又來個 C 進程,也要讀 5 號文件中的 1234 號塊,會是什麼狀況呢?
固然就要等待了。C 進程要在狀態爲 XCUR 的 BH 中加共享的 Buffer Pin 鎖。可是這個 BH 中正有一個獨佔 Buffer Pin 鎖,所以 C 進程被阻塞。這個等待事件固然就是大名鼎鼎的 Buffer Busy Waits 了。
這就是 Oracle 10g 後直到 Oracle 12c 中 Buffer Busy Waits 的重要改變:讀不阻塞寫,而寫阻塞讀。
所以,形成 Buffer Busy Waits 等待的「元兇」只能是 DML 語句。若是在 AWR 中看到 Buffer Busy Waits,那就從 DML 上找緣由吧。
摘自:書籍《Oracle 內核技術揭密》