Go 每日一库之 nutsdb(每日一狗)
mhr18 2024-11-12 11:15 14 浏览 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
参考
- nutsdb GitHub:https://github.com/xujiajun/nutsdb
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
相关推荐
- Spring Boot3 连接 Redis 竟有这么多实用方式
-
各位互联网大厂的后端开发精英们,在日常开发中,想必大家都面临过系统性能优化的挑战。当系统数据量逐渐增大、并发请求不断增多时,如何提升系统的响应速度和稳定性,成为了我们必须攻克的难题。而Redis,这...
- 隧道 ssh -L 命令总结 和 windows端口转发配置
-
摘要:隧道ssh-L命令总结和windows端口转发配置关键词:隧道、ssh-L、端口转发、网络映射整体说明最近在项目中,因为内网的安全密级比较高,只能有一台机器连接内网数据库,推送...
- 火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer
-
火爆BOOS直聘的13个大厂Java社招面经(5年经验)助你狂拿offer综上所述,面试遇到的所有问题,整理成了一份文档,希望大家能够喜欢!!Java面试题分享(Java中高级核心知识全面解析)一、J...
- 「第五期」游服务器一二三面 秋招 米哈游
-
一面下午2点,35分钟golang内存模型golang并发模型golanggc原理过程channel用途,原理redis数据结构,底层实现跳跃表查询插入复杂度进程,线程,协程kill原理除了kil...
- RMQ——支持合并和优先级的消息队列
-
业务背景在一个项目中需要实现一个功能,商品价格发生变化时将商品价格打印在商品主图上面,那么需要在价格发生变动的时候触发合成一张带价格的图片,每一次触发合图时计算价格都是获取当前最新的价格。上游价格变化...
- Redis 中的 zset 为什么要用跳跃表,而不是B+ Tree 呢?
-
Redis中的有序集合使用的是一种叫做跳跃表(SkipList)的数据结构来实现,而不是使用B+Tree。本文将介绍为什么Redis中使用跳跃表来实现有序集合,而不是B+Tree,并且探讨跳跃表...
- 一文让你彻底搞懂 WebSocket 的原理
-
作者:木木匠转发链接:https://juejin.im/post/5c693a4f51882561fb1db0ff一、概述上一篇文章《图文深入http三次握手核心问题【思维导图】》我们分析了简单的一...
- Redis与Java整合的最佳实践
-
Redis与Java整合的最佳实践在这个数字化时代,数据处理速度决定了企业的竞争力。Redis作为一款高性能的内存数据库,以其卓越的速度和丰富的数据结构,成为Java开发者的重要伙伴。本文将带你深入了...
- Docker与Redis:轻松部署和管理你的Redis实例
-
在高速发展的云计算时代,应用程序的部署和管理变得越来越复杂。面对各种操作系统、依赖库和环境差异,开发者常常陷入“在我机器上能跑”的泥潭。然而,容器化技术的兴起,尤其是Docker的普及,彻底改变了这一...
- Java开发中的缓存策略:让程序飞得更快
-
Java开发中的缓存策略:让程序飞得更快缓存是什么?首先,让我们来聊聊什么是缓存。简单来说,缓存是一种存储机制,它将数据保存在更快速的存储介质中,以便后续使用时能够更快地访问。比如,当你打开一个网页时...
- 国庆临近,字节后端开发3+4面,终于拿到秋招第一个offer
-
字节跳动,先面了data部门,3面技术面之后hr说需要实习转正,拒绝,之后另一个部门捞起,四面技术面,已oc分享面经,希望对大家有所帮助,秋招顺利在文末分享了我为金九银十准备的备战资源库,包含了源码笔...
- “快”就一个字!Redis凭什么能让你的APP快到飞起?
-
咱们今天就来聊一个字——“快”!在这个信息爆炸、耐心越来越稀缺的时代,谁不希望自己手机里的APP点一下“嗖”就打开,刷一下“唰”就更新?谁要是敢让咱用户盯着个小圈圈干等,那简直就是在“劝退”!而说到让...
- 双十一秒杀,为何总能抢到?Redis功不可没!
-
一年一度的双十一“剁手节”,那场面,简直比春运抢票还刺激!零点的钟声一敲响,亿万个手指头在屏幕上疯狂戳戳戳,眼睛瞪得像铜铃,就为了抢到那个心心念念的半价商品、限量版宝贝。你有没有发现一个奇怪的现象?明...
- 后端开发必看!为什么说Redis是天然的幂等性?
-
你在做后端开发的时候,有没有遇到过这样的困扰:高并发场景下,同一个操作重复执行多次,导致数据混乱、业务逻辑出错?别担心,很多同行都踩过这个坑。某电商平台就曾因订单创建接口在高并发时不具备幂等性,用户多...
- 开发一个app需要哪些技术和工具
-
APP开发需要一系列技术和工具的支持,以下是对这些技术的清晰归纳和分点表示:一、前端开发技术HTML用于构建页面结构。CSS用于样式设计和布局。JavaScript用于页面交互和逻辑处理。React...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- oracle位图索引 (63)
- oracle批量插入数据 (62)
- oracle事务隔离级别 (53)
- oracle 空为0 (50)
- oracle主从同步 (55)
- oracle 乐观锁 (51)
- redis 命令 (78)
- php redis (88)
- redis 存储 (66)
- redis 锁 (69)
- 启动 redis (66)
- redis 时间 (56)
- redis 删除 (67)
- redis内存 (57)
- redis并发 (52)
- redis 主从 (69)
- redis 订阅 (51)
- redis 登录 (54)
- redis 面试 (58)
- 阿里 redis (59)
- redis 搭建 (53)
- redis的缓存 (55)
- lua redis (58)
- redis 连接池 (61)
- redis 限流 (51)