溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

LevelDB的功能特性是什么

發(fā)布時間:2022-01-17 09:52:17 來源:億速云 閱讀:135 作者:iii 欄目:數(shù)據(jù)庫

本文小編為大家詳細介紹“LevelDB的功能特性是什么”,內(nèi)容詳細,步驟清晰,細節(jié)處理妥當,希望這篇“LevelDB的功能特性是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

打開和關(guān)閉

LevelDB 的數(shù)據(jù)存儲在一個特定的目錄中,里面有很多數(shù)據(jù)文件、日志文件等。使用 LevelDB API 來打開這個目錄,就得到了 db 的引用。后續(xù)我們就使用這個 db 引用來執(zhí)行讀寫操作。下面的代碼是 Java 語言描述的偽代碼。

class LevelDB {
  public static LevelDB open(String dbDir, Options options);
  void close(); // 關(guān)閉數(shù)據(jù)庫
}


打開數(shù)據(jù)庫有很多選項可以配置,比如設(shè)置塊緩存大小、壓縮等

基礎(chǔ) API

LevelDB 用起來就像 HashMap,但是比 HashMap 要稍微弱一些,因為 put 方法不能返回舊值,delete 操作也不知道對應(yīng)的 key 是否真的存在。

class LevelDB {
  byte[] get(byte[] key)
  void put(byte[] key, byte[] value)
  void delete(byte[] key)
  ...
}

原子批處理

對于多個連續(xù)的寫操作如果因為宕機有可能導(dǎo)致這多個連續(xù)的寫操作只完成了一部分。為此 LevelDB 提供了批處理功能,批處理操作就好比事務(wù),LevelDB 確保這一些列寫操作的原子性執(zhí)行,要么全部生效要么完全不生效。

class WriteBatch {
  void put(byte[] key, byte[] value);
  void delete(byte[] key);
}

class LevelDB {
  ...
  void write(WriteBatch wb);
}

日志文件

當我們調(diào)用 LevelDB 的 put 方法往庫里寫數(shù)據(jù)時,它會先將數(shù)據(jù)記錄到內(nèi)存中,延后再通過某種特殊的策略持久化到磁盤。這就存在一個問題,如果突發(fā)宕機,這些來不及寫到磁盤的數(shù)據(jù)就丟失了。所以 LevelDB 也采用了和 Redis AOF 日志類似的策略,先講修改操作的日志寫到磁盤文件中,再進行實際的寫操作流程處理。

如此即使宕機發(fā)生了,數(shù)據(jù)庫啟動時還可以通過日志文件來恢復(fù)。

安全寫(同步寫)

了解 Redis 的同學(xué)都知道它的 AOF 寫策略有多種配置,取決于日志文件同步磁盤的頻率。頻率越高,遇到宕機時丟失的數(shù)據(jù)就越少。操作系統(tǒng)要將內(nèi)核中文件的臟數(shù)據(jù)同步到磁盤需要進行磁盤 IO,這會影響訪問性能,所以通常都不會同步的太頻繁。

LevelDB 也是類似的,如果使用前面的非安全寫,雖然 API 調(diào)用成功了,但是遇到宕機問題,有可能對應(yīng)的操作日志會丟失。所以它提供了安全寫操作,代價就是性能會變差。

class LevelDB {
  ...
  void putSync(byte[] key, byte[] value);
  void deleteSync(byte[] key);
  void writeSync(WriteBatch wb);
}


在安全和性能之間往往需要折中,所以通常我們會定時若干毫秒或者每隔若干寫操作使用一次同步寫。這樣可以在兼顧寫性能的同時盡量少丟失數(shù)據(jù)。

并發(fā)

LevelDB 的磁盤文件會放在一個文件目錄中,里面有很多相關(guān)的數(shù)據(jù)和日志文件。它不支持多進程同時打開這個目錄來使用 LevelDB API 進行讀寫訪問。但是對于同一個進程 LevelDB API 是支持多線程安全讀寫的。LevelDB 內(nèi)部會使用特殊的鎖來控制并發(fā)操作。

遍歷

