leveldb源碼筆記

關於KV數據庫leveldb的介紹,網上已經太多了,這裏只是本身再學習源碼過程當中,整理的筆記,磁盤存儲和內存存儲的結構用了僞代碼表示出來了,首先是內存中存儲結構,而後是log文件存儲結構和磁盤數據sst文件存儲結構。mysql

MemTable存儲格式

MemTable底層是用skiplist(跳躍表)進行存儲, 數據所有存儲在內存中, 具體結構設計以下:sql

class MemTable
{
    enum ValueType
    {
        kTypeDeletion = 0x0,  /*正常標記*/
        kTypeValue = 0x1      /*已刪除標記*/
    };

    /*跳躍表中存儲的實體信息*/
    struct Entity
    {
        /*key長度*/
        key_size;
        /*key數據*/
        key_bytes;
        /*標識是否刪除ValueType中一個*/
        type;
        /*value長度*/
        value_size;
        /*value數據*/
        value_bytes;
    };

    SkipList<Entity*, KeyComparator> table_;
}

Log文件

  Log文件存儲在磁盤上, 用於數據恢復使用, 寫入數據前先寫入log文件, 與mysql方式相似, 寫入的實體格式Entity以下, 寫入塊以32KB爲單位, 若是一個塊空間有足夠空間容納新寫入的Entity, 則直接寫入, 並將記錄類型type置爲KFullType; 若是沒法完整寫入, 則寫入Entity開始部分的塊類型爲kFirstType, 寫入中間部分塊類型爲kMiddleType, 寫入最後部分塊類型爲kLastType. 一個塊內可能寫入多個Entity, 一個Entity可能寫入多個塊中,塊方式寫入以後,在讀取日誌進行恢復數據時, 變得很方便, 直接按塊大小讀取, 加快訪問速度.數據庫

Entity實體結構示意圖

|                 HEADER               |                            key, value對                                |
|--------------|------------|----------|------------|-----------|-------------|------------|-------------|......|
|   checksum   |   length   |   type   |  val_type  |  key_size |  key_bytes  |  val_size  |  val_bytes  |......|
struct Entity
{
    /*主要標識一個Entity是否在當前塊中的*/
    enum RecordType
    {
        kZeroType = 0,
        kFullType = 1,
        kFirstType = 2,
        kMiddleType = 3,
        kLastType = 4
    };

    struct Header
    {
        /*32位crc校驗碼, 對寫入數據校驗*/
        int4 checksum;
        /*日誌塊長度*/
        int2 length;
        /*RecordType中一種*/
        int1 type;
    };

    /*鍵值對能夠批量寫入, 所以一次可能有N個鍵值對*/
    struct KeyValuePair
    {
        /*標識值被刪除,仍是正常狀態*/
        val_type;
        /*鍵長度*/
        key_size;
        /*鍵內容*/
        key_bytes;
        /*key對應的值長度*/
        val_size;
        /*key對應的值內容*/
        val_bytes;
    }[N];
};

SST文件存儲格式

  SST文件存儲最終落入磁盤的數據, 數據是隻讀的, 數據默認是壓縮存儲. 下面是僞代碼的存儲數據結構, 文件依次存儲數據塊, 數據塊索引, 過濾器,文件尾。數組

  1.key共享存儲:block中存儲的一條條記錄, 每條記錄中一個KV對, 假如存儲key爲user1, user2, user3, 則首先存入user1, shared_size值爲0,non_shared_size爲0,後面依次存入value長度和值,存入user2時,因爲user1和user2是共享user部分,所以user2中shared_size爲4,non_shared_size爲1,後面依次存入value長度,non_shared值也就是1,value值,後面存入user3時,同理,shared_size爲4,non_shared_size爲1,後面依次存入value長度,non_shared值也就是3,value值,因爲SST中存儲的key值都是有序的,key若是類似的,這種存儲能夠節省不少空間。緩存

  2.重啓點:block最後存儲了一個重啓點數組,默認間隔16條記錄插入一個重啓點,插入重啓點位置的key是一個完整的key,沒有共享字段,插入重啓點是爲了加快block中查找key的速度,block中進行查找時,咱們首先在重啓點數組中利用二分查找,找到距離查找小於key最近的重啓點,而後順着重啓點依次查找,直到找到key,或者沒有找到。數據結構

  3.過濾器BlockMeta:爲了減小操做磁盤次數,leveldb加入了過濾器,建立db的時候能夠指定過濾器,leveldb實現了布隆過濾器供使用。BlcokMeta中每條記錄對應一個BlockData的過濾器,查找時,若是過濾器中沒有找到則直接返回,不然在BlockData中進行查找。性能

  4.塊索引BlcokIndex:存儲塊對應的索引,其中key爲前一個塊中最後一個key和後一個塊中第一個key之間的一個值,好比block1中最後key爲user1, block2中最小key爲user5,索引的key值爲user2;若是block2中最小的key爲user2,則索引的key只能爲user1。學習

