本文小編為大家詳細介紹“LevelDB的功能特性是什么”,內(nèi)容詳細,步驟清晰,細節(jié)處理妥當(dāng),希望這篇“LevelDB的功能特性是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。
成都創(chuàng)新互聯(lián)長期為成百上千家客戶提供的網(wǎng)站建設(shè)服務(wù),團隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為鹿城企業(yè)提供專業(yè)的成都網(wǎng)站設(shè)計、網(wǎng)站制作,鹿城網(wǎng)站改版等技術(shù)服務(wù)。擁有10年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。
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è)置塊緩存大小、壓縮等
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);
}
當(dāng)我們調(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ù)。
LevelDB 的磁盤文件會放在一個文件目錄中,里面有很多相關(guān)的數(shù)據(jù)和日志文件。它不支持多進程同時打開這個目錄來使用 LevelDB API 進行讀寫訪問。但是對于同一個進程 LevelDB API 是支持多線程安全讀寫的。LevelDB 內(nèi)部會使用特殊的鎖來控制并發(fā)操作。
LevelDB 中的 Key 都是有序的,按照字典序從小到大整齊排列。LevelDB 提供了遍歷 API 可以逐個順序訪問所有的鍵值對,可以指定從中間開始遍歷。
class LevelDB {
...
Iterator 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();
}
快照雖然很神奇,但是實際上它的原理非常簡單,這個我們后文再深入講解。
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è)置不當(dāng),會嚴重影響到存儲效率。如果確實必須要改變排序規(guī)則,那就需要提前規(guī)劃,這里會有一個特別的小技巧,理解它需要了解磁盤存儲的細節(jié),所以我們后續(xù)再仔細探討。
LevelDB 的磁盤數(shù)據(jù)是以數(shù)據(jù)庫塊的形式存儲的,默認的塊大小是 4k。適當(dāng)提升塊大小將有益于批量大規(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 算法,壓縮效率非常高,所以無需擔(dān)心性能損耗問題。如果你不想使用壓縮,也可以動態(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 的指紋信息。當(dāng)它發(fā)現(xiàn)某個 key 的指紋在 Set 集合里找不到,它就可以斷定這個 key 肯定不存在。
如果對應(yīng)的指紋可以在集合里找到,這并不能確定它就一定存在。因為不同的 Key 可能會生成同樣的指紋,這就是布隆過濾器的誤判率。誤判率越低需要的 Key 指紋信息越多,對應(yīng)消耗的內(nèi)存空間也就越大。
如果布隆過濾器能準確知道某個 Key 是否存在,那就不存在誤判了,這時候也就不會存在白白浪費的磁盤讀操作。這樣的極限形式的布隆過濾器就是 HashSet —— 內(nèi)存里存儲了所有的 Key,當(dāng)然內(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)存里面。
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)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。