Leveldb 使用說明文檔
原作者:Jeff Dean, Sanjay Ghemawat
翻譯:烏合之眾solym@sohu.com
英文原文地址https://rawgit.com/google/leveldb/master/doc/index.html
leveldb庫提供持久性鍵值存儲。 鍵和值可以是任意字節數組。 根據用戶指定的比較函數,在鍵值存儲器內對鍵進行排序。
打開一個數據庫
leveldb打開數據庫需要指定一個文件系統目錄。 數據庫的所有內容都存儲在此目錄中。 以下示例顯示如何打開數據庫,如有必要,可創建該數據庫:
#include <cassert>
#include "leveldb/db.h"
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
assert(status.ok());
...
如果要在數據庫 已存在時返回錯誤 ,請在leveldb::DB::Open
調用之前添加以下行:
options.error_if_exists = true;
狀態 Status
你也許已經注意到了上面的leveldb::Status
類型。leveldb中可能遇到錯誤的大多數函數都返回此類型的值。 您可以檢查這樣的結果是否正確,並打印相關的錯誤消息:
leveldb::Status s = ...;
if (!s.ok()) cerr << s.ToString() << endl;
關閉數據庫
使用完數據庫后,只需刪除數據庫對象。 例:
... open the db as described above ...
... do something with db ...
delete db;
讀和寫 Reads And Writes
數據庫提供Put
,Delete
和Get
方法來修改/查詢數據庫。 例如,以下代碼將key1
下存儲的值
移動到key2
。
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) s = db->Put(leveldb::WriteOptions(), key2, value);
if (s.ok()) s = db->Delete(leveldb::WriteOptions(), key1);
原子更新 Atomic Updates
注意,如果進程在Put Key2 之后、Delete key1之前結束(掛了),會使得value被同時保存在多個key下。使用WirteBatch
類進行原子更新一組操作,可以避免這些問題:
#include "leveldb/write_batch.h"
...
std::string value;
leveldb::Status s = db->Get(leveldb::ReadOptions(), key1, &value);
if (s.ok()) {
leveldb::WriteBatch batch;
batch.Delete(key1);
batch.Put(key2, value);
s = db->Write(leveldb::WriteOptions(), &batch);
}
WriteBatch
保存對數據庫進行的一系列編輯,該批次內的這些修改才能被應用。 注意,我們在Put之前調用Delete,因此如果key1與key2相同,我們不會錯誤的完全丟棄該值。
除了它的原子性好處,WriteBatch
也可以用於通過將大量的單個操作
放入同一批次來加速批量更新。
異步寫 Synchronous Writes
默認情況下,每次寫入leveldb都是異步
的:將寫入從進程推送到操作系統后立即返回。從操作系統內存到底層持久存儲的傳輸是異步的。 對於特定的寫操作,可以打開同步標志
,以使寫操作不會返回,直到正在寫入的數據真正寫入永久存儲器。 (在Posix系統上,這是通過在寫操作返回之前調用fsync
(...)或fdatasync
(...)或msync
(...,MS_SYNC)實現的。
leveldb::WriteOptions write_options;
write_options.sync = true;
db->Put(write_options, ...);
異步寫操作通常比同步寫操作快一千倍。 異步寫入的缺點是,機器的崩潰可能導致最后幾個更新丟失。 請注意,只是寫入進程(即不是機器重新啟動)的崩潰不會導致任何損失 ,因為即使sync
為false
,更新從進程內存推送到操作系統在之后即認為完成。
通常可以安全地使用異步寫入。 例如,將大量數據加載到數據庫中時,可以通過在崩潰后重新啟動批量加載來處理丟失的更新。 混合方案也是可能的,其中每第N次寫入是同步的,並且在崩潰的情況下,批量加載剛好在由前一次運行完成的最后同步寫入之后重新啟動。 (同步寫入可以更新描述崩潰時重新啟動位置的標記。)
WriteBatch提供了異步寫入的替代方法。 多個更新可以放置在同一WriteBatch
中,並使用同步寫入(即,write_options.sync
設置為true
)一起應用。 同步寫入的額外成本將在批次中的所有寫入之間攤銷。
並發 Concurrency
數據庫只能由一個進程一次打開。leveldb實現從操作系統獲取鎖
以防止誤用。 在單個進程中,同一個leveldb::DB
對象可以安全地由多個並發線程共享。 即,不同的線程可以寫入或獲取迭代器或在沒有任何外部同步的情況下在同一數據庫上調用Get
(leveldb實現將自動執行所需的同步)。
但是其他對象(如Iterator
和WriteBatch
)可能需要外部同步。如果兩個線程共享這樣的對象,它們必須使用自己的鎖定協議保護對它的訪問。 更多詳細信息在公共頭文件中可用。
迭代器 Iteration
以下示例演示如何在數據庫中打印所有 鍵-值對 。
leveldb::Iterator* it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
cout << it->key().ToString() << ": " << it->value().ToString() << endl;
}
assert(it->status().ok()); // Check for any errors found during the scan
delete it;
以下顯示了如何僅處理范圍[start,limit)中的鍵:
for (it->Seek(start);
it->Valid() && it->key().ToString() < limit;
it->Next()) {
...
}
您還可以按相反的順序處理條目。 (注意:反向迭代可能比前向迭代慢一些。)
for (it->SeekToLast(); it->Valid(); it->Prev()) {
...
}
快照 Snapshots
快照在鍵值存儲的整個狀態上提供一致的只讀視圖。ReadOptions::snapshot
可能是非NULL
的,表示read
操作應該在某個特定版本的DB狀態。 如果ReadOptions::snapshot
為NULL,則讀取操作將對當前狀態的隱式快照
進行操作
快照由DB::GetSnapshot() 方法創建:
leveldb::ReadOptions options;
options.snapshot = db->GetSnapshot();
... apply some updates to db ...
leveldb::Iterator* iter = db->NewIterator(options);
... read using iter to view the state when the snapshot was created ...
delete iter;
db->ReleaseSnapshot(options.snapshot);
請注意,當不再需要快照時,應使用DB :: ReleaseSnapshot接口釋放快照。這允許實現排除維護狀態,只支持從該快照讀取。
分片 Slice
上面it->key()
和it->value()
調用的返回值是leveldb::Slice
類型。Slice是一個簡單的結構,包含一個長度和一個指向外部字節數組的指針。返回Slice
是比返回std::string
的更廉價替代方法,因為我們不需要復制潛在的大key
和value
。此外,leveldb方法不返回以null結束的C風格字符串,因為leveldb鍵和值允許包含\0
字節。
C++ std::string和以null結尾的C風格字符串可以很容易地轉換為Slice:
leveldb::Slice s1 = "hello";
std::string str("world");
leveldb::Slice s2 = str;
一個Slice可以很容易的轉換回一個C++ string:
std::string str = s1.ToString();
assert(str == std::string("hello"));
使用Slice時要小心,因為它取決於調用者,以確保在Slice使用時,Slice所指向的外部字節數組保持有效。 例如,以下是buggy:
leveldb::Slice slice;
if (...) {
std::string str = ...;
slice = str; // str是一個臨時對象,會被析構
}
Use(slice);
當出if語句范圍時,str
將被銷毀,slice
的內指針指向的內存將被釋放。
比較器 Comparators
前面的例子使用了key的默認排序函數,它按字節順序排列
。 然而,您可以在打開數據庫時提供自定義比較器。 例如,假設每個數據庫鍵由兩個數字組成,我們應該按第一個數字排序,用第二個數字斷開關系(這里實際上就是按這兩個數字組合比較大小)。 首先,定義一個表示這些規則的leveldb::Comparator
的適當子類:
class TwoPartComparator : public leveldb::Comparator {
public:
// 三種比較結果:
// if a < b: 結果為負
// if a > b: 結果為正
// else: 結果為0
int Compare(const leveldb::Slice& a, const leveldb::Slice& b) const {
int a1, a2, b1, b2;
ParseKey(a, &a1, &a2);
ParseKey(b, &b1, &b2);
if (a1 < b1) return -1;
if (a1 > b1) return +1;
if (a2 < b2) return -1; // a1==b1
if (a2 > b2) return +1; // a1==b1
return 0;
}
// Ignore the following methods for now:
const char* Name() const { return "TwoPartComparator"; }
void FindShortestSeparator(std::string*, const leveldb::Slice&) const { }
void FindShortSuccessor(std::string*) const { }
};
現在創建一個數據庫,使用自定義的比較器comparator:
TwoPartComparator cmp;
leveldb::DB* db;
leveldb::Options options;
options.create_if_missing = true;
options.comparator = &cmp;
leveldb::Status status = leveldb::DB::Open(options, "/tmp/testdb", &db);
...
后向兼容性 Backwards compatibility
比較器的Name
方法的結果在創建時附加到數據庫,並在每次后續數據庫打開時檢查。 如果Name
更改,leveldb::DB::Open
調用將失敗。
因此,當且僅當新的key格式
和比較函數
(comparison function)與現有數據庫不兼容,並且可以丟棄
所有現有數據庫的內容時,可對Name
修改。
然而,您仍然可以通過一些預先計划逐漸演變您的key格式。例如,您可以在每個鍵的末尾存儲一個版本號(一個字節應該足矣滿足大多數用途)。
當您希望切換到新的鍵格式(例如,在TwoPartComparator處理的鍵添加一個可選的第三部分),
(a)保持相同的比較器名稱
(b)增加新鍵的版本號
(c)更改比較器函數,以便它根據在key中找到的版本號決定如何解析它們。
性能 Performance
性能可以通過改變在include/leveldb/options.h
定義的類型的默認值進行調整。
塊大小 Block size
leveldb
將相鄰的鍵
組合在一起成為同一個塊,並且這樣的塊訪問持久存儲器的傳送單元(一次讀寫大小)。
默認塊大小約為4096
個未壓縮字節。 如果應用程序主要對數據庫內容進行批量掃描,可能希望增加此大小。性能測量表明有改進,那么對小value
進行大量定位讀取的應用程序可能希望切換到較小的塊大小。 使用小於1KB或大於幾MB的塊沒有太大的好處。 還要注意,壓縮對於更大的塊大小將更有效。
壓縮 Compression
每個塊在被寫入永久存儲器之前被單獨壓縮。 壓縮默認情況下處於打開狀態,因為默認壓縮方法非常快,並且會自動禁用不可壓縮數據。 在極少數情況下,如果應用程序可能希望完全禁用壓縮,應該這樣做,基准顯示性能改進:
leveldb::Options options;
options.compression = leveldb::kNoCompression;
... leveldb::DB::Open(options, name, ...) ....
緩存 Cache
數據庫的內容存儲在文件系統中的一組文件中,並且每個文件存儲壓縮塊的序列。 如果options.cache為非NULL,它用於緩存常用的未壓縮塊內容。
#include "leveldb/cache.h"
leveldb::Options options;
options.cache = leveldb::NewLRUCache(100 * 1048576); // 100MB cache
leveldb::DB* db;
leveldb::DB::Open(options, name, &db);
... use the db ...
delete db
delete options.cache;
請注意,高速緩存保存未壓縮數據,因此應根據應用程序級數據大小確定大小,而壓縮不會對其產生任何影響。(壓縮塊的緩存由操作系統緩沖區緩存或客戶端提供的自定義Env實現)。
當執行批量讀取時,應用程序可能希望禁用高速緩存,使得由批量讀取處理的數據不會最終取代大部分高速緩存的內容。 設置每個迭代器選項可以實現這一點:
leveldb::ReadOptions options;
options.fill_cache = false;
leveldb::Iterator* it = db->NewIterator(options);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
...
}
鍵布局 Key Layout
注意,磁盤傳輸和高速緩存以一個塊(Block
)為單位。 相鄰的鍵(根據數據庫排序順序)通常放置在同一個塊中。 因此,應用程序可以通過放置彼此靠近在一起的鍵,並將不常使用的鍵放置在鍵空間的單獨區域中來提高其性能。
例如,假設我們在leveldb之上實現一個簡單的文件系統。 我們可能希望存儲的條目的類型是:
filename -> permission-bits, length, list of file_block_ids
file_block_id -> data
我們可能需要用一個字母前綴標識文件名keys
(比如/
),並用不同的字母標識file_block_id
的keys
(比如0
),以便只掃描元數據而不強迫我們提取和緩存龐大的文件內容。
過濾器 Filters
由於leveldb數據在磁盤上的組織方式,單個Get()
調用可能涉及從磁盤多次讀取。 可選的FilterPolicy
機制可用於顯着減少磁盤讀取數。
leveldb::Options options;
options.filter_policy = NewBloomFilterPolicy(10);
leveldb::DB* db;
leveldb::DB::Open(options, "/tmp/testdb", &db);
... use the database ...
delete db;
delete options.filter_policy;
上述代碼將基於BoolmFilter
的過濾策略與數據庫相關聯。 基於Boolm過濾器的過濾依賴於在每個Key
的存儲器中保持一些數量的數據bits
(在這種情況下每個密鑰是10位,因為這是我們傳遞給NewBloomFilterPolicy的參數)。 此過濾器將Get()
調用所需的 不必要磁盤讀取次數 減少大約100倍。增加每個鍵的bits位將導致更大的減少,但是以更多的內存使用為代價。我們建議不適合將工作集放置在內存中的應用程序,多設置一些隨機讀取過濾策略。
如果您使用自定義比較器,則應確保您使用的過濾器策略與比較器兼容。 例如,考慮一個比較器,在比較鍵時忽略尾隨空格。 NewBloomFilterPolicy
不能與這樣的比較器一起使用。 因此,應用程序也應提供一個也忽略尾部空格的自定義過濾器策略。 例如:
class CustomFilterPolicy : public leveldb::FilterPolicy {
private:
FilterPolicy* builtin_policy_;
public:
CustomFilterPolicy() : builtin_policy_(NewBloomFilterPolicy(10)) { }
~CustomFilterPolicy() { delete builtin_policy_; }
const char* Name() const { return "IgnoreTrailingSpacesFilter"; }
void CreateFilter(const Slice* keys, int n, std::string* dst) const {
// 刪除尾隨空格后使用內置bloom filter代碼
std::vector<Slice> trimmed(n);
for (int i = 0; i < n; i++) {
trimmed[i] = RemoveTrailingSpaces(keys[i]);
}
return builtin_policy_->CreateFilter(&trimmed[i], n, dst);
}
bool KeyMayMatch(const Slice& key, const Slice& filter) const {
// 刪除尾隨空格后使用內置bloom filter代碼
return builtin_policy_->KeyMayMatch(RemoveTrailingSpaces(key), filter);
}
};
Advanced應用程序可以提供不使用Bloom過濾器,但使用一些其他的機制來總結一組鍵的過濾策略。 有關詳細信息,請參閱leveldb/filter_policy.h
。
檢驗和 Checksums
leveldb將校驗和與其存儲在文件系統中的所有數據相關聯。 提供了兩個單獨的控件來對這些校驗進行主動驗證:
ReadOptions::verify_checksums
可以設置為true
,以強制代表特定讀取從文件系統讀取的所有數據的校驗和驗證。 默認情況下,不進行此類驗證。Options::paranoid_checks
可以在打開數據庫之前設置為true,以使數據庫實現在檢測到內部損壞時立即引發錯誤。 根據數據庫的哪個部分已損壞,可能在打開數據庫時或稍后由另一個數據庫操作引發錯誤。 默認情況下,paranoid
檢查關閉,以便即使數據庫的持久存儲器的部分已損壞也可以使用數據庫。- 如果數據庫損壞(或者當打開
paranoid checking
時失敗),leveldb::RepairDB
函數可以用於盡可能多地恢復數據。
大致大小 Approximate Sizes
GetApproximateSizes
方法可用於獲取一個或多個key
范圍使用的文件系統空間的大致字節數。
leveldb::Range ranges[2];
ranges[0] = leveldb::Range("a", "c");
ranges[1] = leveldb::Range("x", "z");
uint64_t sizes[2];
leveldb::Status s = db->GetApproximateSizes(ranges, 2, sizes);
前面的調用會將size[0]和size [1]設置為key
范圍[a..c)和[x..z)使用的文件系統空間的大致字節數。
環境 Environment
由leveldb
實現發出的所有文件操作(和其他操作系統調用)都通過一個leveldb::Env
對象進行轉調。老練的客戶可能希望提供自己的Env
實現以獲得更好的控制。 例如,應用程序可以在文件IO調用中引入人工延遲,以限制leveldb
對系統中的其他活動的影響。
class SlowEnv : public leveldb::Env {
.. implementation of the Env interface ...
};
SlowEnv env;
leveldb::Options options;
options.env = &env;
Status s = leveldb::DB::Open(options, ...);
移植 Porting
leveldb可以通過提供由leveldb/port/port.h
導出特定平台的types/methods/functions
實現而被移植到新的平台。相關更多詳細信息,請參閱leveldb/port/port_example.h
此外,新平台可能需要一個新的默認leveldb::Env
實現。 有關示例,請參見leveldb/util/env_posix.h
。
其它信息 Other Information
有關leveldb實現的細節可以在以下文檔中找到: