第三章 存儲與檢索

第二章關注的是將數據錄入數據庫系統的格式,以及檢索出來的機制,這章關注同樣的問題,但是是從數據庫的視角來看:數據庫如何存儲我們的數據,以及如何檢索出我們需要的數據。

書中開篇列舉了一個 使用bash 命令製作的簡單數據庫的例子:

db_set(){
	echo "$1,$2" >> database
}
db_get () {
    grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

這個數據庫非常簡單,是一個僅追加的日誌文件,即寫入性能非常好;但是由於數據庫文件是個僅追加log 文件,所以更新數據也是添加記錄,那麼要查詢到某條數據最新的值,則需要遍歷整個文件找到最後的這條記錄。

爲了高效的查找數據庫中的值,則需要一個數據結構:索引(Index)

索引是從主數據庫中衍生的附加結構,索引只會影響查詢性能,但是更新這些索引卻需要額外的開銷。

索引

哈希索引

原理就是 KV 存儲,將 key 的hash 值存儲在內存中,value 指明值在數據文件中的位置。這種操作的前提是所有的key 都可以存儲在內存中。作者還提到了一種場景,比如視頻的 URL 和其被點擊的次數,key 不經常發生變化,值的寫入次數很頻繁。提到的數據庫Bitcask 存儲模型

接着作者引出了另外一個問題:一個文件寫滿了怎麼辦,磁盤用完了怎麼辦?

這個簡單,多開幾個文件,一個日誌文件寫滿了換下一個,然後對寫滿的日誌文件進行壓縮;壓縮意味着在把重複的 key 丟掉,只保留每個鍵的最近更新。

在壓縮的過程中,讀寫請求使用原有的文件,壓縮完成後將讀請求切換到新的合併後的文件,對於舊的文件就可以刪除了。當然這些操作都是在後臺進行。

需要考慮幾個重要問題

文件格式

最好使用二進制文件

刪除記錄

刪除一個鍵,需要在數據文件上對鍵做特殊的刪除標記。這樣在進行日誌文件合併的時候就可以不用管這個鍵了。

崩潰恢復

Bitcask有個內存哈希索引,用來存儲鍵和數據的位置,當數據庫重啓時,內存中的數據就沒了,所以這個內存哈希表也需要持久化到磁盤上。同時也能提高構建內存哈希索引的速度

併發控制

寫操作是嚴格按照順序追加到日誌中。通常只會有一個線程去寫入。追加寫有很多好處;

  1. 比隨機寫速度快很多
  2. 崩潰和恢復就簡單許多。這塊會在事務那章仔細展開
  3. 合併舊的文件可以讓數據始終在一處

同時我們也要看到Bitcask內存哈希索引的壞處:

  • 很多的鍵。可能多到內存都放不下,(1)那麼就需要在磁盤上保持哈希索引,大量的IO 操作,會讓查詢性能下降;(2)解決哈希衝突需要很多邏輯,會佔用資源
  • 範圍查詢效率不高。哈希索引不是有序的。當然我們可以附加別的數據結構讓它變得有序!

LSM 樹(Log structure merge)

SSTable

日誌結構存儲按照寫入的順序排列,文件中鍵值是無序的。

如果我們在合併(壓縮)日誌文件的時候按照鍵值對的序列排序,同時要求每個鍵只在合併的段文件中出現一次。這個合併的格式稱爲排序字符串表sorted string table)。

那麼這樣帶來的好處是:

  1. 合併段文件會是簡單而高效的。從低到高依次複製,新的段文件也是按鍵排序的。

合併段文件

  1. 內存中的索引不用在存儲所有的鍵了,由於鍵是排序的,所以只需要一個區間,就可以確認數據的大致位置,然後從對應的文件偏移位置開始掃描,直到找到,或者壓根兒不存在。如果所有的鍵和值都是定長的,二分查找你值得擁有
構建和維護SSTable

上面看見了 SSTable 帶來的好處,接下來我們看看如何以極小的代價構建SSTable。

