本節咱們將全面瞭解一下 LevelDB 的各類特性。LevelDB 的開發語言是 C++,考慮到會使用 C++ 語言的同窗不是不少,在本節咱們將使用 Java 語言來描述 LevelDB 的特性。其它語言棧的同窗也沒必要擔憂,由於不一樣語言操縱 LevelDB 的接口 API 都是同樣的,使用起來大同小異。java
LevelDB 的數據存儲在一個特定的目錄中,裏面有不少數據文件、日誌文件等。使用 LevelDB API 來打開這個目錄,就獲得了 db 的引用。後續咱們就使用這個 db 引用來執行讀寫操做。下面的代碼是 Java 語言描述的僞代碼。算法
class LevelDB {
public static LevelDB open(String dbDir, Options options);
void close(); // 關閉數據庫
}
複製代碼
打開數據庫有不少選項能夠配置,好比設置塊緩存大小、壓縮等數據庫
LevelDB 用起來就像 HashMap,可是比 HashMap 要稍微弱一些,由於 put 方法不能返回舊值,delete 操做也不知道對應的 key 是否真的存在。緩存
class LevelDB {
byte[] get(byte[] key)
void put(byte[] key, byte[] value) void delete(byte[] key) ... } 複製代碼
對於多個連續的寫操做若是由於宕機有可能致使這多個連續的寫操做只完成了一部分。爲此 LevelDB 提供了批處理功能,批處理操做就比如事務,LevelDB 確保這一些列寫操做的原子性執行,要麼所有生效要麼徹底不生效。安全
class WriteBatch {
void put(byte[] key, byte[] value);
void delete(byte[] key);
}
class LevelDB {
...
void write(WriteBatch wb);
}
複製代碼
當咱們調用 LevelDB 的 put 方法往庫裏寫數據時,它會先將數據記錄到內存中,延後再經過某種特殊的策略持久化到磁盤。這就存在一個問題,若是突發宕機,這些來不及寫到磁盤的數據就丟失了。因此 LevelDB 也採用了和 Redis AOF 日誌相似的策略,先講修改操做的日誌寫到磁盤文件中,再進行實際的寫操做流程處理。bash
如此即便宕機發生了,數據庫啓動時還能夠經過日誌文件來恢復。多線程
瞭解 Redis 的同窗都知道它的 AOF 寫策略有多種配置,取決於日誌文件同步磁盤的頻率。頻率越高,遇到宕機時丟失的數據就越少。操做系統要將內核中文件的髒數據同步到磁盤須要進行磁盤 IO,這會影響訪問性能,因此一般都不會同步的太頻繁。併發
LevelDB 也是相似的,若是使用前面的非安全寫,雖然 API 調用成功了,可是遇到宕機問題,有可能對應的操做日誌會丟失。因此它提供了安全寫操做,代價就是性能會變差。app
class LevelDB {
...
void putSync(byte[] key, byte[] value);
void deleteSync(byte[] key);
void writeSync(WriteBatch wb);
}
複製代碼
在安全和性能之間每每須要折中,因此一般咱們會定時若干毫秒或者每隔若干寫操做使用一次同步寫。這樣能夠在兼顧寫性能的同時儘可能少丟失數據。函數
LevelDB 的磁盤文件會放在一個文件目錄中,裏面有不少相關的數據和日誌文件。它不支持多進程同時打開這個目錄來使用 LevelDB API 進行讀寫訪問。可是對於同一個進程 LevelDB API 是支持多線程安全讀寫的。LevelDB 內部會使用特殊的鎖來控制併發操做。
LevelDB 中的 Key 都是有序的,按照字典序從小到大整齊排列。LevelDB 提供了遍歷 API 能夠逐個順序訪問全部的鍵值對,能夠指定從中間開始遍歷。
class LevelDB {
...
Iterator<KV> scan(byte[] startKey, byte[] endKey, int limit);
}
複製代碼
LevelDB 支持多線程併發讀寫,這意味着連續的兩個一樣 key 的讀操做讀到的數據可能不同,由於兩個讀操做中間數據可能被其它線程修改了。這在數據庫理論中稱爲「重複讀」。LevelDB 提供了快照隔離機制,在同一個快照範圍內保證連續的讀寫操做不受其它線程修改操做的影響。
class Snapshot {
byte[] get(byte[] key)
void put(byte[] key, byte[] value) void delete(byte[] key) void write(WriteBatch wb);
...
void close(); // 關閉快照
}
class LevelDB {
...
Snapshot getSnapshot();
}
複製代碼
快照雖然很神奇,可是實際上它的原理很是簡單,這個咱們後文再深刻講解。
LevelDB 的 key 默認使用字典序,不過它也提供了自定義排序規則。你能夠自定義一個排序函數註冊進去,好比按數字排序。必須儘量確保排序規則在整個數據庫生命週期內保持不變,由於排序會影響到磁盤鍵值對的存儲順序,磁盤存儲順序是沒法動態改變的。
Options options = new Options();
options.comparator = new CustomComparator();
db = LevelDB.open("/tmp/ldb", options);
複製代碼
自定義比較器很危險,謹慎使用。比較算法設置不當,會嚴重影響到存儲效率。若是確實必需要改變排序規則,那就須要提早規劃,這裏會有一個特別的小技巧,理解它須要瞭解磁盤存儲的細節,因此咱們後續再仔細探討。
LevelDB 的磁盤數據是以數據庫塊的形式存儲的,默認的塊大小是 4k。適當提高塊大小將有益於批量大規模遍歷操做的效率,若是隨機讀比較頻繁,這時候塊小點性能又會稍好,這就要求咱們本身去折中選擇。
Options options = new Options();
options.blockSize = 8092;
db = LevelDB.open("/tmp/ldb", options);
複製代碼
塊不宜太小低於 1k,也不宜過大設置成了好幾 M,這樣過激的設置並不會給性能帶來多大的提高,反而會大幅增長數據庫在不一樣的讀寫場合的性能波動。咱們要選擇中庸之道,在默認塊大小周邊浮動。塊大小一經初始化就不可再次更改。
LevelDB 的磁盤存儲默認是開啓壓縮的,是業界經常使用的 Snappy 算法,壓縮效率很是高,因此無需擔憂性能損耗問題。若是你不想使用壓縮,也能夠動態關閉。關閉壓縮開關一般不會帶來明顯的性能提高,因此咱們儘量不要去動它。
Options options = new Options();
options.compression = CompressionType.kSnappyCompression;
// options.compression = CompressionType.kNoCompression; // 關閉壓縮
db = LevelDB.open("/tmp/ldb", options);
複製代碼
LevelDB 的內存中存儲了一筆最近讀寫的熱數據,若是請求的數據在熱數據中查不到就須要去磁盤文件中去查找,效率就會大幅下降。LevelDB 爲了下降磁盤文件的搜尋次數,增長了塊緩存,緩存了近期頻繁使用的數據塊解壓縮以後的內容。
Options options = new Options();
options.blockCache = LevelDB.NewLRUCache(100 * 1024 * 1024); // 100M
db = LevelDB.open("/tmp/ldb", options);
複製代碼
默認塊緩存不開啓,打開數據庫時能夠手動設置選項。塊緩存會佔據一部份內存,不過這一般不須要設置太大,100M 左右就差很少了,再大一些效率提高的也不明顯了。
還須要注意遍歷操做對緩存的影響,爲了不遍歷操做將不少冷門數據刷到塊緩存中,能夠在遍歷的時候設置一個選項 fill_cache,它用來控制磁盤遍歷的數據塊是否須要同步到緩存。
內存讀 miss 致使磁盤搜尋是一個比較耗時的操做,LevelDB 爲了進一步減小磁盤讀的次數,在每一個磁盤文件上又加了一層布隆過濾器,它須要消耗必定的磁盤空間,可是在效果上能夠直接將磁盤讀次數大幅減小。布隆過濾器的數據存儲在磁盤文件中數據塊的後面。
LevelDB 的磁盤文件是分層存儲的,它會先去 Level 0 查找,若是找不到繼續去 Level 1 去找,一直遞歸到最底層。因此若是你去找一個不存在的 key,就須要不少次磁盤文件讀操做,會很是耗費時間。而布隆過濾器能夠幫你省去95%以上的磁盤文件搜尋的時間。
布隆過濾器相似於一個內存 Set 結構,它裏面存儲了指定磁盤文件必定範圍內全部 Key 的指紋信息。當它發現某個 key 的指紋在 Set 集合裏找不到,它就能夠判定這個 key 確定不存在。
若是對應的指紋能夠在集合裏找到,這並不能肯定它就必定存在。由於不一樣的 Key 可能會生成一樣的指紋,這就是布隆過濾器的誤判率。誤判率越低須要的 Key 指紋信息越多,對應消耗的內存空間也就越大。
若是布隆過濾器能準確知道某個 Key 是否存在,那就不存在誤判了,這時候也就不會存在白白浪費的磁盤讀操做。這樣的極限形式的布隆過濾器就是 HashSet —— 內存裏存儲了全部的 Key,固然內存空間天然是沒法接受的。
Options options = new Options();
// 每一個 key 的指紋大小是 10bit
options.filterPolicy = LevelDB.NewBloomFilterPolicy(10);
db = LevelDB.open("/tmp/ldb", options);
複製代碼
在使用布隆過濾器時,咱們須要在內存消耗和性能之間作一個折中選擇。若是你想深刻理解布隆過濾器的原理,能夠去看《Redis 深度歷險》,裏面有一個單獨的章節專門講解布隆過濾器的內部原理。
默認布隆過濾器沒有打開,須要在打開數據庫的時候設置 filter_policy 參數才能夠生效。布隆過濾器是減小磁盤讀操做的最後一層堡壘。布隆過濾器內部的位圖數據會存儲在磁盤文件中,可是使用是會緩存在內存裏面。
LevelDB 有嚴格的數據校驗機制,它將校驗的單位精確到了 4K 字節的數據塊。校驗和會浪費一點存儲空間和計算時間,可是在遇到數據塊損壞時能夠較爲精確地恢復健康的數據。
class LevelDB {
...
public void static repairDB(String dbDir, Options options);
}
複製代碼
打開數據庫時默認沒有開啓強制校驗選項,若是開啓了,在遇到校驗錯誤時就會報錯。若是數據真的出現了問題,LevelDB 還提供了修復數據的方法 repairDB() 能夠幫咱們恢復儘量多的數據。
通過了這一節的學習,同窗們應該能夠在腦海中造成下面這樣一張概念圖。圖中的「熱數據」是指最近被修改的鍵值對,這裏面的鍵值對讀取速度是最爲快速的。若是熱數據中讀取不到,就會去塊緩存中讀取。若是還讀不到,就分兩種狀況,一種是真的不存在,另外一個種是存在於磁盤上。若是存在於磁盤上,通過有限層次讀取就讀取到了,一般越冷的數據越在底層。若是真的不存在就要通過布隆過濾器來大幅減小磁盤搜尋 IO,布隆過濾器的數據和鍵值對數據共同放在分層的數據文件中。
下一節咱們使用真實的代碼來親自實踐一下 LevelDB。