简介

nutsdb是一个完全由 Go 编写的简单、快速、可嵌入的持久化存储。nutsdb与我们之前介绍过的buntdb有些类似,但是支持ListSetSorted Set这些数据结构。

快速使用

先安装:

  1. $ go get github.com/xujiajun/nutsdb

后使用:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "github.com/xujiajun/nutsdb"
  6. )
  7. func main() {
  8. opt := nutsdb.DefaultOptions
  9. opt.Dir = "./nutsdb"
  10. db, err := nutsdb.Open(opt)
  11. if err != nil {
  12. log.Fatal(err)
  13. }
  14. defer db.Close()
  15. err = db.Update(func(tx *nutsdb.Tx) error {
  16. key := []byte("name")
  17. val := []byte("dj")
  18. if err := tx.Put("", key, val, 0); err != nil {
  19. return err
  20. }
  21. return nil
  22. })
  23. if err != nil {
  24. log.Fatal(err)
  25. }
  26. err = db.View(func(tx *nutsdb.Tx) error {
  27. key := []byte("name")
  28. if e, err := tx.Get("", key); err != nil {
  29. return err
  30. } else {
  31. fmt.Println(string(e.Value))
  32. }
  33. return nil
  34. })
  35. if err != nil {
  36. log.Fatal(err)
  37. }
  38. }

看过前面介绍buntdb文章的小伙伴会发现,nutsdb的简单使用与buntdb非常相似。首先打开数据库nutsdb.Open(),通过选项指定数据库文件存放目录。数据的插入、修改和查找都是包装在一个事务方法中执行的。nutsdb允许同时存在多个读事务。但是有写事务存在时,其他事务不能并发执行。需要修改数据的操作在db.Update()的回调中执行,无副作用的操作在db.View()的回调中执行。上面代码先插入一个键值对,然后读取这个键。

从代码我们可以看出,由于涉及数据库操作,需要大量的错误处理。为了简洁起见,本文后面的代码省略了错误处理,在实际使用中必须加上!

特性

桶(bucket有点像命名空间的概念。在同一个桶中的键不能重复,不同的桶可以包含相同的键。nutsdb提供的更新和查询接口都需要传入桶名,只是我们在最开始的例子中将桶名设置为空字符串了。

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. key := []byte("name")
  7. val := []byte("dj")
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. tx.Put("bucket1", key, val, 0)
  10. return nil
  11. })
  12. db.Update(func(tx *nutsdb.Tx) error {
  13. tx.Put("bucket2", key, val, 0)
  14. return nil
  15. })
  16. db.View(func(tx *nutsdb.Tx) error {
  17. e, _ := tx.Get("bucket1", key)
  18. fmt.Println("val1:", string(e.Value))
  19. e, _ = tx.Get("bucket2", key)
  20. fmt.Println("val2:", string(e.Value))
  21. return nil
  22. })
  23. }

运行:

  1. val1: dj
  2. val2: dj

我们可以将桶类比于 redis 中的 db 的概念,redis 可以在不同的 db 中存储相同的键,但是同一个 db 的键是唯一的。通过 redis 客户端连接服务器后,使用命令select db切换不同的 db。

更新和删除

上面我们看到使用tx.Put()插入字段,其实tx.Put()也用来更新(如果键已存在)。tx.Delete()用来删除一个字段。

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. key := []byte("name")
  7. val := []byte("dj")
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. tx.Put("", key, val, 0)
  10. return nil
  11. })
  12. db.View(func(tx *nutsdb.Tx) error {
  13. e, _ := tx.Get("", key)
  14. fmt.Println(string(e.Value))
  15. return nil
  16. })
  17. db.Update(func(tx *nutsdb.Tx) error {
  18. tx.Delete("", key)
  19. return nil
  20. })
  21. db.View(func(tx *nutsdb.Tx) error {
  22. e, err := tx.Get("", key)
  23. if err != nil {
  24. log.Fatal(err)
  25. } else {
  26. fmt.Println(string(e.Value))
  27. }
  28. return nil
  29. })
  30. }

删除后再次Get(),返回err

  1. dj
  2. 2020/04/27 22:28:19 key not found in the bucket
  3. exit status 1

过期

nutsdb支持在插入或更新键值对时设置一个过期时间。Put()的第四个参数即为过期时间,单位 s。传 0 表示不过期:

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. key := []byte("name")
  7. val := []byte("dj")
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. tx.Put("", key, val, 10)
  10. return nil
  11. })
  12. db.View(func(tx *nutsdb.Tx) error {
  13. e, _ := tx.Get("", key)
  14. fmt.Println(string(e.Value))
  15. return nil
  16. })
  17. time.Sleep(10 * time.Second)
  18. db.View(func(tx *nutsdb.Tx) error {
  19. e, err := tx.Get("", key)
  20. if err != nil {
  21. log.Fatal(err)
  22. } else {
  23. fmt.Println(string(e.Value))
  24. }
  25. return nil
  26. })
  27. }

插入一个数据,设置过期时间为 10s。等待 10s 之后返回err

  1. dj
  2. 2020/04/27 22:31:16 key not found in the bucket
  3. exit status 1

遍历

nutsdb的每个桶中,键是以字节顺序保存的。这使得顺序遍历异常迅速。

前缀遍历