首先數據寫入可能是任意順序,所以我們需要在寫入時

  • 在內存維護一個有序的數據結構(例如紅黑樹)稱爲內存表( memtable
  • 當內存表超過一定的大小後,將其作爲 SSTable 文件寫入磁盤。這個 SSTable 是數據庫的最新部分。同時寫入可以請求一個新的內存表實例
  • 讀取請求會先在內存表中獲取,然後在排好序的段文件中,依次查找
  • 同時後臺也會運行合併和壓縮段文件,丟棄刪除的值、合併重複的值
數據庫崩潰如何恢復?

現在遇到新的問題是,數據庫崩潰的話,最近寫入內存表的內容都將丟失。爲了解決這個問題,通常在磁盤上保存一個單獨的日誌,每次寫入都會立即附加到該日誌中,該日誌的目的是崩潰後恢復內存表(是不是想起了 redolog,這個日誌的作用就是崩潰恢復 );當內存表中的內容寫到 SSTable 中時,相應的日誌都可以丟棄了。

如果查詢不存在的鍵呢?

首先會去內存表查詢,然後依次讀取到最老的段中,才能確定鍵不存在,這種訪問的效率太低了。

通常會使用額外的 Bloom 過濾器 ,bloom 過濾器會告訴客戶端這個鍵是否在數據庫中出現過,從而節省不必要的磁盤讀取操作

以上就是LSM的基本思想:保存一系列在後臺合併的 SSTable,由於數據是按照順序存儲,因此可以執行高效的範圍查詢。而磁盤寫入是連續的,所以 LSM 樹可以有很高的吞吐量。

B樹

剛剛的日誌結構索引,在非關係型數據庫有很多體現,作者提到的有 BigTable、HBASE LevelDB、RocksDB等。

然而我們更常見的索引實現是 B 樹(或者 B+樹)。B樹也是保持按鍵排序的鍵值對(允許高效的鍵值查詢和範圍查詢),B樹將數據庫分解爲固定大小的塊(block)或頁(page),磁盤也被安排爲固定大小的塊,所以這種設計更接近底層。每個頁面都持有另一些頁面的引用,類似於指針,但在磁盤上而不是內存中。
B樹存儲

在 B樹中查找一個鍵

從一個根頁面開始,每個子頁面負責一段連續範圍的鍵,引用之間的鍵,指向子頁面的地址,並且同時也知道了子頁面的鍵範圍。

分支因子:頁面中包含對子頁面的引用的數量。通常是幾百個。

在 B樹中插入一個新鍵

首先需要找到一個頁面,其範圍包含新鍵,並將其添加到該頁面中,如果頁面已經沒有空間了,則將這個頁面分裂爲兩個頁面,然後將其插入其中一個子頁面,並且需要更新父頁面對子頁面的引用。

插入新數據

由於B樹的底層操作是用新數據覆蓋磁盤上的數據,並且需要保證該頁面的所有引用完整。

可以認爲這是實際的硬件操作,移動磁頭到正確的位置,等待盤面的正確位置,然後覆蓋新的數據。

實際上會比上面的描述更復雜,因爲在有新數據插入時,可能會產生頁分裂,需要更新父頁面對子頁面的引用,如果部分寫入後系統崩潰,那麼就可能導致數據損壞。爲了使數據庫做到異常安全,通常會添加一個 預寫式日誌(write ahead log)或者稱爲重做日誌 (redo log),每個修改都會被追加到這個日誌文件中,讓數據庫可以在崩潰後恢復。

B 樹 VS LSM 樹

  • B樹索引必須至少兩次寫入每一段數據,一次 WAL 日誌,一次樹頁面本身,即使只更新了幾個字節,也會將整個頁面覆蓋。日誌結構索引會反覆壓縮合並 SSTable,並且重寫數據,在寫入繁重時,存儲引擎的寫入會導致應用的性能降低,磁盤的可用帶寬減少。

  • 通常 LSM 樹擁有比B樹更高的寫入吞吐量。部分原因是順序寫入 SSTable 文件,而不是必須要覆蓋 B樹的某個頁面;在硬盤上順序寫比隨機寫要快的多

  • LSM 樹可以被壓縮的很好,B樹由於刪除或者頁分裂產生一些未使用的磁盤空間。

  • 日誌存儲結構的壓縮過程會影響正在進行的讀寫操作。存儲引擎嘗試逐步執行壓縮而不影響併發訪問,但是磁盤資源有限。而 B樹的行爲可預測性更高。

    同時在數據庫數據量很小時,壓縮執行的很快,當數據量越來越大時,壓縮所需的帶寬就越多。

  • B樹可以提供強大的事務語義。

最後作者提到了其他的索引結構。

將值也存在索引中
  • 例如 MYSQL 的InNoDB 存儲引擎中,表的主鍵是一個聚簇索引,二級索引使用主鍵。

  • 包含列的索引覆蓋索引),索引的數據覆蓋了查詢