/*M個數據塊, 存儲具體數據*/
struct Block
{
    /*每一個數據塊中存儲N條記錄*/
    struct Record
    {
        /*Key中共享字段長度*/
        size_t shared_size;
        /*Key中獨有字段長度*/
        size_t non_shared_size;
        /*Key對應的Value字段長度*/
        size_t value_size;
        /*Key中獨有字段內容*/
        byte non_shared_bytes[non_shared_size];
        /*Key對應Value字段內容*/
        byte value_bytes[value_size];
    }[N];

    /*重啓點數組方式保存, 長度和重啓點都已固定大小存儲,值表示重啓點距離block開始位置的偏移量*/
    uint32 restarts[restart_num];
    /*重啓點個數*/
    uint32 restart_num;
    /*標識是否進行壓縮*/
    byte type;
    /*數據校驗碼, 若是壓縮數據, 則校驗碼是數據壓縮以後的校驗碼, 校驗數據的完整性*/
    uint32 crc;
};

class table
{
    /*存放數據的數據塊*/
    Block BlocKData[N];
    /*存放Data數據塊對應的索引, 每一個記錄對應一個Block, 其中value存儲的是塊相對於文件頭的偏移量*/
    Block BlockIndex;
    /*存儲過濾規則,默認沒有,通常使用布隆過濾器,可能爲空,裏面每條記錄對應一個block生成的過濾器*/
    Block BlcokMeta;

    struct Footer
    {
        /*過濾器數據相對於文件頭的偏移量*/
        uint64 metaindex_offset;
        /*過濾器數據長度*/
        uint64 metaindex_size;
        /*BlockIndex數據相對於文件頭的偏移量*/
        uint64 blockindex_offset;
        /*BlockIndex數據長度*/
        uint64 blockindex_size;
        /*文件尾部填充的魔數*/
        uint64 magic_number;
    }
};

LRU緩存

  leveldb中讀性能比不了內存數據庫,因爲分層存儲,爲了儘可能減小磁盤操做,實現了一套緩存機制,緩存以查找的key做爲hash,對應值爲key所在的table指針。緩存作了兩級,外層是是固定大小爲16的hash表,hash表中每條記錄中對應一個隨元素數量增加的hash表, 兩層hash一方面能夠減小hash碰撞次數, 另外一方面rehash時減小copy內存的長度, 內層的緩存操做是須要加鎖的, 分層以後減小鎖的競爭次數.ui

分層

leveldb磁盤存儲的文件分爲level-0到level-6, 每一層中有若干個文件, 全部文件長度和最大限制以下, 默認存儲總量10TB左右. 其中level-0中默認最大文件個數限制爲4設計

level-0 10M
level-1 100M
level-2 1000M
level-3 10000M
level-4 100000M
level-5 1000000M
level-6 10000000M

