BoltDB 使用入门实践
TechCats 成员/朋克程序员:看看,这就叫专业
Getting Started
安装
go get go.etcd.io/bbolt/...
会 get 两项
- go package ->
$GOPATH
bolt
command line ->$GOBIN
Open Database
使用 kv 数据库都很简单,只需要一个文件路径即可搭建完成环境。
package main
import (
"log"
bolt "go.etcd.io/bbolt"
)
func main() {
// Open the my.db data file in your current directory.
// It will be created if it doesn't exist.
db, err := bolt.Open("my.db", 0600, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
...
}
这里到 db 不支持多链接。这是因为对于 database file 一个链接保持了一个文件锁 file lock
。
如果并发,后续链接会阻塞。
可以为单个链接添加 超时控制
db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 1 * time.Second})
Transaction
本文无关
与 google 的 levelDB 不同,bbolt 支持事务。 detail bolt 优缺点:detail 同时 bbolt 出自 bolt ,没太多不同,只是 bbolt 目前还在维护。
事务
并发读写
同时只能有
- 一个读写事务
- 多个只读事务
actions⚠️:在事务开始时,会保持一个数据视图 这意味着事务处理过程中不会由于别处更改而改变
线程安全
单个事务和它所创建的所有对象(桶,键)都不是线程安全的。
建议加锁 或者 每一个 goroutine 并发都开启 一个 事务
当然,从 db
这个 bbolt 的顶级结构创建 事务 是 线程安全 的
死锁
前面提到的 读写事务 和 只读事务 拒绝相互依赖。当然也不能在同一个 goroutine 里。
死锁原因是 读写事务 需要周期性重新映射 data 文件(即database
)。这在开启只读事务时是不可行的。
读写事务
使用 db.Update
开启一个读写事务
err := db.Update(func(tx *bolt.Tx) error{
···
return nil
})
上文提过,在一个事务中 ,数据视图是一样的。 (详细解释就是,在这个函数作用域中,数据对你呈现最终一致性)
返回值
bboltdb 根据你的返回值判断事务状态,你可以添加任意逻辑并认为出错时返回 return err
bboltdb 会回滚,如果 return nil
则提交你的事务。
建议永远检查 Update
的返回值,因为他会返回如 硬盘压力 等造成事务失败的信息(这是在你的逻辑之外的情况)
⚠️:你自定义返回 error 的 error 信息同样会被传递出来。
只读事务
使用 db.View
来新建一个 只读事务
err := db.View(func(tx *bolt.Tx) error {
···
return nil
})
同上,你会获得一个一致性的数据视图。
当然,只读事务 只能检索信息,不会有任何更改。(btw,但是你可以 copy 一个 database 的副本,毕竟这只需要读数据)
批量读写事务
读写事务 db.Update
最后需要对 database
提交更改,这会等待硬盘就绪。
每一次文件读写都是和磁盘交互。这不是一个小开销。
你可以使用 db.Batch
开启一个 批处理事务。他会在最后批量提交(其实是多个 goroutines 开启 db.Batch
事务时有机会合并之后一起提交)从而减小了开销。 ⚠️:db.Batch
只对 goroutine 起效
使用 批处理事务 需要做取舍,用 幂等函数 换取 速度 ⚠️: db.Batch
在一部分事务失败的时候会尝试多次调用那些事务函数,如果不是幂等会造成不可预知的非最终一致性。
例:使用事务外的变量来使你的日志不那么奇怪
var id uint64
err := db.Batch(func(tx *bolt.Tx) error {
// Find last key in bucket, decode as bigendian uint64, increment
// by one, encode back to []byte, and add new key.
...
id = newValue
return nil
})
if err != nil {
return ...
}
fmt.Println("Allocated ID %d", id)
手动事务
可以手动进行事务的 开启 ,回滚,新建对象,提交等操作。因为本身 db.Update
和 db.View
就是他们的包装 ⚠️:手动事务记得 关闭 (Close)
开启事务使用 db.Begin(bool)
同时参数代表着是否可以写操作。如下:
- true - 读写事务
- false - 只读事务
// Start a writable transaction.
tx, err := db.Begin(true)
if err != nil {
return err
}
defer tx.Rollback()
// Use the transaction...
_, err := tx.CreateBucket([]byte("MyBucket"))
if err != nil {
return err
}
// Commit the transaction and check for error.
if err := tx.Commit(); err != nil {
return err
}
Using Store ?
Using Buckets
桶是键值对的集合。在一个桶中,键值唯一。
创建
使用 Tx.CreateBucket()
和 Tx.CreateBucketIfNotExists()
建立一个新桶(推荐使用第二个) 接受参数是 桶的名字
删除
使用 Tx.DeleteBucket()
根据桶的名字来删除
例子
func main() {
db, err := bbolt.Open("./data", 0666, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
db.Update(func(tx *bbolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("MyBucket"))
if err != nil {
return fmt.Errorf("create bucket: %v", err)
}
if err = tx.DeleteBucket([]byte("MyBucket")); err != nil {
return err
}
return nil
})
}
Using key/value pairs ?
最重要的部分,就是 kv 存储怎么使用了,首先需要一个 桶 来存储键值对。
Put
使用Bucket.Put()
来存储键值对,接收两个 []byte
类型的参数
db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
err := b.Put([]byte("answer"), []byte("42"))
return err
})
很明显,上面的例子设置了 Pair: key:answer value:42
Get
使用 Bucket.Get()
来查询键值。参数是一个 []byte
(别忘了这次我们只是查询,可以使用 只读事务)
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
v := b.Get([]byte("answer"))
fmt.Printf("The answer is: %s\n", v)
return nil
})
细心会注意到,Get
是不会返回 error
的,这是因为 Get()
一定能正常工作(除非系统错误),相应的,当返回 nil
时,查询的键值对不存在。 ⚠️:注意 0 长度的值 和 不存在键值对 的行为是不一样的。(一个返回是 nil, 一个不是)
func main() {
db, err := bolt.Open("./data.db", 0666, nil)
if err != nil {
log.Fatal(err)
}
defer db.Close()
err