Go 开源KV引擎--badgerBD

Badger简介

badger 是 dgraph 开源的 LSMTree 的 KV 引擎,它相比 leveldb 有 KV 分离、事务、并发合并等增强,是 go 生态中比较生产级的存储引擎了。

文档:https://dgraph.io/docs/badger/get-started/

Github 地址:https://github.com/dgraph-io/badger

安装

安装 Badger

要开始使用 Badger,请安装 Go 1.12 或更高版本。

$ go get github.com/dgraph-io/badger/v3

注意:Badger 不直接使用 CGO,但它依赖于https://github.com/DataDog/zstd进行压缩,并且需要 gcc/cgo。

如果你想在没有 gcc/cgo 的情况下使用 badger,你可以运行CGO_ENABLED=0 go get github.com/dgraph-io/badger/…它将下载不支持 ZSTD 压缩算法的 badger。

安装 Badger 命令行工具

https://github.com/dgraph-io/badger/releases下载并提取最新的 Badger DB 版本,然后运行以下命令。

$ cd badger-<version>/badger
$ go install

这会将 badger 命令行实用程序安装到您的 $GOBIN 路径中。

数据库操作

打开数据库

必需指定dir和valuedir

其他选项都可以使用默认

使用默认配置创建数据库

package main

import (
	"github.com/dgraph-io/badger"
)

func main() {
	badgerPath := "./badger"
    //新建默认配置
    badgerOptions := badger.DefaultOptions(badgerPath)
    
	//设置badger存储的文件夹
	badgerOptions.Dir = badgerPath
	badgerOptions.ValueDir = badgerPath

	//新建badger数据库
	db, err := badger.Open(badgerOptions)
	if err != nil {
		panic(err)
	}
	defer db.Close() //关闭数据库
}

使用自定义的配置

package main

import (
	"github.com/dgraph-io/badger"
    "github.com/dgraph-io/badger/options"
)

func main() {
	badgerPath := "./badger"
    //新建自定义配置
    badgerOptions := badger.Options{
		LevelOneSize:        64 << 20, //第一层大小
		LevelSizeMultiplier: 10,       //下一层是上一层的多少倍
		MaxLevels:           12,       //LSM tree最多几层
		//key存在内存中,values(实际上value指针)存在磁盘中--称为vlog file
		TableLoadingMode:        options.MemoryMap, //LSM tree完全载入内存。MemoryMap是省内存,但查询速度比LoadToRAM慢一倍
		ValueLogLoadingMode:     options.FileIO,    //使用FileIO而非MemoryMap可以节省大量内存
		MaxTableSize:            8 << 20,           //8M
		NumCompactors:           8,                 //compaction线程数
		NumLevelZeroTables:      4,
		NumLevelZeroTablesStall: 10,
		NumMemtables:            4,     
        //写操作立即反应在MemTable上,当MemTable达到一定的大小时,它被刷新到磁盘,作为一个不可变的SSTable
        
		SyncWrites:              false, 
        //异步写磁盘。即实时地去写内存中的LSM tree,当数据量达到MaxTableSize时,才对数据进行compaction然后写入磁盘。当调用Close时也会把内存中的数据flush到磁盘
        
		NumVersionsToKeep:       1,
		ValueLogFileSize:        64 << 20, //单位:字节。vlog文件超过这么大时就分裂文件。64M
		ValueLogMaxEntries:      100000,
		ValueThreshold:          32,
	}
    
	//设置badger存储的文件夹
	badgerOptions.Dir = badgerPath
	badgerOptions.ValueDir = badgerPath

	//新建badger数据库
	db, err := badger.Open(badgerOptions)
	if err != nil {
		panic(err)
	}
	defer db.Close() //关闭数据库
}

注意点

1、需要保证文件夹一定存在

2、一定要保证锁文件一定没有

所以可以添加上校验的部分,实现没有文件夹就创建文件夹,有锁文件就删除锁文件

package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/dgraph-io/badger"
	"github.com/dgraph-io/badger/options"
)