合併

  leveldb數據存儲分爲兩部份內存中MemTable和磁盤上Table文件, 合併的過程就是將內存數據合併入磁盤中, 磁盤中低層數據向高層合併.向數據庫中寫入一個key時, 首先將Key和Value值寫入log文件中, 而後檢查MemTable中數據大小, 若是大於臨界值(默認4M), 則從新建立MemTable, 將Key插入, 原來的MemTable則保存在Imm中, 只用於查詢使用, 檢查是否須要進行合併操做, 流程以下.

  1.判斷Imm是否爲空, Imm非空先遍歷Imm中數據依次寫入sst文件中, 而後挑選合適的level進行合併, 從level-0開始遍歷到level-6, 挑選過程以下, 挑選結束後直接將生成的sst文件添加進挑選的level.

  a) 因爲level-0不一樣文件中存在重疊key, 所以單獨判斷Imm中key和level0中key是否重疊, 重疊則直接將Imm中數據合併入level-0中, 不然繼續向下;
  b) 假設遍歷到level-1層發現key和Imm中key有重疊, 則直接將Imm合併入level-0層; 不然繼續向下.
  c) 假如遍歷到level-1層發現key和Imm中key沒有重疊, 可是level-2層中key與Imm中key重疊文件長度大於kMaxGrandParentOverlapBytes(默認20M), 則直接合併入level-0層, 避免level-1和level-2層重疊太多,後面產生過多的合併操做. 不然level+1後繼續步驟b進行遍歷.

  2.Imm爲空時, 則須要合併磁盤中的數據是否須要合併, 每次修改VersionSet集合中的文件時,都會對每層數據評估得出一個score, 評估出下次最合適合併的level,

  level-0層 : score=文件個數/文件最大總數.
  level-1~6層, score=文件總長度/本層文件最大長度.

  根據獲取的score值, 得出本次最須要合併的level, 若是level中文件在level+1中key沒有重疊, 則直接將level中文件移除, 並添加到level+1中; level和level+1中key存在重疊, 則須要使用合併迭代器, 包含了level和level+1層須要合併的文件迭代器(可能包含多個文件), 每次合併迭代器迭代一次, 選擇兩層中最小的key, 插入到新的輸出文件, 若是當前遍歷的key已經被刪除或者不是最新的, 則直接忽略. 最終生成一個新的文件, 插入到level+1層.

查找元素

  查找元素過程, 首先在MemTable中查找, 找到則返回, 不然在Imm中查找, 找到則返回, 不然繼續開始在level0~6中進行查找, 首先在每一層中使用二分查找key所在的文件, 文件找到以後, 經過快索引二分查找key所在的塊, 經過塊中的過濾器(通常是布隆過濾), 匹配key值是否存在, 不存在直接返回查找不到, 不然經過重啓點二分查找key所在的記錄, 從而定位key是否存在, 存在返回key對應的value, 不然返回查找不到.

添加刪除修改元素

  leveldb添加元素,只須要將元素添加進MemTable中便可, 添加元素時會生成一個內部key, 包含是否刪除元素標誌和惟一的序列號, 經過刪除標誌肯定是否爲刪除元素, 經過序列號能夠肯定元素是否爲最新元素, 進行合併操做時能夠判斷元素狀態. leveldb刪除元素時並不會對原來的元素進行修改移除, 只是插入一個設置刪除標誌位的新元素, 合併時會移除原來的元素, 更新操做操做同樣, 一樣插入一個新元素, 合併時經過序列號肯定元素是否爲最新的, 從而移除老的元素.

Version管理

  leveldb中文件版本信息和數據庫的信息都寫入在MANIFEST-xxxxx文件中, 文件及其重要, 包含每一層的全部文件的描述, 日誌文件序號, 插入key的序列號等信息, 丟失以後數據庫基本廢掉. VersionSet版本集合操做版本信息, VersionEdit保存了Version的修改信息, 以追加的方式添加在MANIFEST-xxxxx文件中, 所以MANIFEST-xxxxx文件中還保存有歷史版本信息, 每次數據庫重啓都須要從新讀取MANIFEST-xxxxx文件並將全部的版本信息讀出, 並執行相應的VersionEdit, 生成當前版本Version. 每次進行合併操做都會生成一個VersionEdit, 追加到VersionSet中, 並寫入MANIFEST-xxxxx文件中.

相關文章
相關標籤/搜索