Go 每日一庫之 nutsdb

簡介

nutsdb是一個徹底由 Go 編寫的簡單、快速、可嵌入的持久化存儲。nutsdb與咱們以前介紹過的buntdb有些相似,可是支持ListSetSorted 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_21user_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😄

參考

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

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索