標籤:style blog http color io os ar 使用 strong
----------------------------我是分割線-------------------------------
本文翻譯自微軟白皮書《SQL Server In-Memory OLTP Internals Overview》:http://technet.microsoft.com/en-us/library/dn720242.aspx
譯者水平有限,如有翻譯不當之處,歡迎指正。
----------------------------我是分割線-------------------------------
行和索引儲存
記憶體中OLTP的記憶體最佳化表與表上的索引採用了與基於磁碟的表非常不同的方式進行儲存。記憶體最佳化表並不像基於磁碟的表那樣儲存在資料頁或者從盤區中分配的空間中,這是由於記憶體最佳化表是為按位元組定址的記憶體而不是按塊定址的磁碟所最佳化的設計原則決定的。
資料行
資料行是從被稱為堆的結構中分配的,這些堆與SQL Server為基於磁碟的表所支援的堆的類型不同。單表上的資料行不一定存放同一張表的其他資料行附近, SQL Server知道哪些資料行屬於同一個表的唯一方法是因為它們都是用這張表的索引串連在一起。這就是為什麼記憶體最佳化表必須擁有至少一個建立在其中的索引的要求。索引為表提供了結構。
資料行本身的結構與基於磁碟的表使用的資料行結構有很大不同。每個資料行包含一個標題和含有這個資料行屬性的承載。圖2展示了這樣的結構,以及擴充了標題區中的內容。
圖2 記憶體最佳化表中一個資料行的結構
資料列名
標題包含了兩個8位元組的欄位,這兩個欄位記錄了記憶體中OLTP的時間戳記:開始時間戳和結束時間戳記。每個支援記憶體最佳化表的資料庫都會管理兩個用於產生這些時間戳記的內部計數器。
- 事務ID計數器是一個全域的唯一值,當SQL Server執行個體重新啟動時,這個值會被重設。每次新的事務啟動時它會遞增。
- 全域事務時間戳記也是全域和唯一的,但在重新啟動時不會被重設。這個值在每個事務結束後並開始驗證處理過程時增加。新的值則為當前事務的時間戳記。在使用從恢複記錄中找到的最大事務時間戳記進行恢複時,全域事務時間戳記的數值會被初始化。 (我們稍後將在本白皮書中看到更多關於恢複的內容。)
開始時間戳的值是插入資料行的事務的時間戳記,結束時間戳記的值是刪除資料行的事務的時間戳記。一個特殊值(稱為“無窮大”)被用作為尚未被刪除行的結束時間戳記。但是,當資料行第一次被插入時,在插入事務完成之前,事務的時間戳記是未知的,因此在事務提交前開始時間戳會使用全域的事務ID值。類似地,對於刪除操作,事務的時間戳記是未知的,所以對於被刪除的資料行的結束時間戳記則使用全域的事務ID值,一旦確定了事務實際的時間戳記,這個全域的事務ID值則會被更換。正如在討論資料操作時我們所看到的,開始時間戳和結束時間戳記確定了哪些其它的事務將能夠看到這個資料行。
標題還包含了一個四位元組的語句ID值。一個事務中的每條語句都有一個唯一的StmtId值,當資料行被建立時,它會為儲存建立這個資料行語句的StmtId。如果相同的語句再次訪問同一資料行,它的StmtId則會被跳過。
最後,標題包含一個雙位元組值(idxLinkCount),這個引用計數表明了有多少索引引用了這一資料行。接下來的idxLinkCount值是一組索引指標,在下一節中將進行介紹。指標的數量等於索引的數量。資料行的引用值需要從1開始,這樣即使該資料行不再串連到任何索引,資料行仍可以被垃圾收集(garbage collection ,GC)機制所引用。垃圾收集被認為是初始引用的“所有者”。
如之前所提到的,表上的每個索引都有一個指標,並且是這些指標加上索引資料結構將資料行串連在一起。除了索引指標將資料行串連在一起之外,並沒有其他結構將資料行合并成一張表。正是這個原因才導致了所有記憶體最佳化表中都必須至少有一個索引的要求。另外,由於指標的數量是資料行結構的一部分並且資料行從來都不能被修改,因此所有索引都必須在建立記憶體最佳化表時進行定義。
承載資料
承載就是資料行本身,包含索引值列加上資料行中所有其他的列。(這就意味著,在記憶體最佳化表上的所有索引實際上都是覆蓋索引)承載的格式依據表進行變化。正如在前面建立表的章節所提到的,記憶體中OLTP編譯器為表操作產生了DLL檔案,只要它知道在資料行插入表時所使用的承載的格式,它就可以為所有的資料行操作產生適當的命令。
記憶體最佳化表中的索引
所有記憶體最佳化表必須至少有一個索引,因為正是索引將所有資料行串連在一起。正如前面提到的,資料行並不儲存在資料頁中,所以沒有資料頁或盤區的集合,也沒有分區或配置單位可以被引用來擷取表上所有的資料頁。各類索引中一類索引有索引頁的一些概念,但這些索引頁儲存的方式不同於基於磁碟的表的索引。
記憶體中OLTP的索引,以及在資料處理過程中對它們所做的更改,都不會被寫入磁碟。只有資料行以及資料的變化,才會被寫入到交易記錄中。記憶體最佳化表的所有索引都在資料庫恢複期間基於索引的定義進行建立。我們將在接下來的檢查點和恢複章節中討論相關的細節。
雜湊索引
雜湊索引由一個指標數組組成,數組中的每個元素被稱為一個雜湊桶。每個資料行中的索引索引值列都有一個對其應用的雜湊函數,並且這個函數的結果確定了這個資料行使用哪個雜湊桶。具有相同雜湊值的所有索引值(即擁有通過雜湊函數所得出的相同的結果)通過雜湊索引中同一個指標進行訪問,並且串連成一條鏈。當某一資料行被添加到表中時,雜湊函數被應用到該資料行中的索引的索引值上。由於重複索引值將總是產生相同的函數結果,因此如果存在重複的索引值,索引值將始終位於同一條鏈中。
圖3顯示了在Name列上一個雜湊索引的一個資料行。對於這個例子,假定有一個非常簡單的雜湊函數,這個函數產生與該字串中索引索引值列長度相等的一個數值。第一個值“Jane”的雜湊值為4,目前為止,這個值在雜湊索引的第一個雜湊桶中。(請注意,真正的雜湊函數是更加隨機和不可預測的,不過現在使用的這個長度的例子使其更加易於解釋說明。)你可以看到從雜湊表中值為4的條目到Jane所在資料行的指標。這一資料行並不指向任何其他的資料行,所以這條記錄的索引指標為NULL。
圖3 擁有單個資料行的一個雜湊索引
在圖4中,一個Name列值為Greg的資料行已經被添加到表中。由於我們假設Grag的雜湊值也映射到4上,因此Grag與Jane在同一個雜湊桶中,並且資料行被連結到與Jane所在資料行的同一條鏈中。Greg所在資料行有一個指向Jane所在資料行的指標。
圖4擁有兩個資料行的一個雜湊索引
在City列上,表定義中包含的第二個雜湊索引建立了第二個指標欄位。現在,表中的每一個資料行都有兩個指標(每個索引各有一個)指向它,並具備指向兩個以上資料行的能力。每一個資料行的第一個指標為Name列上的索引指向鏈中的下一個值;第二個指標為City列上的索引指向鏈中的下一個值。圖5顯示了在Name列上同一個雜湊索引,現在這個索引擁有了雜湊值為4的三個資料行,以及雜湊值為5 的兩個資料行,等於5的雜湊值使用了Name列上索引的第二個雜湊桶。在City列上的第二個索引使用了三個雜湊桶。雜湊值為6對應的雜湊桶在鏈中擁有三個值,雜湊值為7對應的雜湊桶在鏈中擁有一個值,而雜湊值為8對應的雜湊桶在鏈中也擁有一個值。
圖5 在同張表上的兩個雜湊索引
如此前在CREATE TABLE的樣本所示,當建立一個雜湊索引時,必須指定雜湊桶的數量。我們建議選擇雜湊桶的數量等於或大於索引索引值列預期的基數(即唯一值的數量),這樣每個雜湊桶只擁有鏈中單一值的資料行的可能性更大一些。不過要注意不要選擇一個過大的數量,因為每個雜湊桶都需要消耗記憶體。你提供的數字會被湊整為2的下一個乘方,所以50000的值將被湊整至65536。擁有額外的雜湊桶並不會提高效能,但很明顯會浪費記憶體並且有可能減少降低掃描的效能,因為掃描會為資料行檢查每一個雜湊桶。
在決定建立一個雜湊索引時,記住實際使用的雜湊函數是基於所有的索引值列。這就是說,如果在一個employees表中的lastname和firstname列上有一個雜湊索引,值為“Harrison”和“Josh”的資料行可能會被雜湊到與值為“Harrison”和“John”的資料行不同的雜湊桶中。只提供一個lastname值或者一個不精確的firstname值(例如,“Jo%”)的查詢將完全無法使用這個索引。
記憶體最佳化的非叢集索引
如果你不知道某個列所需的雜湊桶的數量,或者如果你知道你會根據值的範圍來尋找資料,你應該考慮建立一個記憶體最佳化的非叢集索引,而不是雜湊索引。這些索引都是通過採用被稱為Bw樹的新型資料結構來實現的,Bw樹最初是由微軟研究院在2011年設想並定義的。記憶體最佳化的非叢集索引是B樹的一個沒有鎖和閂鎖的變型。
除了索引頁沒有固定的大小之外,這個非叢集索引的大體結構類似於SQL Server的常規B樹,並且一旦建立,它們是不可改變的。像一個普通B樹頁一樣,每個索引頁包含了一組有序的索引值,並為每個值都有一個對應的指標。在索引的上層,在所謂的內部頁上,指標指向索引樹下一級的索引頁,而在分葉層級,指標指向一個資料行。就像記憶體中OLTP的雜湊索引那樣,多個資料行可以被串連到一起。對於非叢集索引,具有相同索引索引值的資料行將會被串連起來。
記憶體最佳化的非叢集索引和SQL Server的B樹之間的很大的一個區別是頁指標是一個邏輯頁ID(PID),而不是一個物理頁號。 PID標誌著映射表中的一個位置,映射表中通過實體記憶體地址串連到每個PID上。索引頁永遠不會被更新,而是被替換為一個新的頁並更新映射表,這樣,相同的PID就指示到了一個新的實體記憶體地址上。
圖6表明了一個記憶體最佳化的非叢集索引的大體結構,以及頁映射表。
圖6 一個記憶體最佳化的非叢集索引的大體結構
在圖6中並沒有將所有的PID值都標記出來,而且映射表也沒有顯示出在使用中的所有PID值。索引頁顯示了索引引用的索引值。內部索引頁中的每個索引行包含了一個索引值(),和下一個層級頁的PID。索引值為所引用的頁上可能的最大值。 (注意,這與常規的B樹索引不同,對於常規的B樹索引,索引行儲存了在下一層級頁上的最小值。)
葉級索引頁也包含索引值,但不是一個PID,它們包含一個資料行的實際記憶體位址,這個實際記憶體位址可能位於索引值相同的資料行的鏈中的第一位。
記憶體最佳化的非叢集索引和SQL Server的B樹的另一大區別是在葉級節點,使用一組增量值來記錄對資料更改的跟蹤。對於每次更改,葉級頁本身並不被替換。每次對一個頁的更新,可以是在頁中插入或刪除一個索引值,都會產生一個包含了表明所執行更改的增量紀錄的頁。一個更新則是由兩個新的增量記錄來表示,一個是刪除的原始值,一個是插入的新值。當添加每個增量記錄時,映射表都會用包含新增增量記錄的頁的物理地址進行更新。圖7說明了這種行為。映射表只顯示了邏輯地址為P的單個頁。如P頁所示,在映射表中的物理地址原本是對應的葉級索引頁的記憶體位址。在索引索引值為50的新行(假設在表的資料中這個值此前並沒有發生過)被添加到表中之後,記憶體中的OLTP將delta記錄增加到P頁,這表明了新值的插入,並更新了P頁的物理地址,以表明第一個增量記錄頁的地址。假設然後索引索引值為48的唯一行從表中被刪除。那麼記憶體中OLTP必須刪除索引值為48的索引行,因此建立了另一個增量記錄,並且P頁上的物理地址再次被更新。
圖7 連結到一個葉級索引頁的增量記錄
索引頁結構
儘管最大的索引頁的大小仍然是8KB,但與基於磁碟的表上的索引不同,記憶體中OLTP的非叢集索引頁並沒有一個固定的大小。
記憶體最佳化的所有非叢集索引頁都擁有包含以下資訊的標題區:
- PID -到映射表的指標
- 頁類型 - 葉級頁,內部頁,增量頁或特殊頁
- 右頁PID -當前頁面右側頁面的PID
- 高度 - 從當前頁到葉級頁的垂直距離
- 頁面的統計資料 – 增量記錄的數量加上頁上記錄的數量
- 最大索引值 - 頁上數值的上限
此外,葉級頁及內部頁都包含兩個或三個固定長度的數組:
- 數值 - 這事實上是一個指標數組。數組中的每個條目為8位元組長。內部頁的條目包含了下個層級中一個頁的PID,而葉級頁的條目包含了具有相等索引值的資料行所在鏈中第一個資料行的記憶體位址。(需要注意的是從技術的角度說,PID可以儲存在4個位元組中,但為了對所有的索引頁都採用相同的數值結構,因此數組允許每個條目為8個位元組)。
- 位移 - 這個數組只存在於擁有可變長度索引值的索引的頁中。每個條目為2個位元組,並包含了對應索引值在頁上的索引值數組中起始位置的位移。
- 索引值 - 這是索引值的數組。如果當前頁是一個內部頁,索引值表示PID所引用的頁上的第一個值。如果當前頁是葉級頁,索引值則是資料行所在鏈中的值。
最小的頁通常是增量頁,增量頁擁有一個包含了與內部頁或葉級頁大部分相同資訊的標題。但是增量頁的標題沒有葉級頁或內部頁所介紹的數組資訊。一個增量頁只包含一個作業碼(插入或刪除)和一個數值,就是資料行所在鏈中第一個資料行的記憶體位址。最後,增量頁也還包含用於當前增量操作的索引值。實際上,你可以把一個增量頁看作是一個小型的儲存單個元素的索引頁,而普通索引頁則儲存了N個元素的一個數組。
非聚集內部重構作業
有三種不同的操作可以被用於管理這個索引的結構:匯總,拆分和合并。對於所有這些操作,都不會更改現有的索引頁。而是更改映射表來更新一個PID值所對應的物理地址。如果一個索引頁需要添加一個新的資料行(或者刪除一個資料行),則會建立一個全新的頁,並在映射表中更新PID值。
增量記錄的匯總
一條增量記錄的長鏈最終會導致搜尋效能的降低,因為當SQL Server通過索引進行搜尋時,它必須考慮在增量記錄中的更改以及索引頁的內容。如果記憶體中OLTP嘗試將一個新的增量記錄添加到一個已經有16個元素的鏈中時,增量記錄中的更改將被匯總為一個引用的索引頁,並且該頁將被重建,並包含了觸發匯總的那條新的增量記錄所指示的更改。新的重建頁的PID值和原有的值相同,但是擁有一個新的記憶體位址。舊的頁(索引頁加上增量頁)將被標記為記憶體回收。
一個完整索引頁的拆分
一個索引頁根據按需的原則增長,可以從儲存單行開始到最多儲存8K位元組。一旦索引頁增長到8K位元組,一條新插入的資料行將會導致索引頁進行拆分。對於一個內部頁,這就意味著沒有更多的空間來添加另一個索引值和指標,而對於一個葉級頁,則表示一旦合并所有的增量記錄,這個資料行則太大而無法放在這個頁中。葉級頁的網頁標題中的統計資訊持續對匯總增量記錄所需空間進行跟蹤,每添加一個新的增量記錄時,這個資訊都會進行調整。拆分操作由接下來介紹的兩個原子步驟完成。假設Ps是拆分成頁P1和P2的頁,而PP則是父級頁,其擁有指向Ps的一個資料行。
- 步驟1:分配兩個新的頁P1和P2,並將資料行從Ps頁面分割到這些頁上,包括新插入的行。在頁映射表中的一個新的位置用來儲存?? P2頁的物理地址。P1和P2這兩個頁此時還不能被任何並發的操作訪問到。另外還設定了從P1到 P2的“邏輯”指標。這個操作完成後,在同一個原子操作內對頁映射表進行更新,將指標從指向Ps更改為指向P1。這個操作之後,就沒有指向Ps頁的指標了。
- 步驟2:步驟1後,父級頁PP指向了P1,但沒有一個直接的指標從父級頁指向頁P2。頁P2隻能通過頁P1訪問。為了建立一個從父級頁到頁P2的指標,需要分配一個新的父級頁PNP,從頁PP複製所有的資料行,並添加一個新的資料行指向頁P2。這個操作完成後,在同一個原子操作內對頁映射表進行更新,將指標從PP改變到PNP。
相鄰索引頁的合并
當刪除操作使得一個索引頁P只剩下少於最大頁面大小(目前為8K)的10%,或者在頁上只有單個資料行時,頁P將會被合并到其鄰近的頁面。類似於拆分,這也是一個多步驟的操作。對於這個例子,假設我們將一個頁及其左鄰的頁進行合并,左鄰的頁也就是擁有更小數值的那個頁。當一個資料行從頁P中刪除時,標識刪除的增量記錄照常添加。此外,還執行了一個檢查以確定頁P是否符合合并的條件(比如,刪除資料行後剩餘的空間會小於最大頁面大小的10%)。如果符合條件,合并則會按照以下的介紹在三個原子步驟中執行。對於這個例子,假設頁PP是擁有一個指向頁P的資料行的父級頁,Pln表示左鄰的頁面,並且我們假設它的最大值是5。這就意味著在父級頁PP中指向Pln的資料行中包含為5的值。我們將在頁P中刪除索引值為10的資料行。刪除後,將只有一個索引值為9的資料行保留在頁P中。
- 步驟1:建立了一個表示索引值為10的增量頁DP10,並且將其指標設定為指向P。另外建立了一個特殊的“合并增量頁”DPM,將其指向DP10。請注意,在這個階段,頁DP10和DPM對於任何並發事務都還是不可見的。在同個原子步驟中,在頁映射表中指向頁P的指標被更新成指向DPM。這一步之後,在父頁面PP中索引值為10的條目現在指向DPM了。
- 步驟2:在這一步中,在頁PP中表示索引值為5的資料行被刪除,並且索引值為10的條目被更新為指向頁Pln。為了做到這一點,分配了一個新的非葉級頁PP2,並且頁PP中除了表示索引值為5的資料行之外,其他所有資料行都被複製到頁PP2中;然後索引值為10的那個資料行被更新為指向頁Pln。這個操作完成後,在同一個原子操作內,指向頁PP的頁映射表條目被更新為指向頁PP2。頁PP則不能再被訪問到。
- 步驟3:在這一步中,葉級頁P和Pln被合并,而增量頁則被刪除。為了做到這一點,分配了一個新的頁Pnew,並且P和Pln中的資料行進行合并,並且新的頁Pnew包含了增量頁中的更改。現在,在同一個原子操作內,指向頁Pln的頁映射表條目被更新為指向頁Pnew。
資料操作
SQL Server的記憶體中OLTP通過維護一個以提供時間戳記的目的的內部事務ID來確定哪些行版本對哪些事務可見,在本節中將稱其為時間戳記。時間戳記是由每次事務提交時增長的一個單調遞增計數器產生。一個事務的開始時間是在事務開始的那個時間點資料庫中最大的時間戳記,並在事務提交時,會產生一個新的時間戳記,這個時間戳記唯一標識了這個事務。時間戳記是用來指定以下內容:
- 提交/結束時間:修改資料的每個事務提交的不同的時間點被稱為事務的提交或結束時間戳記。提交時間能夠在序列化的記錄中有效地標識事務所在的位置。
- 一條記錄某個版本的有效時間:2所示,資料庫中的所有記錄都包含兩個時間戳記——開始時間戳(Begin-Ts)和結束時間戳記(End-Ts)。開始時間戳是指建立版本的事務的提交時間,而結束時間戳記是指刪除版本(也許是用一個新版本替換)的事務的提交時間戳記。一條紀錄版本的有效時間是指其版本被其他事務可見的時間戳記的範圍。比如,在圖5中,Susan的記錄是在時間“90”時從Vienna被更新到Bogota。
- 邏輯讀取時間:讀取時間可以是事務的開始時間和目前時間之間的任意值。只有有效時間與邏輯讀取時間重疊的版本,對於讀操作才可見。對於除了已提交讀之外的其他所有隔離等級,一個事務的邏輯讀取時間對應於事務的開始。對於已提交讀則對應於事務中一條語句的開始。
版本可視性的概念是在記憶體中OLTP中進行恰當的並發控制的基礎。在邏輯讀取時間RT執行的事務必須只能看到那些開始時間戳小於RT和結束時間戳記大於RT的版本。
記憶體最佳化表允許的隔離等級
記憶體最佳化表上的資料操作總是使用樂觀的多版本並發控制(Multi Version Concurrency Control ,MVCC)。樂觀的資料訪問不使用鎖或閂鎖來提供事務隔離。我們將介紹這個無鎖和無閂鎖的行為如何進行管理的細節,以及關於後面章節中提到的允許的交易隔離等級原因的詳細資料。在本節中,我們將只討論理解資料訪問和修改操作基本原理所需的交易隔離等級的細節。
訪問記憶體最佳化表的事務支援下列的隔離等級。
- SNAPSHOT
- REPEATABLE READ
- SERIALIZABLE
交易隔離等級可以被指定為本地編譯預存程序的原子塊的一部分。另外,當通過解釋型Transact-SQL訪問記憶體最佳化表時,可以使用表提示或者新的名為MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT的資料庫選項來指定隔離等級,這個資料庫選項透明地將較低的隔離等級(例如未提交讀和已提交讀)映射為快照隔離等級,從而減少將遷移部分應用程式以使用記憶體最佳化表所需的對應用程式進行的更改。更多細節請參考http://msdn.microsoft.com/en-us/library/dn133175(v=sql.120).aspx。
對於自動認可(單條語句)事務中的記憶體最佳化表支援READ COMMITTED隔離等級。記憶體最佳化表不支援顯式或隱式的使用者事務。(隱含交易是在IMPLICIT_TRANSACTIONS會話選項下調用的事務。在這個模式下,行為與明確交易一樣,但不需要BEGIN TRANSACTION語句。任何DML語句將啟動一個事務,並且事務必須顯式地提交或者復原。只有BEGIN TRANSACTION語句是隱式的。)自動認可事務的記憶體最佳化表支援READ_COMMITTED_SNAPSHOT隔離等級,並且僅在查詢未訪問任何基於磁碟的表時才支援。 此外,在 SNAPSHOT 隔離等級下通過解釋型 Transact-SQL 啟動的事務不能訪問記憶體最佳化表。 在 REPEATABLE READ 或 SERIALIZABLE 隔離等級下使用解釋型 Transact-SQL 的事務必須使用 SNAPSHOT 隔離等級訪問記憶體最佳化表 。
根據之前介紹的資料行在記憶體中的結構,現在讓我們通過一個例子來看看DML操作是如何執行的。我們將通過在角括弧中按順序列出內容來表示資料行。假設我們有一個在SERIALIZABLE隔離等級上啟動並執行事務ID為100的事務TX1,事務在時間戳記240時開始,並執行了兩個操作:
- 刪除資料行<Greg , Lisbon>
- 更新<Jane, Helsinki >為<Jane, Perth>
同時,另外兩個事務將讀取資料行。 TX2是一個在時間戳記243時啟動並執行,自動認可的單個SELECT語句。TX3是一個明確交易,它讀取一個資料行,然後基於它在Select語句中讀到的值更新另一個資料行,TX3的時間戳記為246。
首先我們來看看資料修改事務。事務開始時,擷取了一個表明事務開始的開始時間戳,這於資料庫的序列化順序相關。在這個例子中,這個時間戳記為240。
在事務運行時,事務TX1將只能夠訪問開始時間戳小於或等於240的記錄和結束時間戳記大於240的記錄。
刪除
事務TX1首先通過一個索引定位<Greg , Lisbon>。為了刪除這一資料行,該行的結束時間戳記被設定為100和一個額外的位元標誌位,這個標誌位表示100這個值是一個事務的ID。現在嘗試訪問該行的任何其它事務發現結束時間戳記包含了事務ID(100),這個值表明該行可能已經被刪除。然後它在事務圖中找到了TX1,並檢查事務TX1是否仍處於活動狀態,以確定<Greg , Lisbon>的刪除是否已經完成。
更新和插入
接下來,<Jane, Helsinki>的更新是通過將操作分為兩個獨立的操作來執行的:刪除整個原始的資料行,並插入一個完整的新的資料行。這通過構建一個新的資料行<Jane, Perth>開始,資料行包括值為100的開始時間戳和表示100這個值是一個事務ID的位元標誌位,然後將結束時間戳記設定為∞(無窮大)。試圖訪問該行的任何其他事務將需要確定事務TX1是否仍處於活動狀態以決定它是否可以看到<Jane, Perth>。然後通過將<Jane, Perth>串連到兩個索引中來進行插入。接下來,<Jane, Helsinki>會按照上個段落所描述的刪除操作進行刪除。任何其他試圖更新或刪除<Jane, Helsinki>的事務發現,結束時間戳記包含一個事務ID而不是無窮,從而確定有寫寫衝突,並會立即中止。
這時,事務TX1已經完成了操作,但還沒有提交。處理提交的過程通過為事務獲得一個結束時間戳記來開始。在這個例子中,假設時間戳記為250,標識其在資料庫的序列化順序中的點,在這個點上這個事務的更新邏輯上都已經完成。在獲得了這個結束時間戳記後,事務進入了被稱為驗證的狀態,在這個狀態下,資料庫執行檢查以確保事務並未違反當前的隔離等級。如果驗證失敗,事務則被中止。有關驗證的更多細節稍後進行介紹。在驗證階段的最後,SQL Server還將會寫入交易記錄。
事務跟蹤在一個寫入集合中的所有更改,寫入集合主要上是一系列的刪除/插入操作以及到與每個操作相關聯的版本的指標。本次事務的寫入集合以及更改的資料行在圖8的綠色框中顯示。這個寫入集合構成了事務的日誌內容。事務通常只產生一個包含其ID和提交時間戳記的單個日誌記錄,以及它刪除或插入的所有記錄的版本。對於受到影響的每條記錄將不會像基於磁碟的表那樣有單獨的日誌記錄。然而,日誌記錄的大小有一個上限,如果記憶體最佳化表中的一個事務超過了這個限制,則會產生多個日誌記錄。一旦將日誌記錄已被固化到儲存中,這個事務的狀態被改變成已提交,並啟動後續的處理。
後續的處理涉及到對寫入集合的遍曆,以及按如下的方式處理每個條目:
- 對於一個DELETE操作,將資料行的結束時間戳記設定為事務的結束時間戳記(在這種例子中是250)並清除在資料行的結束時間戳記欄位的類型標誌。
- 對於一個INSERT操作,將受影響資料行的開始時間戳設定為事務的結束時間戳記(在這種例子中是250),並清除在資料行的開始時間戳欄位的類型標誌。
舊資料行版本實際的取消連結和刪除操作由垃圾收集系統處理,這將在之後介紹。
圖8 在一張表上的事務的修改
讀取
現在,讓我們來看一看讀的事務,TX2和TX3,這兩個事務將與TX1同時進行處理。請記住,TX1正刪除<Greg , Lisbon>資料行,並將<Jane, Helsinki >更新為<Jane, Perth>。
TX2是讀取整個表的一個自動認可事務:
SELECT Name, City
FROM T1
TX2的會話在預設的隔離等級READ COMMITTED下運行,但如上所述,因為沒有指定提示,並且T1是記憶體最佳化表,因此將使用SNAPSHOT隔離來訪問資料。由於TX2在時間戳記為243時運行,因此它能夠讀取當時已存在的資料行。時間戳記為243時,資料行<Greg , Beijing>不再是有效,所以它不能夠訪問該行。資料行<Greg , Lisbon>在時間戳記為250時將被刪除,但它在時間戳記200到250之間有效,所以事務TX2可以讀取到它。 TX2也會讀取到資料行<Susan, Bogota >和資料行<Jane, Helsinki >。
TX3是在時間戳記246開始的一個明確交易,它將讀取一個資料行並基於讀取的值更新另一個資料行。
DECLARE @City nvarchar(32);BEGIN TRAN TX3 SELECT @City = City FROM T1 WITH (REPEATABLEREAD) WHERE Name = ‘Jane‘; UPDATE T1 WITH (REPEATABLEREAD) SET City = @City WHERE Name = ‘Susan‘;COMMIT TRAN -- commits at timestamp 255
在事務TX3中,SELECT語句將讀取到資料行<Jane, Helsinki >,因為該行在時間戳記243之後仍然是可訪問的。然後語句將會把資料行< Susan, Bogota >更新為< Susan, Helsinki >。然而,如果在TX1已經提交之後,事務TX3嘗試提交,SQL Server將會檢測到資料行<Jane, Helsinki >已經被另一個事務更新了。這是違反了REPEATABLE READ隔離等級的要求,所以提交將會失敗並且事務TX3將復原。在下一節我們將介紹關於驗證更多的內容。
驗證
在最後提交與記憶體最佳化表相關的事務之前,SQL Server會執行一個驗證步驟。因為在資料修改過程中不需要鎖,所以根據所請求的隔離等級,資料更改可能會導致無效資料。因此提交處理過程的這一階段可以確保不會有無效資料。
下面列出了在每一個可能的隔離等級中,一些可能會遇到的違反隔離等級的情況。更有可能的違反情況以及提交的依賴性,將在下一章節更加詳細地描述隔離等級和並發控制時進行介紹。
如果在SNAPSHOT隔離等級下訪問記憶體最佳化表,當嘗試執行COMMIT時,可能會有以下驗證錯誤:
- 如果當前事務插入了一個資料行與在當前事務之前提交的另一個事務插入的資料行,擁有同樣的主索引值,將會產生41325錯誤(“The current transaction failed to commit due to a serializable validation failure .”),並且事務將被中止。
如果在REPEATABLE READ隔離等級下訪問記憶體最佳化表,當嘗試執行COMMIT時,可能會有以下驗證錯誤:
- 如果當前事務已經讀取了在當前事務之前提交的另一個事務更新的任何資料行,將會產生41305錯誤(“The current transaction failed to commit due to a repeatable read validation failure.”),並且事務將被中止。
如果在SERIALIZABLE隔離等級下訪問記憶體最佳化表,當嘗試執行COMMIT時,可能會有以下驗證錯誤:
- 如果當前事務無法讀取符合指定過濾條件的任何有效資料行,或遇到了由其他事務插入的符合指定過濾條件的幻影資料行,提交將會失敗。事務需要按照好像不存在任何並發事務那樣執行。所有的行動邏輯上都在單個序列化的時間點上發生。如果違反了這些保障中的任意一條時,都將會產生41305錯誤,並且事務將被中止。
T-SQL支援
記憶體最佳化表可以通過兩種不同的方式進行訪問:要麼通過採用解釋型Transact-SQL來進行互操作,要麼通過本地編譯的預存程序。
解釋型的
Transact-SQL
當使用互操作功能時,對於使用記憶體最佳化表,幾乎能夠獲得Transact-SQL的全部功能,但並不能獲得與使用本地編譯預存程序訪問記憶體最佳化表相同的效能。運行即席查詢,或者在將應用程式遷移到記憶體中OLTP時,在遷移對效能影響最大的預存程序之前,作為遷移過程中的一步,互操作都是合適的選擇。解釋型的Transact-SQL也應該在需要同時訪問記憶體最佳化表和基於磁碟的表時使用。
使用互操作訪問記憶體最佳化表時,不支援的Transact-SQL功能如下:
- TRUNCATE TABLE
- MERGE(當目標是記憶體最佳化表時)
- 動態和鍵集遊標(這些遊標都都自動降級為靜態資料指標)
- 跨資料庫查詢
- 跨資料庫事務
- 連結的伺服器
- 鎖提示:TABLOCK,XLOCK,PAGLOCK等(支援NOLOCK,但是會被自動忽略)。
- 隔離等級提示READUNCOMMITTED,READCOMMITTED和READCOMMITTEDLOCK
T-SQL
的本地編譯預存程序
原生編譯預存程序允許你以最快的方式執行Transact-SQL語句,其中包括訪問記憶體最佳化表中的資料。然而,這些預存程序比起Transact-SQL語句卻有更多的限制。在本地編譯預存程序中,對可以訪問和處理的資料類型和定序也有限制。支援的Transact-SQL語句,資料類型和允許的運算子的完整列表,請參閱文檔。此外,在本地編譯預存程序中,完全不允許訪問基於磁碟的表。
這些限制的原因是由於事實上在引擎內部,必須為每個表上的每個操作建立一個單獨的函數。這個介面在後續版本中將會進行擴充。
在記憶體中的資料行的記憶體回收
因為記憶體中OLTP是一個多版本的系統,刪除和更新操作(以及中止的插入操作)會產生行版本,而這些行版本最終將變為失效,這意味著對任何事務它們都將不再可見。這些不需要的版本會減慢索引結構掃描的速度,還會建立未被使用的需要被回收的記憶體。
對於記憶體最佳化表中失效版本的垃圾收集過程類似於採用基於快照的隔離等級之一的情況下,SQL Server對於基於磁碟的表執行的版本儲存清理。但是一個很大的區別是,清理不是在tempdb中進行,而是在記憶體表的結構本身中進行。
要確定哪些資料行可以安全刪除,系統會持續跟蹤在系統中啟動並執行最早的活動事務的時間戳記,並使用這個值來決定仍可能需要哪些資料行。在這個時間點之後任何無效的資料行(即,它們的結束時間戳記早於這個時間點)都被認為是失效的。失效的資料行會被刪除,它們的記憶體會被釋放回系統。
垃圾收集系統的設計是非阻塞的、協同的、高效的、積極反應的和可擴充的。特別令人感興趣的是“協同”屬性。雖然有一個系統線程專門用於記憶體回收過程,但實際上使用者線程做了大部分的工作。如果一個使用者線程正在掃描索引(記憶體最佳化表上的所有索引訪問都被認為是索引掃描),並且遇到一個失效的行版本,那麼它會將這個行版本從當前的鏈中取消連結,並調整指標。它也將遞減資料列名地區中的引用計數。此外,當使用者線程完成一個事務時,使用者線程接著將有關事務的資訊添加到一個待垃圾收集過程處理的事務隊列中。最後,使用者線程從垃圾收集線程建立的一個隊列中擷取一個或多個工作條目,並釋放組成工作條目的資料行使用的記憶體。
垃圾收集線程大約每分鐘檢查一次已完成的事務隊列,但系統可以根據等待處理的已完成的事務數量在內部調整頻率。對於每一個事務,它決定哪些資料行是失效的,並建立由一組準備移除的資料行組成的工作條目。在CTP2版本中,一個組中資料行的數量為16,但這個數字在未來的版本中可能會改變。這些工作條目分布在多個隊列中,每個SQL Server使用的CPU對應一個隊列。一般情況下,從記憶體中移除資料行的實際工作是留給處理隊列中這些工作條目的使用者線程來處理,但是如果使用者活動很少,垃圾收集線程本身會刪除資料行來回收系統的記憶體。
動態管理檢視 sys.dm_db_xtp_index_stats對於每個記憶體最佳化表中的每個索引有一條記錄,rows_expired列表示在索引的掃描過程中,有多少資料行被檢測為是失效的。還有一個名為rows_expired_removed的列表示多少資料行已經從索引中取消連結。如上面提到的,一旦資料行已經從一張表的所有索引中取消連結,它則可以由垃圾收集線程刪除。所以,在對於記憶體最佳化表中的每個索引rows_expired計數器都已經增加了之前,rows_expired_removed的值都不會增長。
以下查詢可以觀察這些值。它將sys.dm_db_xtp_index_stats動態管理檢視與sys.indexes目錄檢視串連從而能夠返回索引的名字。
SELECT name AS ‘index_name‘, s.index_id, scans_started, rows_returned, rows_expired, rows_expired_removedFROM sys.dm_db_xtp_index_stats s JOIN sys.indexes i ON s.object_id=i.object_id and s.index_id=i.index_idWHERE object_id(‘<memory-optimized table name>‘) = s.object_id;GO
---------------------------待續-------------------------------
SQL Server 記憶體中OLTP內部機制概述(一)
SQL Server 記憶體中OLTP內部機制概述(二)
SQL Server 記憶體中OLTP內部機制概述(三)
SQL Server 記憶體中OLTP內部機制概述(四)
SQL Server 記憶體中OLTP內部機制概述(五)
SQL Server 記憶體中OLTP內部機制概述(二)