LevelDB 中的 Key 都是有序的,按照字典序從小到大整齊排列。LevelDB 提供了遍歷 API 可以逐個順序訪問所有的鍵值對,可以指定從中間開始遍歷。

class LevelDB {
  ...
  Iterator<KV> scan(byte[] startKey, byte[] endKey, int limit);
}

快照隔離

LevelDB 支持多線程并發(fā)讀寫,這意味著連續(xù)的兩個同樣 key 的讀操作讀到的數(shù)據(jù)可能不一樣,因為兩個讀操作中間數(shù)據(jù)可能被其它線程修改了。這在數(shù)據(jù)庫理論中稱為「重復(fù)讀」。LevelDB 提供了快照隔離機制,在同一個快照范圍內(nèi)保證連續(xù)的讀寫操作不受其它線程修改操作的影響。

class Snapshot {
  byte[] get(byte[] key)
  void put(byte[] key, byte[] value)
  void delete(byte[] key)
  void write(WriteBatch wb);
  ...
  void close();  // 關(guān)閉快照
}

class LevelDB {
  ...
  Snapshot getSnapshot();
}


快照雖然很神奇,但是實際上它的原理非常簡單,這個我們后文再深入講解。

自定義 Key 比較器

LevelDB 的 key 默認使用字典序,不過它也提供了自定義排序規(guī)則。你可以自定義一個排序函數(shù)注冊進去,比如按數(shù)字排序。必須盡可能確保排序規(guī)則在整個數(shù)據(jù)庫生命周期內(nèi)保持不變,因為排序會影響到磁盤鍵值對的存儲順序,磁盤存儲順序是無法動態(tài)改變的。

Options options = new Options();
options.comparator = new CustomComparator();
db = LevelDB.open("/tmp/ldb", options);


自定義比較器很危險,謹慎使用。比較算法設(shè)置不當,會嚴重影響到存儲效率。如果確實必須要改變排序規(guī)則,那就需要提前規(guī)劃,這里會有一個特別的小技巧,理解它需要了解磁盤存儲的細節(jié),所以我們后續(xù)再仔細探討。

數(shù)據(jù)塊

LevelDB 的磁盤數(shù)據(jù)是以數(shù)據(jù)庫塊的形式存儲的,默認的塊大小是 4k。適當提升塊大小將有益于批量大規(guī)模遍歷操作的效率,如果隨機讀比較頻繁,這時候塊小點性能又會稍好,這就要求我們自己去折中選擇。

Options options = new Options();
options.blockSize = 8092;
db = LevelDB.open("/tmp/ldb", options);


塊不宜過小低于 1k,也不宜過大設(shè)置成了好幾 M,這樣過激的設(shè)置并不會給性能帶來多大的提升,反而會大幅增加數(shù)據(jù)庫在不同的讀寫場合的性能波動。我們要選擇中庸之道,在默認塊大小周邊浮動。塊大小一經(jīng)初始化就不可再次更改。

壓縮

LevelDB 的磁盤存儲默認是開啟壓縮的,是業(yè)界常用的 Snappy 算法,壓縮效率非常高,所以無需擔心性能損耗問題。如果你不想使用壓縮,也可以動態(tài)關(guān)閉。關(guān)閉壓縮開關(guān)通常不會帶來明顯的性能提升,所以我們盡可能不要去動它。

Options options = new Options();
options.compression = CompressionType.kSnappyCompression;
// options.compression = CompressionType.kNoCompression; // 關(guān)閉壓縮
db = LevelDB.open("/tmp/ldb", options);

塊緩存

LevelDB 的內(nèi)存中存儲了一筆最近讀寫的熱數(shù)據(jù),如果請求的數(shù)據(jù)在熱數(shù)據(jù)中查不到就需要去磁盤文件中去查找,效率就會大幅降低。LevelDB 為了降低磁盤文件的搜尋次數(shù),增加了塊緩存,緩存了近期頻繁使用的數(shù)據(jù)塊解壓縮之后的內(nèi)容。

Options options = new Options();
options.blockCache = LevelDB.NewLRUCache(100 * 1024 * 1024); // 100M
db = LevelDB.open("/tmp/ldb", options);