func main() {
	badgerPath := "./badger"
    
    //检查文件夹是否存在,不存在创建,如果本身有个同名的文件,就删掉文件再创建文件夹
    infos, err := os.Stat(badgerPath)
	if err != nil {
		//如果文件夹不存在,需要创建文件夹
		if strings.Contains(err.Error(), "The system cannot find the file specified.") {
			fmt.Println("文件夹不存在,创建文件夹", badgerPath)
			err := os.MkdirAll(badgerPath, os.ModePerm)
			if err != nil {
				panic(err)
			}
		} else {
			panic(err)
		}
	} else if !infos.IsDir() {
		fmt.Printf("%s不是文件夹,需要创建对应的文件夹\n", badgerPath)
		err := os.MkdirAll(badgerPath, os.ModePerm)
		if err != nil {
			//如果有同名的文件,就删除掉,然后创建文件夹
			if strings.Contains(err.Error(), "The system cannot find the path specified.") {
				os.RemoveAll(badgerPath)
				err = os.MkdirAll(badgerPath, os.ModePerm)
				if err != nil {
					panic(err)
				}
			} else {
				panic(err)
			}
		}
	}
    
    //新建自定义配置
    badgerOptions := badger.Options{
		LevelOneSize:        64 << 20, //第一层大小
		LevelSizeMultiplier: 10,       //下一层是上一层的多少倍
		MaxLevels:           12,       //LSM tree最多几层
		//key存在内存中,values(实际上value指针)存在磁盘中--称为vlog file
		TableLoadingMode:        options.MemoryMap, //LSM tree完全载入内存。MemoryMap是省内存,但查询速度比LoadToRAM慢一倍
		ValueLogLoadingMode:     options.FileIO,    //使用FileIO而非MemoryMap可以节省大量内存
		MaxTableSize:            8 << 20,           //8M
		NumCompactors:           8,                 //compaction线程数
		NumLevelZeroTables:      4,
		NumLevelZeroTablesStall: 10,
		NumMemtables:            4,     
        //写操作立即反应在MemTable上,当MemTable达到一定的大小时,它被刷新到磁盘,作为一个不可变的SSTable
        
		SyncWrites:              false, 
        //异步写磁盘。即实时地去写内存中的LSM tree,当数据量达到MaxTableSize时,才对数据进行compaction然后写入磁盘。当调用Close时也会把内存中的数据flush到磁盘
        
		NumVersionsToKeep:       1,
		ValueLogFileSize:        64 << 20, //单位:字节。vlog文件超过这么大时就分裂文件。64M
		ValueLogMaxEntries:      100000,
		ValueThreshold:          32,
	}
    
	//设置badger存储的文件夹
	badgerOptions.Dir = badgerPath
	badgerOptions.ValueDir = badgerPath

    //检查锁文件是否存在,如果存在就删除锁文件
    lockfile := filepath.Join(badgerPath, "LOCK")
	_, err = os.Stat(lockfile)
	if err == nil {
		//如果文件存在,就删除文件
		os.RemoveAll(lockfile)
	}
    
	//新建badger数据库
	db, err := badger.Open(badgerOptions)
	if err != nil {
		panic(err)
	}
	defer db.Close() //关闭数据库
}

关闭数据库

//可以使用defer
defer db.Close() //关闭数据库

//或者写个函数
func Close() {
	err := db.Close()
	if err == nil {
		fmt.Println("数据库关闭成功")
	} else {
		fmt.Println("数据库关闭失败," , err)
	}
}

存储操作

写入数据

单条数据

使用函数func (db *DB) Update(fn func(txn *Txn) error) error中的匿名函数的参数txn的方法func (txn *Txn) Set(key, val []byte) error

例子:这里默认已经创建了上面的数据库,数据库为db

func main() {
	err = db.Update(func(txn *badger.Txn) error {
		return txn.Set([]byte("aaaa"), []byte("bbbbb"))
	})
	if err != nil {
		panic(err)
	}
}

注意:存入的key和value必须都是[]byte切片

多条数据

可以结合事务使用

使用NewWriteBatch()来存储

//创建一个批次写,然后用这个批次写添加数据,最后再刷新提交
wb := db.NewWriteBatch()
defer wb.Cancel()
for i := 0; i < 10; i++ {
	k := "a" + strconv.Itoa(i)
	v := "b" + strconv.Itoa(i*i)
    //使用set方法存储
	err := wb.Set([]byte(k), []byte(v))
	if err != nil {
		panic(err)
	}
}
//需要刷新了才算提交
wb.Flush()

设置TTL的写入数据

Badger 允许在键上设置可选的生存时间 (TTL) 值。

一旦 TTL 过去,密钥将不再可检索,并且将有资格进行垃圾收集。

可以使用和API 方法将 TTL 设置为time.Duration值。

这里需要使用函数func (wb *WriteBatch) SetEntry(e *Entry) error来写入数据

然后新建Entry来存储数据 这里需要用到函数func NewEntry(key, value []byte) *Entry

设置过期时间就是使用func (e *Entry) WithTTL(dur time.Duration) *Entry

wb := db.NewWriteBatch()
defer wb.Cancel()
//或者使用SetEntry()来存数据
for i := 0; i < 10; i++ {
	k := "a11" + strconv.Itoa(i)
	v := "b2" + strconv.Itoa(i*i)
	err := wb.SetEntry(badger.NewEntry([]byte(k), []byte(v)).WithMeta(0).WithTTL(5 * time.Second))//设置值的同时设置过期时间
	if err != nil {
		panic(err)
	}
}
wb.Flush()

读取数据

要读取数据,我们可以使用以下Txn.Get()方法。

db.View(func(txn *badger.Txn) error {
	for i := 0; i < 10; i++ {
		k := "a11" + strconv.Itoa(i)
		item, err := txn.Get([]byte(k))//先那道对应的item
		if err != nil {
			return err
		}
        //然后通过value方法获取具体的值
		err = item.Value(func(val []byte) error {
			ival := val
			fmt.Println(string(ival))
			return nil
		})
		if err != nil {
			break
		}
	}
	return err
})

注意

如果不存在 Txn.Get() 将会返回一个 ErrKeyNotFound 错误

Get()返回的值只在事务打开时有效。如果需要在事务外部使用值,则必须使用item.ValueCopy(buffer)将其复制到另一个字节片。

var ival []byte
err := db.View(func(txn *badger.Txn) error {
	k := "a1"
	item, err := txn.Get([]byte(k))
	if err != nil {
		return err
	}
	ival, err = item.ValueCopy(nil) //item只能在事务内部使用,如果要在事务外部使用需要通过ValueCopy
	return err
})
if err != nil {
	log.Println("无法从缓存中读取数据,", "error", err)
}
fmt.Println(ival)

存在键

利用函数func (db *DB) View(fn func(txn *Txn) error) error查找

exist := false
err = db.View(func(txn *badger.Txn) error {
	_, err := txn.Get([]byte("a12"))
	if err != nil {
		fmt.Println(err, "值不存在")
	} else {
		exist = true
	}
	return err
})
if err != nil {
	fmt.Println(err) //Key not found
}
if exist {
	fmt.Println("值存在")
}

删除键

使用Txn.Delete()方法删除 key。

wb := db.NewWriteBatch()
defer wb.Cancel()
wb.Delete([]byte("a12"))
wb.Flush()

查询操作

遍历key和value

要迭代键,我们可以使用Iterator,可以使用 Txn.NewIterator()方法获得。

迭代以按字节排序的字典顺序发生。

err = db.View(func(txn *badger.Txn) error {
	opts := badger.DefaultIteratorOptions
	opts.PrefetchValues = true //设置取出键对应的值
	opts.PrefetchSize = 10  //预设的容量
	iterator := txn.NewIterator(opts)//新建迭代器
	defer iterator.Close()
    //遍历迭代器
	for iterator.Rewind(); iterator.Valid(); iterator.Next() {
        //取出每个item
		item := iterator.Item()
        //取出item对应的key
		k := item.Key()
        //根据key取出value
		err = item.Value(func(val []byte) error {
			fmt.Println(string(k), string(val))
			return nil
		})
		if err != nil {
			return err
		}
	}
	return nil
})
if err != nil {
	fmt.Println(err)
}
/*
a10 b20
a11 b21
a110 b2100
a111 b2121
a112 b2144
a113 b2169
a114 b2196
a115 b2225
a116 b2256
a117 b2289
a118 b2324
a119 b2361
a12 b24
a120 b2400
a13 b29
a14 b216
a15 b225
a16 b236
a17 b249
a18 b264
a19 b281
*/

仅遍历keys

Badger 支持一种独特的迭代模式,称为key-only迭代。

它比常规迭代快几个数量级,因为它只涉及对 LSM 树的访问,它通常完全驻留在 RAM 中。

要启用仅键迭代,您需要将该IteratorOptions.PrefetchValues 字段设置为false

这也可用于在迭代期间对选定键进行稀疏读取,item.Value()仅在需要时调用。

err = db.View(func(txn *badger.Txn) error {
	opts := badger.DefaultIteratorOptions
	opts.PrefetchValues = false     //设置不取键对应的值
	iterator := txn.NewIterator(opts)  //新建迭代器
	defer iterator.Close()
	for iterator.Rewind(); iterator.Valid(); iterator.Next() {
		item := iterator.Item()
		k := item.Key()
		fmt.Println(string(k))
	}
	return nil
})
if err != nil {
	fmt.Println(err)
}

前缀扫描

要遍历一个键前缀,您可以组合Seek()and ValidForPrefix()

err = db.View(func(txn *badger.Txn) error {
	opts := badger.DefaultIteratorOptions
	iterator := txn.NewIterator(opts) //新建迭代器
	defer iterator.Close()
	prefix := []byte("b") //设置前缀
    //根据前缀遍历
	for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() {
		item := iterator.Item()
		k := item.Key() //获取key
        //获取value
		err = item.Value(func(val []byte) error {
			fmt.Println(string(k), string(val))
			return nil
		})
		if err != nil {
			return err
		}
	}
	return nil
})
if err != nil {
	fmt.Println(err) //Key not found
}

数据流

Badger提供了一个流框架,它可以并发地遍历数据库的全部或部分,将数据转换为自定义键值,并连续地将数据流输出,以便通过网络发送、写入磁盘,甚至写入Badger。

这是比使用单个迭代器更快的遍历Badger的方法。

Stream在管理模式和正常模式下都支持Badger。

stream := db.NewStream()
// db.NewStreamAt(readTs) 托管模式
// -- 可选设置
stream.NumGo = 16                     // 设置用于迭代的goroutine数。
stream.Prefix = []byte("a")           // 前缀仅在特定范围的键上迭代。如果设置为nil(默认值),Stream将遍历整个DB
stream.LogPrefix = "Badger.Streaming" // 日志的前缀,用来区分日志。默认为“Badger.Stream”。
// 选择匹配的key
stream.ChooseKey = func(item *badger.Item) bool {
	return bytes.HasSuffix(item.Key(), []byte("0"))//这里是收以0结尾的key才会被迭代
}
// 同时为选定的键调用KeyToList。
//这可用于将Badger数据转换为自定义键值。
//如果为niu,则使用stream.ToList,这是一个默认实现,它会选择所有有效的键值。
stream.KeyToList = nil
// -- 可选设置结束。
// Send被串行调用,而Stream被串行调用。Orchestrate正在运行。
stream.Send = func(list *pb.KVList) error {
	return proto.MarshalText(os.Stdout, list) // 把读取的信息写道输出上
}
// 运行这个流
if err := stream.Orchestrate(context.Background()); err != nil {
	fmt.Println(err)
}

事务

只读事务

只读事务使用 DB.View()方法

db.View(func(txn *badger.Txn) error {
	k := "a11"
	item, err := txn.Get([]byte(k)) //先那道对应的item
	if err != nil {
		return err
	}
	//然后通过value方法获取具体的值
	err = item.Value(func(val []byte) error {
		ival := val
		fmt.Println(string(ival))
		return nil
	})
	if err != nil {
		return err
	}
	return err
})

读写事务锁

读写事务可以使用 DB.Update()方法

err = db.Update(func(txn *badger.Txn) error {
	return txn.Set([]byte("aaaa"), []byte("bbbbb"))
})
if err != nil {
	panic(err)
}

手动管理事务

直接使用DB.NewTransaction()函数,手动创建和提交事务。它接受一个布尔参数来指定是否需要读写事务。

对于读写事务,需要调用Txn.Commit()来确保事务已提交。

对于只读事务,调用 txn.reject()就可以了。

commit()也在内部调用 txn .reject()来清除事务,因此只需调用Txn.Commit()就足以执行读写事务。

但是,如果您的代码由于某种原因(出错)没有调用Txn.Commit()。就需要在defer中调用 txn.reject()

// 启动读写事务。
txn := db.NewTransaction(true)
defer txn.Discard()
// 使用事务来设置
err = txn.Set([]byte("aaaa"), []byte("42"))
if err != nil {
	fmt.Println(err)
}
// 提交事务,检查错误
if err := txn.Commit(); err != nil {
	fmt.Println(err)
}
posted @ 2022-11-15 10:56  厚礼蝎  阅读(445)  评论(0编辑  收藏  举报