《Go語言四十二章經(jīng)》第四十章 LevelDB與BoltDB
作者:李驍
LevelDB 和 BoltDB 都是k/v非關(guān)系型數(shù)據(jù)庫。
LevelDB沒有事務,LevelDB實現(xiàn)了一個日志結(jié)構(gòu)化的merge tree。它將有序的key/value存儲在不同文件的之中,通過db, _ := leveldb.OpenFile("db", nil),在db目錄下有很多數(shù)據(jù)文件,并通過“層級”把它們分開,并且周期性地將小的文件merge為更大的文件。這讓其在隨機寫的時候會很快,但是讀的時候卻很慢。
這也讓LevelDB的性能不可預知:但數(shù)據(jù)量很小的時候,它可能性能很好,但是當隨著數(shù)據(jù)量的增加,性能只會越來越糟糕。而且做merge的線程也會在服務器上出現(xiàn)問題。
LSM樹而且通過批量存儲技術(shù)規(guī)避磁盤隨機寫入問題。 LSM樹的設(shè)計思想非常樸素,它的原理是把一顆大樹拆分成N棵小樹, 它首先寫入到內(nèi)存中(內(nèi)存沒有尋道速度的問題,隨機寫的性能得到大幅提升),在內(nèi)存中構(gòu)建一顆有序小樹,隨著小樹越來越大,內(nèi)存的小樹會flush到磁盤上。磁盤中的樹定期可以做merge操作,合并成一棵大樹,以優(yōu)化讀性能。
BoltDB會在數(shù)據(jù)文件上獲得一個文件鎖,所以多個進程不能同時打開同一個數(shù)據(jù)庫。BoltDB使用一個單獨的內(nèi)存映射的文件(.db),實現(xiàn)一個寫入時拷貝的B+樹,這能讓讀取更快。而且,BoltDB的載入時間很快,特別是在從crash恢復的時候,因為它不需要去通過讀log去找到上次成功的事務,它僅僅從兩個B+樹的根節(jié)點讀取ID。
BoltDB支持完全可序列化的ACID事務,讓應用程序可以更簡單的處理復雜操作。
BoltDB設(shè)計源于LMDB,具有以下特點:
- 直接使用API存取數(shù)據(jù),沒有查詢語句;
- 支持完全可序列化的ACID事務,這個特性比LevelDB強;
- 數(shù)據(jù)保存在內(nèi)存映射的文件里。沒有wal、線程壓縮和垃圾回收;
- 通過COW技術(shù),可實現(xiàn)無鎖的讀寫并發(fā),但是無法實現(xiàn)無鎖的寫寫并發(fā),這就注定了讀性能超高,但寫性能一般,適合與讀多寫少的場景。
- 最后,BoltDB使用Golang開發(fā),而且被應用于influxDB項目作為底層存儲。
LMDB的全稱是Lightning Memory-Mapped Database(快如閃電的內(nèi)存映射數(shù)據(jù)庫),它的文件結(jié)構(gòu)簡單,包含一個數(shù)據(jù)文件和一個鎖文件.
LMDB文件可以同時由多個進程打開,具有極高的數(shù)據(jù)存取速度,訪問簡單,不需要運行單獨的數(shù)據(jù)庫管理進程,只要在訪問數(shù)據(jù)的代碼里引用LMDB庫,訪問時給文件路徑即可。讓系統(tǒng)訪問大量小文件的開銷很大,而LMDB使用內(nèi)存映射的方式訪問文件,使得文件內(nèi)尋址的開銷非常小,使用指針運算就能實現(xiàn)。數(shù)據(jù)庫單文件還能減少數(shù)據(jù)集復制/傳輸過程的開銷。
40.1 LevelDB
Go語言LevelDB的實現(xiàn)我們使用 github.com/syndtr/goleveldb/leveldb 包,通過go get命令下載該包后在程序中導入。
goleveldb主要有Get(),Put()等方法,可進行key/value的讀取和寫入,可進行事務批量Put()插入key,Delete()刪除某個key。
package main
import (
"fmt"
"strconv"
"crypto/md5"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
var md = md5.New()
// 測試專用
func Read(db *leveldb.DB, num int) {
var kStr string
var haskKey string
kStr = strconv.Itoa(num)
md.Write([]byte(kStr))
haskKey = fmt.Sprintf("%x", md.Sum(nil))
md.Reset()
db.Get([]byte(haskKey), nil)
}
// 測試專用
func Write(db *leveldb.DB, num int) {
var kStr string
var haskKey string
kStr = strconv.Itoa(num)
md.Write([]byte(kStr))
haskKey = fmt.Sprintf("%x", md.Sum(nil))
md.Reset()
db.Put([]byte(haskKey), []byte(kStr), nil)
}
func main() {
// 打開數(shù)據(jù)庫文件 /path/to/db ,第一個參數(shù)為存放數(shù)據(jù)的目錄,不是具體文件
// o := &opt.Options{ Filter: filter.NewBloomFilter(10),}
// OpenFile第2個參數(shù)這里指定為nil,在數(shù)據(jù)集大時可設(shè)置比如布隆過濾器。
// *opt.Options 為nil默認為false ,true為只讀模式ReadOnly
db, _ := leveldb.OpenFile("levdb", nil)
defer db.Close()
// 讀數(shù)據(jù)庫:Get(key,nil),寫數(shù)據(jù)庫:Put(key,value,nil)
// Put第三個參數(shù)為nil,默認就好,默認時寫的時候如果機器崩了數(shù)據(jù)會丟失。
// key和value都是字節(jié)slice
_ = db.Put([]byte("key1"), []byte("好好檢查"), nil)
_ = db.Put([]byte("key2"), []byte("天天向上"), nil)
_ = db.Put([]byte("key:3"), []byte("就會一個本事"), nil)
_ = db.Put([]byte("uname"), []byte("Jim"), nil)
_ = db.Put([]byte("time"), []byte("1450932202"), nil)
// 讀數(shù)據(jù)庫:Get(key,nil),返回字節(jié)slice
data, _ := db.Get([]byte("key1"), nil)
fmt.Println("key1=>", string(data))
// 刪除某個key(key,nil),key不存在時并不返回錯誤
_ = db.Delete([]byte("key"), nil)
//迭代數(shù)據(jù)庫內(nèi)容:
iter := db.NewIterator(nil, nil)
fmt.Println("迭代所有key/value")
for iter.Next() {
key := iter.Key()
value := iter.Value()
fmt.Println(string(key), "=>", string(value))
}
iter.Release()
iter.Error()
//Seek()定位到比給定key值(字節(jié)值)要大的第一個key,可next迭代所有篩選出的key/value:
iter = db.NewIterator(nil, nil)
fmt.Println("\nSeek()按值篩選查找key")
for ok := iter.Seek([]byte("t")); ok; ok = iter.Next() {
// Use key/value.
fmt.Println("Seek-then-Iterate:")
fmt.Println(string(iter.Key()), "=>", string(iter.Value()))
}
iter.Release()
//迭代內(nèi)容子集:start表示key中包含有的字符串, Limit表示key不能包含有字符串
fmt.Println("\n 按照指定(排除)條件篩選key")
iter = db.NewIterator(&util.Range{Start: []byte("key"), Limit: []byte("no")}, nil)
for iter.Next() {
// Use key/value.
fmt.Println("Iterate over subset of database content:")
fmt.Println(string(iter.Key()), "=>", string(iter.Value()))
}
iter.Release()
//迭代子集內(nèi)容,key的前綴是指定字符串:
fmt.Println("\n 查找指定前綴key")
iter = db.NewIterator(util.BytesPrefix([]byte("key")), nil)
for iter.Next() {
// Use key/value.
fmt.Println("Iterate over subset of database content with a particular prefix:")
fmt.Println(string(iter.Key()), "=>", string(iter.Value()))
}
iter.Release()
_ = iter.Error()
//批量寫:
batch := new(leveldb.Batch)
var kStr string
var batchkey string
for i := 0; i < 10; i++ {
kStr = strconv.Itoa(i)
md.Write([]byte(kStr))
batchkey = fmt.Sprintf("%x", md.Sum(nil))
batch.Put([]byte(batchkey), []byte(kStr))
}
md.Reset()
batch.Delete([]byte("lazy"))
_ = db.Write(batch, nil)
}
Leveldb比較突出的問題是在讀操作上,在大量key的情況下可能成為性能的瓶頸,我們可以根據(jù)場景來選擇使用。下面是我們進行的幾種數(shù)量級別的基準測試數(shù)據(jù):
BenchmarkWrite-4 100000 14541 ns/op
BenchmarkRead-4 100000 13094 ns/op
BenchmarkWrite-4 500000 12724 ns/op
BenchmarkRead-4 500000 17002 ns/op
BenchmarkWrite-4 1000000 13355 ns/op
BenchmarkRead-4 1000000 20610 ns/op
BenchmarkWrite-4 3000000 15644 ns/op
BenchmarkRead-4 3000000 22742 ns/op
我們可以看到隨著key的數(shù)量的增加,讀的性能明顯地下降,而寫的性能則不受影響。
40.2 BoltDB
Go語言BoltDB的實現(xiàn)我們使用 github.com/boltdb/bolt 包,通過go get命令下載該包后在程序中導入。
BoltDB中存儲比較重要的概念是bucket,存取操作之前都需要指定bucket,如果讀數(shù)據(jù)時指定bucket不存在,則會panic。
package main
import (
"bytes"
"fmt"
"log"
"time"
"github.com/boltdb/bolt"
)
func main() {
Boltdb()
}
func Boltdb() error {
// Bolt 會在數(shù)據(jù)文件上獲得一個文件鎖,所以多個進程不能同時打開同一個數(shù)據(jù)庫。
// 打開一個已經(jīng)打開的 Bolt 數(shù)據(jù)庫將導致它掛起,直到另一個進程關(guān)閉它。
// 為防止無限期等待,您可以將超時選項傳遞給Open()函數(shù):
db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 10 * time.Second})
defer db.Close()
if err != nil {
log.Fatal(err)
}
// 兩種處理方式:讀-寫和只讀操作,讀-寫方式開始于db.Update方法:
// Bolt 一次只允許一個讀寫事務,但是一次允許多個只讀事務。
// 每個事務處理都有一個始終如一的數(shù)據(jù)視圖
err = db.Update(func(tx *bolt.Tx) error {
// 這里還有另外一層:k-v存儲在bucket中,
// 可以將bucket當做一個key的集合或者是數(shù)據(jù)庫中的表。
//(順便提一句,buckets中可以包含其他的buckets,這將會相當有用)
// Buckets 是鍵值對在數(shù)據(jù)庫中的集合.所有在bucket中的key必須唯一,
// 使用DB.CreateBucket() 函數(shù)建立buket
// Tx.DeleteBucket() 刪除bucket
// b := tx.Bucket([]byte("MyBucket"))
b, err := tx.CreateBucketIfNotExists([]byte("MyBucket"))
//要將 key/value 對保存到 bucket,請使用 Bucket.Put() 函數(shù):
//這將在 MyBucket 的 bucket 中將 "answer" key的值設(shè)置為"42"。
err = b.Put([]byte("answer"), []byte("42"))
err = b.Put([]byte("why"), []byte("101010"))
return err
})
// 可以看到,傳入db.update函數(shù)一個參數(shù),在函數(shù)內(nèi)部你可以get/set數(shù)據(jù)和處理error。
// 如返回為nil,事務就會從數(shù)據(jù)庫得到一個commit,但如果返回一個實際的錯誤,則會做回滾,
// 你在函數(shù)中做的事情都不會commit。這很自然,因為你不需要人為地去關(guān)心事務的回滾,
// 只需要返回一個錯誤,其他的由Bolt去幫你完成。
// 只讀事務 只讀事務和讀寫事務不應該相互依賴,一般不應該在同一個例程中同時打開。
// 這可能會導致死鎖,因為讀寫事務需要定期重新映射數(shù)據(jù)文件,
// 但只有在只讀事務處于打開狀態(tài)時才能這樣做。
// 批量讀寫事務.每一次新的事物都需要等待上一次事物的結(jié)束,
// 可以通過DB.Batch()批處理來完
err = db.Batch(func(tx *bolt.Tx) error {
return nil
})
//只讀事務在db.View函數(shù)之中:在函數(shù)中可以讀取,但是不能做修改。
db.View(func(tx *bolt.Tx) error {
//要檢索這個value,我們可以使用 Bucket.Get() 函數(shù):
//由于Get是有安全保障的,所有不會返回錯誤,不存在的key返回nil
b := tx.Bucket([]byte("MyBucket"))
//tx.Bucket([]byte("MyBucket")).Cursor() 可這樣寫
v := b.Get([]byte("answer"))
id, _ := b.NextSequence()
fmt.Printf("The answer is: %s %d \n", v, id)
//游標遍歷key
c := b.Cursor()
fmt.Println("\n游標遍歷key")
for k, v := c.First(); k != nil; k, v = c.Next() {
fmt.Printf("key=%s, value=%s\n", k, v)
}
//游標上有以下函數(shù):
//First() 移動到第一個健.
//Last() 移動到最后一個健.
//Seek() 移動到特定的一個健.
//Next() 移動到下一個健.
//Prev() 移動到上一個健.
//Prefix 前綴掃描
fmt.Println("\nPrefix 前綴掃描")
prefix := []byte("a")
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
fmt.Printf("key=%s, value=%s\n", k, v)
}
return nil
})
//如果你知道所在桶中擁有鍵,你也可以使用ForEach()來迭代:
db.View(func(tx *bolt.Tx) error {
fmt.Println("\nForEach()來迭代")
b := tx.Bucket([]byte("MyBucket"))
b.ForEach(func(k, v []byte) error {
fmt.Printf("key=%s, value=%s\n", k, v)
return nil
})
return nil
})
//事務處理
// 開始事務
tx, err := db.Begin(true)
if err != nil {
return err
}
defer tx.Rollback()
// 使用事務...
_, err = tx.CreateBucket([]byte("MyBucket"))
if err != nil {
return err
}
// 事務提交
if err = tx.Commit(); err != nil {
return err
}
return err
//還可以在一個鍵中存儲一個桶,以創(chuàng)建嵌套的桶:
//func (*Bucket) CreateBucket(key []byte) (*Bucket, error)
//func (*Bucket) CreateBucketIfNotExists(key []byte) (*Bucket, error)
//func (*Bucket) DeleteBucket(key []byte) error
}
BoltDB的性能測試這里就不再做闡述,和LevelDB正好相反,它在寫性能上存在瓶頸,而讀性能上非常有優(yōu)勢,這兩者我們需要根據(jù)場景來選擇使用。
本書《Go語言四十二章經(jīng)》內(nèi)容在github上同步地址:https://github.com/ffhelicopter/Go42
本書《Go語言四十二章經(jīng)》內(nèi)容在簡書同步地址: http://m.itdecent.cn/nb/29056963雖然本書中例子都經(jīng)過實際運行,但難免出現(xiàn)錯誤和不足之處,煩請您指出;如有建議也歡迎交流。
聯(lián)系郵箱:roteman@163.com