打造先進的內存KV數據庫-2 B樹索引的建立(2)

索引的插入

接上篇文章,我們實現了B樹的查找log2n的算法,然而在後來的單元測試中,我發現了bug,在此進行修正,修正後的查找函數:

//查找指定索引 如果找到返回找到的node和position,失敗的話返回最近的node和position,
//並返回返回值相對於查詢值是大了還是小了 true->小 false->大 優先返回大 供插入時插在前面
func (bt *B_Tree) Select(index []byte) (*node,int,uint64,error) {
    if bt.root.hash_key_num == 0 {
        return &bt.root,0,0,SelectError{false}
    }
    hash_key := hash(index)
    node_ := &bt.root
    pos_r := 0                                  //返回位置
    var err SelectError                             //返回錯誤
    var node_r *node
    for node_ != nil && node_.hash_key_num > 0{

        if node_.hash_key_num > 0 {
            //超出節點邊界
            if node_.hash_key[0] > hash_key {
                node_r = node_              //返回node,防止變成nil
                pos_r = 0
                err.less = false                    //返回大於
                node_ = node_.child[0]
                continue
            }
            if node_.hash_key[node_.hash_key_num - 1] < hash_key {
                node_r = node_              //返回node,防止變成nil
                pos_r = node_.hash_key_num
                err.less = true                 //返回小於
                node_ = node_.child[node_.hash_key_num]
                continue
            }
            //進行二分查找
            pos := node_.hash_key_num / 2
            c := node_.hash_key_num / 2
            for node_.hash_key_num > 0{
                if node_.hash_key[pos] == hash_key {
                    return node_,pos,node_.primary_key[pos],nil //找到
                } else if node_.hash_key[pos] < hash_key {
                    if node_.hash_key[pos + 1] > hash_key {
                        node_r = node_              //返回node,防止變成nil
                        pos_r = pos + 1
                        err.less = false                    //返回大於
                        node_ = node_.child[pos + 1]
                        break
                    }
                    c /= 2
                    if c == 0 { c=1 }
                    pos += c
                } else if node_.hash_key[pos] > hash_key {

                    if node_.hash_key[pos - 1] < hash_key {
                        node_r = node_              //返回node,防止變成nil
                        pos_r = pos
                        err.less = true                 //返回小於
                        node_ = node_.child[pos]
                        break
                    }
                    c /= 2
                    if c == 0 { c=1 }
                    pos -= c
                }
            }
        }
    }
    return node_r,pos_r,0,err
}

插入索引時,我們使用如下算法:
1.先通過select尋找應該插入的位置
2.如果被插入的節點中關鍵字樹小於節點最多關鍵字數 - 1,則直接插入完成,否則到3
3.如果被插入節點關鍵字數等於節點最多關鍵字數 - 1,先插入該節點,再將該節點分裂成2個節點,將中間關鍵字插入他的父節點,如果此時父節點也滿,則繼續分裂父節點,直到根節點滿,此索引樹不允許其他任何插入;如果被插入的關鍵字數等於節點最多關鍵字數,則說明此樹已滿,不接受任何插入
實現如下:

// 設置值
func (bt *B_Tree) Set(text []byte,primary_key uint64) (error) {
    node_,pos,_,err := bt.Select(text)  //查詢
    if err == nil {                         //找到了
        //TODO 和儲存引擎協調進行
    } else {
        //TODO 儲存引擎加入記錄
        if node_.hash_key_num < node_data_num - 1 { //有空間可以插入
            if err.(SelectError).less {         //插在最後一個:只有查詢到最後一個還小的時候纔會返回less
                node_.hash_key[node_.hash_key_num] = hash(text)
                node_.primary_key[node_.hash_key_num] = primary_key
                node_.hash_key_num++
            }else {                             //插在前面
                for i := node_.hash_key_num;i > pos;i-- {
                    node_.hash_key[i] = node_.hash_key[i - 1]
                    node_.primary_key[i] = node_.primary_key[i - 1]
                }
                node_.hash_key[pos] = hash(text)
                node_.primary_key[pos] = primary_key
                node_.hash_key_num++
            }
            return nil
        }else { //沒有空間插入,進行分裂           
            //一定是最右兒子節點不存在的情況
            if node_.child[node_data_num] != nil {
                //TODO 嚴重邏輯錯誤 索引結構可能已損壞
                panic(errors.New("嚴重邏輯錯誤,索引結構可能已損壞!"))
            } else {
                //放入節點
                if err.(SelectError).less {         //插在最後一個:只有查詢到最後一個還小的時候纔會返回less
                    node_.hash_key[node_.hash_key_num] = hash(text)
                    node_.primary_key[node_.hash_key_num] = primary_key
                    node_.hash_key_num++
                }else {                             //插在前面
                    for i := node_.hash_key_num;i > pos;i-- {
                        node_.hash_key[i] = node_.hash_key[i - 1]
                        node_.primary_key[i] = node_.primary_key[i - 1]
                    }
                    node_.hash_key[pos] = hash(text)
                    node_.primary_key[pos] = primary_key

                    node_.hash_key_num++
                }
                split:
                //尋找父節點
                node_temp_num := node_.hash_key_num
                node_.hash_key_num = 0  //僞造空節點,讓Select返回父節點
                node_parent,pos_parent,_,err_parent := bt.Select(text)

                //分裂出一個新節點
                node_new := new(node)
                mid := node_temp_num / 2
                node_.hash_key_num = mid
                j := 0
                for i := mid + 1;i < node_temp_num;i++ {
                    node_new.hash_key[j] = node_.hash_key[i]
                    node_new.primary_key[j] = node_.primary_key[i]
                    j++
                }
                node_new.hash_key_num = j

                //如果node_是根節點,分裂出的作爲兒子節點
                if node_ == &bt.root {

                    node_.hash_key_num++    //mid迴歸root
                    if node_.child[mid + 1] != nil {
                        panic(errors.New("Memory is full!"))
                    }
                    node_.child[mid + 1] = node_new
                    return nil
                } else {        //不是根節點,則將mid插入父節點

                    if node_parent.hash_key_num < node_data_num - 1 {

                        if err_parent.(SelectError).less {
                            //一定是最後一個元素都小於hash(text)
                            node_parent.hash_key[node_parent.hash_key_num] = node_.hash_key[mid]
                            node_parent.primary_key[node_parent.hash_key_num] = node_.primary_key[mid]
                            node_parent.hash_key_num++

                            if node_parent.child[node_parent.hash_key_num] != nil {
                                panic(errors.New("Memory is full!"))
                            }
                            node_parent.child[node_parent.hash_key_num] = node_new

                            //TODO 父節點滿了怎麼辦
                            if node_parent.hash_key_num > node_data_num - 1 {

                                node_ = node_parent
                                goto split
                            }

                        }else {

                            for i := node_parent.hash_key_num;i > pos_parent;i-- {
                                node_parent.hash_key[i] = node_parent.hash_key[i - 1]
                                node_parent.primary_key[i] = node_parent.primary_key[i - 1]
                                node_parent.child[i] = node_parent.child[i - 1]
                            }
                            node_parent.hash_key[pos_parent] = node_.hash_key[mid]
                            node_parent.primary_key[pos_parent] = node_.primary_key[mid]
                            if node_parent.hash_key[pos_parent + 1] > node_parent.hash_key[pos_parent + 2] {
                                panic(errors.New("Memory is full!"))
                            }
                            if node_parent.child[pos_parent] != node_{
                                panic(errors.New("Memory is full!"))
                            }
                            node_parent.child[pos_parent + 1] = node_new
                            node_parent.hash_key_num++
                            //TODO 父節點滿了怎麼辦
                            if node_parent.hash_key_num > node_data_num - 1 {
                                node_ = node_parent
                                goto split
                            }
                        }
                        return nil
                    } else {
                        //父節點已滿
                        if node_.child[mid] != nil {
                            panic(errors.New("Memory is full!"))
                        }
                        node_.child[mid] = node_new
                        return nil
                    }
                }
            }
        }
    }
    return nil
}

單元測試如下,測試了大量插入和查詢的數據:

package index
import (
    "fmt"
    "testing"
    "os"
    "time"
)

var num int

func expected(t *testing.T,expecting uint64,real uint64) {
    if expecting != real {
        t.Error("-Expected-")
        t.Error(expecting)
        t.Error("-Real-")
        t.Error(real)
        os.Exit(1)
        num ++
    }
}