多列索引
  • 連接索引,它通過將一列的值追加到另一列後面,簡單地將多個字段組合成一個鍵(索引定義中指定了字段的連接順序)。
  • 多維索引,例如用戶在地圖查看餐館時,需要搜索出正在查看區域內的所有餐館,這需要一個二維查詢;通常使用 R樹實現。
  • 全文搜索和模糊索引,模糊查詢需要不同的技術,見 Lucene

內存數據庫

在內存中存儲一切

在線分析和在線事務

這是本章的最後一節,描述了訪問數據的模式:交互式的,根據用戶的的輸入和操作,更新或插入數據,稱爲在線事務處理(OLTP);掃描大量的記錄,計算彙總統計信息,提供給幫助管理層做出更好決策的報告,稱爲在線分析處理(OLAP)

並介紹了數據倉庫,數據倉庫通常是一個單獨的數據庫,分析人員掃描大量的數據集,進行他們的分析,而不影響 OLTP的工作。數據倉庫中包含了 OLTP 的系統的只讀副本,通過定期的從 OLTP 數據庫中提取數據,並轉換爲適合分析的模式存儲在數據倉庫中。

數據倉庫的查詢通常會掃描很大的數據集,取出其中部分列,進行聚合計算,OLTP通常針對事務處理有很好的支持,所以兩者的查詢模式有很大不同。並且數倉的存儲的數據可能非常巨大,幾十 PB 級別,如何高效的存儲和訪問?

列存儲

OLTP 數據庫中,存儲都是以的方式進行佈局,每一行的所有列都相鄰存儲。同時查詢時,面向行的存儲引擎會查詢出所有列,在內存中解析並過濾。

面向列的存儲,不將每一行的數據存儲在一起,而將每一列的數據存儲在一起。將每一列的數據放在單獨的文件,查詢只需要讀取指定的列。

列壓縮,大量的列數據可能非常相似,這爲數據壓縮帶來了可能,數據壓縮就可以進一步降低消耗磁盤帶寬。

列存儲的排序,每列單獨排序並沒有太大的意義,這樣就不知道列中哪些項是屬於同一行,所以仍然要對一整行進行排序,通過選擇常用的查詢列進行排序。例如以日常範圍,那麼查詢某個月的數據,就會很快。排序另一個好處是幫助列壓縮。

以及聚合,常用的COUNT,SUM,AVG,MAX 等待查詢函數經常被其他的查詢使用,不如將其緩存起來加快其他 SQL 的查詢。這稱爲物化視圖。當數據更新時物化視圖也需要同步更新。

總結

本章主要講了存儲和檢索方式,包括 LSM樹(日誌結構索引)僅追加寫入和 B 樹面向頁面的原地更新。都是面向 OLTP 的事務處理的優化。在線分析(OLAP)則擁有不同的訪問和存儲模式,由於分析通常需要掃描數百萬條記錄,並且往往只需要查詢少量的列,那麼磁盤帶寬則成爲了瓶頸,所以列存儲變的越來越流行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章