我们可以使用PrefixScan()遍历具有特定前缀的键值对。它可以指定从第几个数据开始,返回多少条满足条件的数据。例如,每个玩家在nutsdb中保存在user_ + 玩家id的键中:

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. bucket := "user_list"
  7. prefix := "user_"
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. for i := 1; i <= 300; i++ {
  10. key := []byte(prefix + strconv.FormatInt(int64(i), 10))
  11. val := []byte("dj" + strconv.FormatInt(int64(i), 10))
  12. tx.Put(bucket, key, val, 0)
  13. }
  14. return nil
  15. })
  16. db.View(func(tx *nutsdb.Tx) error {
  17. entries, _, _ := tx.PrefixScan(bucket, []byte(prefix), 25, 100)
  18. for _, entry := range entries {
  19. fmt.Println(string(entry.Key), string(entry.Value))
  20. }
  21. return nil
  22. })
  23. }

先插入 300 条数据,然后使用PrefixScan()从第 25 条数据开始,一共返回 100 条数据。需要注意的是,键是以字节顺序排列,所以user_21user_209之后。观察输出:

  1. ...
  2. user_208 dj208
  3. user_209 dj209
  4. user_21 dj21
  5. user_210 dj210

范围遍历

可以使用tx.RangeScan()只返回键在指定范围内的数据:

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. bucket := "user_list"
  7. prefix := "user_"
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. for i := 1; i <= 300; i++ {
  10. key := []byte(prefix + strconv.FormatInt(int64(i), 10))
  11. val := []byte("dj" + strconv.FormatInt(int64(i), 10))
  12. tx.Put(bucket, key, val, 0)
  13. }
  14. return nil
  15. })
  16. db.View(func(tx *nutsdb.Tx) error {
  17. lbound := []byte("user_100")
  18. ubound := []byte("user_199")
  19. entries, _ := tx.RangeScan(bucket, lbound, ubound)
  20. for _, entry := range entries {
  21. fmt.Println(string(entry.Key), string(entry.Value))
  22. }
  23. return nil
  24. })
  25. }

获取全部

调用tx.GetAll()返回某个桶中所有的数据:

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. bucket := "user_list"
  7. prefix := "user_"
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. for i := 1; i <= 300; i++ {
  10. key := []byte(prefix + strconv.FormatInt(int64(i), 10))
  11. val := []byte("dj" + strconv.FormatInt(int64(i), 10))
  12. tx.Put(bucket, key, val, 0)
  13. }
  14. return nil
  15. })
  16. db.View(func(tx *nutsdb.Tx) error {
  17. entries, _ := tx.GetAll(bucket)
  18. for _, entry := range entries {
  19. fmt.Println(string(entry.Key), string(entry.Value))
  20. }
  21. return nil
  22. })
  23. }

数据结构

相比其他数据库,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长度。

下面简单演示一下如何使用这些方法,每一步的操作结果都以注释写在了命令下方:

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. defer db.Close()
  6. bucket := "list"
  7. key := []byte("userList")
  8. db.Update(func(tx *nutsdb.Tx) error {
  9. // 从头部依次插入多个值,注意顺序
  10. tx.LPush(bucket, key, []byte("user1"), []byte("user3"), []byte("user5"))
  11. // 当前list:user5, user3, user1
  12. // 从尾部依次插入多个值
  13. tx.RPush(bucket, key, []byte("user7"), []byte("user9"), []byte("user11"))
  14. // 当前list:user5, user3, user1, user7, user9, user11
  15. return nil
  16. })
  17. db.Update(func(tx *nutsdb.Tx) error {
  18. // 从头部删除一个值
  19. tx.LPop(bucket, key)
  20. // 当前list:user3, user1, user7, user9, user11
  21. // 从尾部删除一个值
  22. tx.RPop(bucket, key)
  23. // 当前list:user3, user1, user7, user9
  24. // 从头部删除两个值
  25. tx.LRem(bucket, key, 2)
  26. // 当前list:user7, user9
  27. return nil
  28. })
  29. db.View(func(tx *nutsdb.Tx) error {
  30. // 头部第一个值,user7
  31. b, _ := tx.LPeek(bucket, key)
  32. fmt.Println(string(b))
  33. // 长度
  34. l, _ := tx.LSize(bucket, key)
  35. fmt.Println(l)
  36. return nil
  37. })
  38. }

注意不要在同一个Update中执行插入和删除

数据库备份

nutsdb可以很方便地进行数据库备份,只需要调用db.Backup(),传入备份存放目录即可:

  1. func main() {
  2. opt := nutsdb.DefaultOptions
  3. opt.Dir = "./nutsdb"
  4. db, _ := nutsdb.Open(opt)
  5. key := []byte("name")
  6. val := []byte("dj")
  7. db.Update(func(tx *nutsdb.Tx) error {
  8. tx.Put("", key, val, 0)
  9. return nil
  10. })
  11. db.Backup("./backup")
  12. db.Close()
  13. opt.Dir = "./backup"
  14. backupDB, _ := nutsdb.Open(opt)
  15. backupDB.View(func(tx *nutsdb.Tx) error {
  16. e, _ := tx.Get("", key)
  17. fmt.Println(string(e.Value))
  18. return nil
  19. })
  20. }

上面先备份,再从备份中加载数据库,读取键。

总结

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. nutsdb GitHub:https://github.com/xujiajun/nutsdb
  2. Go 每日一库 GitHub:https://github.com/go-quiz/go-daily-lib