默認塊緩存不開啟,打開數(shù)據(jù)庫時可以手動設(shè)置選項。塊緩存會占據(jù)一部分內(nèi)存,不過這通常不需要設(shè)置太大,100M 左右就差不多了,再大一些效率提升的也不明顯了。

還需要注意遍歷操作對緩存的影響,為了避免遍歷操作將很多冷門數(shù)據(jù)刷到塊緩存中,可以在遍歷的時候設(shè)置一個選項 fill_cache,它用來控制磁盤遍歷的數(shù)據(jù)塊是否需要同步到緩存。

布隆過濾器

內(nèi)存讀 miss 導(dǎo)致磁盤搜尋是一個比較耗時的操作,LevelDB 為了進一步減少磁盤讀的次數(shù),在每個磁盤文件上又加了一層布隆過濾器,它需要消耗一定的磁盤空間,但是在效果上可以直接將磁盤讀次數(shù)大幅減少。布隆過濾器的數(shù)據(jù)存儲在磁盤文件中數(shù)據(jù)塊的后面。

LevelDB 的磁盤文件是分層存儲的,它會先去 Level 0 查找,如果找不到繼續(xù)去 Level 1 去找,一直遞歸到最底層。所以如果你去找一個不存在的 key,就需要很多次磁盤文件讀操作,會非常耗費時間。而布隆過濾器可以幫你省去95%以上的磁盤文件搜尋的時間。

布隆過濾器類似于一個內(nèi)存 Set 結(jié)構(gòu),它里面存儲了指定磁盤文件一定范圍內(nèi)所有 Key 的指紋信息。當它發(fā)現(xiàn)某個 key 的指紋在 Set 集合里找不到,它就可以斷定這個 key 肯定不存在。

如果對應(yīng)的指紋可以在集合里找到,這并不能確定它就一定存在。因為不同的 Key 可能會生成同樣的指紋,這就是布隆過濾器的誤判率。誤判率越低需要的 Key 指紋信息越多,對應(yīng)消耗的內(nèi)存空間也就越大。

如果布隆過濾器能準確知道某個 Key 是否存在,那就不存在誤判了,這時候也就不會存在白白浪費的磁盤讀操作。這樣的極限形式的布隆過濾器就是 HashSet —— 內(nèi)存里存儲了所有的 Key,當然內(nèi)存空間自然是無法接受的。

Options options = new Options();
// 每個 key 的指紋大小是 10bit
options.filterPolicy = LevelDB.NewBloomFilterPolicy(10);
db = LevelDB.open("/tmp/ldb", options);


在使用布隆過濾器時,我們需要在內(nèi)存消耗和性能之間做一個折中選擇。如果你想深入理解布隆過濾器的原理,可以去看《Redis 深度歷險》,里面有一個單獨的章節(jié)專門講解布隆過濾器的內(nèi)部原理。

默認布隆過濾器沒有打開,需要在打開數(shù)據(jù)庫的時候設(shè)置 filter_policy 參數(shù)才可以生效。布隆過濾器是減少磁盤讀操作的最后一層堡壘。布隆過濾器內(nèi)部的位圖數(shù)據(jù)會存儲在磁盤文件中,但是使用是會緩存在內(nèi)存里面。

數(shù)據(jù)校驗

LevelDB 有嚴格的數(shù)據(jù)校驗機制,它將校驗的單位精確到了 4K 字節(jié)的數(shù)據(jù)塊。校驗和會浪費一點存儲空間和計算時間,但是在遇到數(shù)據(jù)塊損壞時可以較為精確地恢復(fù)健康的數(shù)據(jù)。

class LevelDB {
  ...
  public void static repairDB(String dbDir, Options options);
}


打開數(shù)據(jù)庫時默認沒有開啟強制校驗選項,如果開啟了,在遇到校驗錯誤時就會報錯。如果數(shù)據(jù)真的出現(xiàn)了問題,LevelDB 還提供了修復(fù)數(shù)據(jù)的方法 repairDB() 可以幫我們恢復(fù)盡可能多的數(shù)據(jù)。

讀到這里,這篇“LevelDB的功能特性是什么”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI