Go 每日一库之 nutsdb
2020/4/28 5:03:48
本文主要是介绍Go 每日一库之 nutsdb,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
简介
nutsdb
是一个完全由 Go 编写的简单、快速、可嵌入的持久化存储。nutsdb
与我们之前介绍过的buntdb
有些类似,但是支持List
、Set
、Sorted Set
这些数据结构。
快速使用
先安装:
$ go get github.com/xujiajun/nutsdb
后使用:
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()
的回调中执行。上面代码先插入一个键值对,然后读取这个键。
从代码我们可以看出,由于涉及数据库操作,需要大量的错误处理。为了简洁起见,本文后面的代码省略了错误处理,在实际使用中必须加上!
特性
桶
桶(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😄
参考
- nutsdb GitHub:https://github.com/xujiajun/nutsdb
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~
这篇关于Go 每日一库之 nutsdb的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-20MongoDB教程:从入门到实践详解
- 2024-11-17执行 Google Ads API 查询后返回的是空数组什么原因?-icode9专业技术文章分享
- 2024-11-17google广告数据不同经理账户下的凭证可以获取对方的api数据吗?-icode9专业技术文章分享
- 2024-11-15SendGrid 的 Go 客户端库怎么实现同时向多个邮箱发送邮件?-icode9专业技术文章分享
- 2024-11-15SendGrid 的 Go 客户端库怎么设置header 和 标签tag 呢?-icode9专业技术文章分享
- 2024-11-12Cargo deny安装指路
- 2024-11-02MongoDB项目实战:从入门到初级应用
- 2024-11-01随时随地一键转录,Google Cloud 新模型 Chirp 2 让语音识别更上一层楼
- 2024-10-25Google Cloud动手实验详解:如何在Cloud Run上开发无服务器应用
- 2024-10-24AI ?先驱齐聚 BAAI 2024,发布大规模语言、多模态、具身、生物计算以及 FlagOpen 2.0 等 AI 模型创新成果。