百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Go 每日一库之 nutsdb(每日一狗)

mhr18 2024-11-12 11:15 19 浏览 0 评论

简介

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

快速使用

先安装:

nbsp;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

参考

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

相关推荐

Java培训机构,你选对了吗?(java培训机构官网)

如今IT行业发展迅速,不仅是大学生,甚至有些在职的员工都想学习java开发,需求量的扩大,薪资必定增长,这也是更多人选择java开发的主要原因。不过对于没有基础的学员来说,java技术不是一两天就能...

产品经理MacBook软件清单-20个实用软件

三年前开始使用MacBookPro,从此再也不想用Windows电脑了,作为生产工具,MacBook可以说是非常胜任。作为产品经理,值得拥有一台MacBook。MacBook是工作平台,要发挥更大作...

RAD Studio(Delphi) 本月隆重推出新的版本12.3

#在头条记录我的2025#自2024年9月,推出Delphi12.2版本后,本月隆重推出新的版本12.3,RADStudio12.3,包含了Delphi12.3和C++builder12.3最...

图解Java垃圾回收机制,写得非常好

什么是自动垃圾回收?自动垃圾回收是一种在堆内存中找出哪些对象在被使用,还有哪些对象没被使用,并且将后者删掉的机制。所谓使用中的对象(已引用对象),指的是程序中有指针指向的对象;而未使用中的对象(未引用...

Centos7 初始化硬盘分区、挂载(针对2T以上)添加磁盘到卷

1、通过命令fdisk-l查看硬盘信息:#fdisk-l,发现硬盘为/dev/sdb大小4T。2、如果此硬盘以前有过分区,则先对磁盘格式化。命令:mkfs.文件系统格式-f/dev/sdb...

半虚拟化如何提高服务器性能(虚拟化 半虚拟化)

半虚拟化是一种重新编译客户机操作系统(OS)将其安装在虚拟机(VM)上的一种虚拟化类型,并在主机操作系统(OS)运行的管理程序上运行。与传统的完全虚拟化相比,半虚拟化可以减少开销,并提高系统性能。虚...

HashMap底层实现原理以及线程安全实现

HashMap底层实现原理数据结构:HashMap的底层实现原理主要依赖于数组+链表+红黑树的结构。1、数组:HashMap最底层是一个数组,称为table,它存放着键值对。2、链...

long和double类型操作的非原子性探究

前言“深入java虚拟机”中提到,int等不大于32位的基本类型的操作都是原子操作,但是某些jvm对long和double类型的操作并不是原子操作,这样就会造成错误数据的出现。其实这里的某些jvm是指...

数据库DELETE 语句,还保存原有的磁盘空间

MySQL和Oracle的DELETE语句与数据存储MySQL的DELETE操作当你在MySQL中执行DELETE语句时:逻辑删除:数据从表中标记为删除,不再可见于查询结果物理...

线程池—ThreadPoolExecutor详解(线程池实战)

一、ThreadPoolExecutor简介在juc-executors框架概述的章节中,我们已经简要介绍过ThreadPoolExecutor了,通过Executors工厂,用户可以创建自己需要的执...

navicat如何使用orcale(详细步骤)

前言:看过我昨天文章的同鞋都知道最近接手另一个国企项目,数据库用的是orcale。实话实说,也有快三年没用过orcale数据库了。这期间问题不断,因为orcale日渐消沉,网上资料也是真真假假,难辨虚...

你的程序是不是慢吞吞?GraalVM来帮你飞起来性能提升秘籍大公开

各位IT圈内外的朋友们,大家好!我是你们的老朋友,头条上的IT技术博主。不知道你们有没有这样的经历:打开一个软件,半天没反应;点开一个网站,图片刷不出来;或者玩个游戏,卡顿得想砸电脑?是不是特别上火?...

大数据正当时,理解这几个术语很重要

目前,大数据的流行程度远超于我们的想象,无论是在云计算、物联网还是在人工智能领域都离不开大数据的支撑。那么大数据领域里有哪些基本概念或技术术语呢?今天我们就来聊聊那些避不开的大数据技术术语,梳理并...

秒懂列式数据库和行式数据库(列式数据库的特点)

行式数据库(Row-Based)数据按行存储,常见的行式数据库有Mysql,DB2,Oracle,Sql-server等;列数据库(Column-Based)数据存储方式按列存储,常见的列数据库有Hb...

AMD发布ROCm 6.4更新:带来了多项底层改进,但仍不支持RDNA 4

AMD宣布,对ROCm软件栈进行了更新,推出了新的迭代版本ROCm6.4。这一新版本里,AMD带来了多项底层改进,包括更新改进了ROCm的用户空间库和AMDKFD内核驱动程序之间的兼容性,使其更容易...

取消回复欢迎 发表评论: