nutsdb
是一个彻底由 Go 编写的简单、快速、可嵌入的持久化存储。nutsdb
与咱们以前介绍过的buntdb
有些相似,可是支持List
、Set
、Sorted Set
这些数据结构。git
先安装:github
$ go get github.com/xujiajun/nutsdb
复制代码
后使用:golang
package main
import (
"fmt"
"log"
"github.com/xujiajun/nutsdb"
)
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, err := nutsdb.Open(opt)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err = db.Update(func(tx *nutsdb.Tx) error {
key := []byte("name")
val := []byte("dj")
if err := tx.Put("", key, val, 0); err != nil {
return err
}
return nil
})
if err != nil {
log.Fatal(err)
}
err = db.View(func(tx *nutsdb.Tx) error {
key := []byte("name")
if e, err := tx.Get("", key); err != nil {
return err
} else {
fmt.Println(string(e.Value))
}
return nil
})
if err != nil {
log.Fatal(err)
}
}
复制代码
看过前面介绍buntdb
文章的小伙伴会发现,nutsdb
的简单使用与buntdb
很是类似。首先打开数据库nutsdb.Open()
,经过选项指定数据库文件存放目录。数据的插入、修改和查找都是包装在一个事务方法中执行的。nutsdb
容许同时存在多个读事务。可是有写事务存在时,其余事务不能并发执行。须要修改数据的操做在db.Update()
的回调中执行,无反作用的操做在db.View()
的回调中执行。上面代码先插入一个键值对,而后读取这个键。redis
从代码咱们能够看出,因为涉及数据库操做,须要大量的错误处理。为了简洁起见,本文后面的代码省略了错误处理,在实际使用中必须加上!数据库
**桶(bucket
)**有点像命名空间的概念。在同一个桶中的键不能重复,不一样的桶能够包含相同的键。nutsdb
提供的更新和查询接口都须要传入桶名,只是咱们在最开始的例子中将桶名设置为空字符串了。服务器
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
key := []byte("name")
val := []byte("dj")
db.Update(func(tx *nutsdb.Tx) error {
tx.Put("bucket1", key, val, 0)
return nil
})
db.Update(func(tx *nutsdb.Tx) error {
tx.Put("bucket2", key, val, 0)
return nil
})
db.View(func(tx *nutsdb.Tx) error {
e, _ := tx.Get("bucket1", key)
fmt.Println("val1:", string(e.Value))
e, _ = tx.Get("bucket2", key)
fmt.Println("val2:", string(e.Value))
return nil
})
}
复制代码
运行:微信
val1: dj
val2: dj
复制代码
咱们能够将桶类比于 redis 中的 db 的概念,redis 能够在不一样的 db 中存储相同的键,可是同一个 db 的键是惟一的。经过 redis 客户端链接服务器后,使用命令select db
切换不一样的 db。数据结构
上面咱们看到使用tx.Put()
插入字段,其实tx.Put()
也用来更新(若是键已存在)。tx.Delete()
用来删除一个字段。并发
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
key := []byte("name")
val := []byte("dj")
db.Update(func(tx *nutsdb.Tx) error {
tx.Put("", key, val, 0)
return nil
})
db.View(func(tx *nutsdb.Tx) error {
e, _ := tx.Get("", key)
fmt.Println(string(e.Value))
return nil
})
db.Update(func(tx *nutsdb.Tx) error {
tx.Delete("", key)
return nil
})
db.View(func(tx *nutsdb.Tx) error {
e, err := tx.Get("", key)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(string(e.Value))
}
return nil
})
}
复制代码
删除后再次Get()
,返回err
:学习
dj
2020/04/27 22:28:19 key not found in the bucket
exit status 1
复制代码
nutsdb
支持在插入或更新键值对时设置一个过时时间。Put()
的第四个参数即为过时时间,单位 s。传 0 表示不过时:
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
key := []byte("name")
val := []byte("dj")
db.Update(func(tx *nutsdb.Tx) error {
tx.Put("", key, val, 10)
return nil
})
db.View(func(tx *nutsdb.Tx) error {
e, _ := tx.Get("", key)
fmt.Println(string(e.Value))
return nil
})
time.Sleep(10 * time.Second)
db.View(func(tx *nutsdb.Tx) error {
e, err := tx.Get("", key)
if err != nil {
log.Fatal(err)
} else {
fmt.Println(string(e.Value))
}
return nil
})
}
复制代码
插入一个数据,设置过时时间为 10s。等待 10s 以后返回err
:
dj
2020/04/27 22:31:16 key not found in the bucket
exit status 1
复制代码
在nutsdb
的每一个桶中,键是以字节顺序保存的。这使得顺序遍历异常迅速。
咱们可使用PrefixScan()
遍历具备特定前缀的键值对。它能够指定从第几个数据开始,返回多少条知足条件的数据。例如,每一个玩家在nutsdb
中保存在**user_
+ 玩家id**的键中:
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
bucket := "user_list"
prefix := "user_"
db.Update(func(tx *nutsdb.Tx) error {
for i := 1; i <= 300; i++ {
key := []byte(prefix + strconv.FormatInt(int64(i), 10))
val := []byte("dj" + strconv.FormatInt(int64(i), 10))
tx.Put(bucket, key, val, 0)
}
return nil
})
db.View(func(tx *nutsdb.Tx) error {
entries, _, _ := tx.PrefixScan(bucket, []byte(prefix), 25, 100)
for _, entry := range entries {
fmt.Println(string(entry.Key), string(entry.Value))
}
return nil
})
}
复制代码
先插入 300 条数据,而后使用PrefixScan()
从第 25 条数据开始,一共返回 100 条数据。须要注意的是,键是以字节顺序排列,因此user_21
在user_209
以后。观察输出:
...
user_208 dj208
user_209 dj209
user_21 dj21
user_210 dj210
复制代码
可使用tx.RangeScan()
只返回键在指定范围内的数据:
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
bucket := "user_list"
prefix := "user_"
db.Update(func(tx *nutsdb.Tx) error {
for i := 1; i <= 300; i++ {
key := []byte(prefix + strconv.FormatInt(int64(i), 10))
val := []byte("dj" + strconv.FormatInt(int64(i), 10))
tx.Put(bucket, key, val, 0)
}
return nil
})
db.View(func(tx *nutsdb.Tx) error {
lbound := []byte("user_100")
ubound := []byte("user_199")
entries, _ := tx.RangeScan(bucket, lbound, ubound)
for _, entry := range entries {
fmt.Println(string(entry.Key), string(entry.Value))
}
return nil
})
}
复制代码
调用tx.GetAll()
返回某个桶中全部的数据:
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
bucket := "user_list"
prefix := "user_"
db.Update(func(tx *nutsdb.Tx) error {
for i := 1; i <= 300; i++ {
key := []byte(prefix + strconv.FormatInt(int64(i), 10))
val := []byte("dj" + strconv.FormatInt(int64(i), 10))
tx.Put(bucket, key, val, 0)
}
return nil
})
db.View(func(tx *nutsdb.Tx) error {
entries, _ := tx.GetAll(bucket)
for _, entry := range entries {
fmt.Println(string(entry.Key), string(entry.Value))
}
return nil
})
}
复制代码
相比其余数据库,nutsdb
比较强大的地方在于它支持多种数据结构:list/set/sorted set
。命令主要仿造redis
命令编写。这三种结构的操做与redis
中对应的命令很是类似,本文简单介绍一下list
相关方法,set/sorted set
可自行探索。
nutsdb
中支持的list
方法以下:
LPush
:从头部插入一个元素;RPush
:从尾部插入一个元素;LPop
:从头部删除一个元素;RPop
:从尾部删除一个元素;LPeek
:返回头部第一个元素;RPeek
:返回尾部第一个元素;LRange
:返回指定索引范围内的元素;LRem
:删除指定数量的值等于特定值的项;LSet
:设置某个索引的值;Ltrim
:只保留指定索引范围内的元素,其它都移除;LSize
:返回list
长度。下面简单演示一下如何使用这些方法,每一步的操做结果都以注释写在了命令下方:
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
defer db.Close()
bucket := "list"
key := []byte("userList")
db.Update(func(tx *nutsdb.Tx) error {
// 从头部依次插入多个值,注意顺序
tx.LPush(bucket, key, []byte("user1"), []byte("user3"), []byte("user5"))
// 当前list:user5, user3, user1
// 从尾部依次插入多个值
tx.RPush(bucket, key, []byte("user7"), []byte("user9"), []byte("user11"))
// 当前list:user5, user3, user1, user7, user9, user11
return nil
})
db.Update(func(tx *nutsdb.Tx) error {
// 从头部删除一个值
tx.LPop(bucket, key)
// 当前list:user3, user1, user7, user9, user11
// 从尾部删除一个值
tx.RPop(bucket, key)
// 当前list:user3, user1, user7, user9
// 从头部删除两个值
tx.LRem(bucket, key, 2)
// 当前list:user7, user9
return nil
})
db.View(func(tx *nutsdb.Tx) error {
// 头部第一个值,user7
b, _ := tx.LPeek(bucket, key)
fmt.Println(string(b))
// 长度
l, _ := tx.LSize(bucket, key)
fmt.Println(l)
return nil
})
}
复制代码
注意不要在同一个Update
中执行插入和删除。
nutsdb
能够很方便地进行数据库备份,只须要调用db.Backup()
,传入备份存放目录便可:
func main() {
opt := nutsdb.DefaultOptions
opt.Dir = "./nutsdb"
db, _ := nutsdb.Open(opt)
key := []byte("name")
val := []byte("dj")
db.Update(func(tx *nutsdb.Tx) error {
tx.Put("", key, val, 0)
return nil
})
db.Backup("./backup")
db.Close()
opt.Dir = "./backup"
backupDB, _ := nutsdb.Open(opt)
backupDB.View(func(tx *nutsdb.Tx) error {
e, _ := tx.Get("", key)
fmt.Println(string(e.Value))
return nil
})
}
复制代码
上面先备份,再从备份中加载数据库,读取键。
你们若是发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄
个人博客:darjun.github.io
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~