索引的插入
接上篇文章,我們實現了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一番。