func Test_hash(t *testing.T) {
    expected(t,15108241,hash([]byte("我")))
    expected(t,14990752,hash([]byte("你")))
    expected(t,14990230,hash([]byte("他")))
    expected(t,0,hash([]byte{}))
    fmt.Println(hash([]byte("吳")))
}

func Test_Select(t *testing.T) {
    bt := new(B_Tree)
    bt.Init()
    bt.root.hash_key[2] = 15108241
    bt.root.hash_key[1] = 14990752
    bt.root.hash_key[0] = 14990230
    bt.root.hash_key_num = 3
    bt.root.primary_key[2] = 2
    bt.root.primary_key[1] = 1
    bt.root.primary_key[0] = 0
    bt.root.child[0] = new(node)
    bt.root.child[0].Init()
    bt.root.is_leaf = false
    bt.root.child[0].hash_key_num++
    bt.root.child[0].hash_key[0] = 65
    bt.root.child[0].primary_key[0] = 65
    bt.root.child[2] = new(node)
    bt.root.child[2].Init()
    bt.root.child[2].hash_key_num++
    bt.root.child[2].hash_key[0] = 15044788
    bt.root.child[2].primary_key[0] = 15044788
    _,_,i,_ := bt.Select([]byte("我"))
    expected(t,2,i)
    _,_,i,_ = bt.Select([]byte("你"))
    expected(t,1,i)
    _,_,i,_ = bt.Select([]byte("他"))
    expected(t,0,i)
    _,_,i,_ = bt.Select([]byte("呵呵"))
    expected(t,0,i)
    _,_,i,_ = bt.Select([]byte("呼呼"))
    expected(t,0,i)
    _,_,i,_ = bt.Select([]byte("A"))
    expected(t,65,i)
    _,_,i,_ = bt.Select([]byte("吳"))
    expected(t,15044788,i)
}

func Test_Set(t *testing.T) {
    bt := new(B_Tree)
    bt.Init()
    var i uint64

    bs := make([]byte,3)
    start := time.Now().UnixNano()
    for temp := 10000;temp < 15555000;temp++ {

        bs[0] = byte(temp / 256 / 256)
        bs[1] = byte((temp % (256 * 256)) / 256)
        bs[2] = byte(temp % 256)
        bt.Set(bs,uint64(temp))
        //fmt.Println(i)
        //expected(t,uint64(temp),i)
    }
    fmt.Println(time.Now().UnixNano() - start)
    start = time.Now().UnixNano()/*
    for temp := 10000;temp < 1555000;temp++ {

        bs[0] = byte(temp / 256 / 256)
        bs[1] = byte((temp % (256 * 256)) / 256)
        bs[2] = byte(temp % 256)
        //bt.Set(bs,uint64(temp))
        _,_,i,_ = bt.Select(bs)
        //if i == 0 {fmt.Println(temp,i)}
        expected(t,uint64(temp),i)
    }*/
    for temp := 15500000-1;temp > 10000;temp-- {

        bs[0] = byte(temp / 256 / 256)
        bs[1] = byte((temp % (256 * 256)) / 256)
        bs[2] = byte(temp % 256)
        //bt.Set(bs,uint64(temp))
        _,_,i,_ = bt.Select(bs)
        //if i == 0 {fmt.Println(temp,i)}
        //expected(t,uint64(temp),i)
    }
    fmt.Println(time.Now().UnixNano() - start)
    fmt.Println(i)

}

經測試,我的弱渣筆記本低電壓8G內存,i7-4712mq單核插入15M數據,每條數據時間大約爲120ns,每條查詢時間大約爲100ns;15M以上數據可能導致樹滿,該索引樹設計爲16G數據,每條1k,約16M條數據設計。暫時測試得,節點大小爲L2緩存的3/8時能獲得較高性能,不過我是go test測試,可能會有所影響,完整編譯後將再次優化參數。目前實現了10M/s單核的吞吐量,8核吞吐量大約估計在50~60M/s之間。Redis在15M環境下的吞吐量在3k/s左右,不過他還有儲存引擎,日誌記錄,持久化等等的消耗,待完全完成此數據庫後,將會與Redis PK一番。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章