Absernode
TechCats 成员/朋克程序员:看看,这就叫专业linux
go get go.etcd.io/bbolt/...
复制代码
会 get 两项android
$GOPATH
bolt
command line -> $GOBIN
使用 kv 数据库都很简单,只须要一个文件路径便可搭建完成环境。git
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
。程序员
若是并发,后续连接会阻塞。github
能够为单个连接添加 超时控制golang
db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 1 * time.Second})
复制代码
与 google 的 levelDB 不一样,bbolt 支持事务。 detail bolt 优缺点:detail 同时 bbolt 出自 bolt ,没太多不一样,只是 bbolt 目前还在维护。web
同时只能有redis
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)
同时参数表明着是否能够写操做。以下:
// 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
}
复制代码
桶是键值对的集合。在一个桶中,键值惟一。
使用 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
})
}
复制代码
最重要的部分,就是 kv 存储怎么使用了,首先须要一个 桶 来存储键值对。
使用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
使用 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 = db.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("MyBucket"))
if err != nil {
return fmt.Errorf("create bucket: %v", err)
}
if err = b.Put([]byte("answer"), []byte("42")); err != nil {
return err
}
if err = b.Put([]byte("zero"), []byte("")); err != nil {
return err
}
return nil
})
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
v := b.Get([]byte("noexists"))
fmt.Println(reflect.DeepEqual(v, nil)) // false
fmt.Println(v == nil) // true
v = b.Get([]byte("zero"))
fmt.Println(reflect.DeepEqual(v, nil)) // false
fmt.Println(v == nil) // true
return nil
})
}
复制代码
使用 Bucket.Delete()
删除键值对
db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("MyBucket"))
fmt.Println(b.Get([]byte("answer")))
err := b.Delete([]byte("answer"))
if err != nil {
return err
}
return nil
})
复制代码
⚠️: Get()
获取到的字节切片值只在当前事务(当前函数做用域)有效,若是要在其余事务中使用须要使用 copy()
将其拷贝到其余的字节切片
使用 NextSequence()
来建立自增键,见下例
// CreateUser saves u to the store. The new user ID is set on u once the data is persisted.
func (s *Store) CreateUser(u *User) error {
return s.db.Update(func(tx *bolt.Tx) error {
// Retrieve the users bucket.
// This should be created when the DB is first opened.
b := tx.Bucket([]byte("users"))
// Generate ID for the user.
// This returns an error only if the Tx is closed or not writeable.
// That can't happen in an Update() call so I ignore the error check. id, _ := b.NextSequence() u.ID = int(id) // Marshal user data into bytes. buf, err := json.Marshal(u) if err != nil { return err } // Persist bytes to users bucket. return b.Put(itob(u.ID), buf) }) } // itob returns an 8-byte big endian representation of v. func itob(v int) []byte { b := make([]byte, 8) binary.BigEndian.PutUint64(b, uint64(v)) return b } type User struct { ID int ... } 复制代码
很简单的,桶能够实现嵌套存储
func (*Bucket) CreateBucket(key []byte) (*Bucket, error)
func (*Bucket) CreateBucketIfNotExists(key []byte) (*Bucket, error)
func (*Bucket) DeleteBucket(key []byte) error
复制代码
假设您有一个多租户应用程序,其中根级别存储桶是账户存储桶。该存储桶内部有一系列账户的序列,这些账户自己就是存储桶。在序列存储桶(子桶)中,可能有许多相关的存储桶(Users,Note等)。
// createUser creates a new user in the given account.
func createUser(accountID int, u *User) error {
// Start the transaction.
tx, err := db.Begin(true)
if err != nil {
return err
}
defer tx.Rollback()
// Retrieve the root bucket for the account.
// Assume this has already been created when the account was set up.
root := tx.Bucket([]byte(strconv.FormatUint(accountID, 10)))
// Setup the users bucket.
bkt, err := root.CreateBucketIfNotExists([]byte("USERS"))
if err != nil {
return err
}
// Generate an ID for the new user.
userID, err := bkt.NextSequence()
if err != nil {
return err
}
u.ID = userID
// Marshal and save the encoded user.
if buf, err := json.Marshal(u); err != nil {
return err
} else if err := bkt.Put([]byte(strconv.FormatUint(u.ID, 10)), buf); err != nil {
return err
}
// Commit the transaction.
if err := tx.Commit(); err != nil {
return err
}
return nil
}
复制代码
在桶中,键值对根据 键 的 值是有字节序的。 使用 Bucket.Cursor()
对其进行迭代
db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte("MyBucket"))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
fmt.Printf("key=%s, value=%s\n", k, v)
}
return nil
})
复制代码
Cursor 有 5 种方法进行迭代
First()
Move to the first key.
Last()
Move to the last key.
Seek()
Move to a specific key.\
Next()
Move to the next key.\
Prev()
Move to the previous key.
每个方法都返回 (key []byte, value []byte)
两个值 当方法所指值不存在时返回 两个 nil
值,发生在如下状况:
Cursor.Next()
Cursor.Prev()
Next()
和 5. Prev()
方法而未使用 1.First()
2.Last()
3. Seek()
指定初始位置时⚠️特殊状况:当 key
为 非 nil
但 value
是 nil
是,说明这是嵌套桶,value
值是子桶,使用 Bucket.Bucket()
方法访问 子桶,参数是 key
值
db.View(func(tx *bolt.Tx) error {
c := b.Cursor()
fmt.Println(c.First())
k, v := c.Prev()
fmt.Println(k == nil, v == nil) // true,true
if k != nil && v == nil {
subBucket := b.Bucket()
// doanything
}
return nil
})
复制代码
经过使用 Cursor
咱们可以作到一些特殊的遍历,如:遍历拥有特定前缀的 键值对
db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
c := tx.Bucket([]byte("MyBucket")).Cursor()
prefix := []byte("1234")
for k, v := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, v = c.Next() {
fmt.Printf("key=%s, value=%s\n", k, v)
}
return nil
})
复制代码
在一个范围里遍历,如:使用可排序的时间编码(RFC3339)能够遍历特定日期范围的数据
db.View(func(tx *bolt.Tx) error {
// Assume our events bucket exists and has RFC3339 encoded time keys.
c := tx.Bucket([]byte("Events")).Cursor()
// Our time range spans the 90's decade. min := []byte("1990-01-01T00:00:00Z") max := []byte("2000-01-01T00:00:00Z") // Iterate over the 90's.
for k, v := c.Seek(min); k != nil && bytes.Compare(k, max) <= 0; k, v = c.Next() {
fmt.Printf("%s: %s\n", k, v)
}
return nil
})
复制代码
⚠️:Golang 实现的 RFC3339Nano 是不可排序的
在桶中有值的状况下,可使用 ForEach()
遍历
db.View(func(tx *bolt.Tx) error {
// Assume bucket exists and has keys
b := tx.Bucket([]byte("MyBucket"))
b.ForEach(func(k, v []byte) error {
fmt.Printf("key=%s, value=%s\n", k, v)
return nil
})
return nil
})
复制代码
⚠️:在 ForEach()
中遍历的键值对须要copy()
到事务外才能在事务外使用
boltdb 是一个单一的文件,因此很容易备份。你可使用Tx.writeto()
函数写一致的数据库。若是从只读事务调用这个函数,它将执行热备份,而不会阻塞其余数据库的读写操做。
默认状况下,它将使用一个常规文件句柄,该句柄将利用操做系统的页面缓存。
有关优化大于RAM数据集的信息,请参见[Tx](https://link.zhihu.com/?target=https%3A//godoc.org/go.etcd.io/bbolt%23Tx)
文档。
一个常见的用例是在HTTP上进行备份,这样您就可使用像cURL
这样的工具来进行数据库备份:
func BackupHandleFunc(w http.ResponseWriter, req *http.Request) {
err := db.View(func(tx *bolt.Tx) error {
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", `attachment; filename="my.db"`)
w.Header().Set("Content-Length", strconv.Itoa(int(tx.Size())))
_, err := tx.WriteTo(w)
return err
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
复制代码
而后您可使用此命令进行备份:
$ curl http://localhost/backup > my.db
或者你能够打开你的浏览器以http://localhost/backup,它会自动下载。
若是你想备份到另外一个文件,你可使用TX.copyfile()
辅助功能。
数据库对运行的许多内部操做保持一个运行计数,这样您就能够更好地了解发生了什么。经过捕捉两个时间点数据的快照,咱们能够看到在这个时间范围内执行了哪些操做。
例如,咱们能够用一个 goroutine 里记录统计每个 10 秒:
go func() {
// Grab the initial stats.
prev := db.Stats()
for {
// Wait for 10s.
time.Sleep(10 * time.Second)
// Grab the current stats and diff them.
stats := db.Stats()
diff := stats.Sub(&prev)
// Encode stats to JSON and print to STDERR.
json.NewEncoder(os.Stderr).Encode(diff)
// Save stats for the next loop.
prev = stats
}
}()
复制代码
将这些信息经过管道输出到监控也颇有用。
能够开启只读模式防止错误更改
db, err := bolt.Open("my.db", 0666, &bolt.Options{ReadOnly: true})
if err != nil {
log.Fatal(err)
}
复制代码
如今使用 db.Update()
等开启读写事务 将会阻塞
移动端支持由 gomobile 工具提供
Create a struct that will contain your database logic and a reference to a *bolt.DB
with a initializing constructor that takes in a filepath where the database file will be stored. Neither Android nor iOS require extra permissions or cleanup from using this method.
func NewBoltDB(filepath string) *BoltDB {
db, err := bolt.Open(filepath+"/demo.db", 0600, nil)
if err != nil {
log.Fatal(err)
}
return &BoltDB{db}
}
type BoltDB struct {
db *bolt.DB
...
}
func (b *BoltDB) Path() string {
return b.db.Path()
}
func (b *BoltDB) Close() {
b.db.Close()
}
复制代码
Database logic should be defined as methods on this wrapper struct. To initialize this struct from the native language (both platforms now sync their local storage to the cloud. These snippets disable that functionality for the database file):
String path;
if (android.os.Build.VERSION.SDK_INT >=android.os.Build.VERSION_CODES.LOLLIPOP){
path = getNoBackupFilesDir().getAbsolutePath();
} else{
path = getFilesDir().getAbsolutePath();
}
Boltmobiledemo.BoltDB boltDB = Boltmobiledemo.NewBoltDB(path)
复制代码
- (void)demo {
NSString* path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
NSUserDomainMask,
YES) objectAtIndex:0];
GoBoltmobiledemoBoltDB * demo = GoBoltmobiledemoNewBoltDB(path);
[self addSkipBackupAttributeToItemAtPath:demo.path];
//Some DB Logic would go here
[demo close];
}
- (BOOL)addSkipBackupAttributeToItemAtPath:(NSString *) filePathString
{
NSURL* URL= [NSURL fileURLWithPath: filePathString];
assert([[NSFileManager defaultManager] fileExistsAtPath: [URL path]]);
NSError *error = nil;
BOOL success = [URL setResourceValue: [NSNumber numberWithBool: YES]
forKey: NSURLIsExcludedFromBackupKey error: &error];
if(!success){
NSLog(@"Error excluding %@ from backup %@", [URL lastPathComponent], error);
}
return success;
}
复制代码
For more information on getting started with Bolt, check out the following articles:
关系数据库将数据组织成行,而且只能经过使用SQL进行访问。这种方法在存储和查询数据方面提供了灵活性,可是在解析和计划SQL语句时也会产生开销。Bolt经过字节切片键访问全部数据。这使得Bolt能够快速地经过键读取和写入数据,可是不提供将值链接在一块儿的内置支持。 大多数关系数据库(SQLite除外)都是独立于应用程序运行的独立服务器。这使您的系统具备将多个应用程序服务器链接到单个数据库服务器的灵活性,但同时也增长了在网络上序列化和传输数据的开销。Bolt做为应用程序中包含的库运行,所以全部数据访问都必须通过应用程序的过程。这使数据更接近您的应用程序,但限制了对数据的多进程访问。
LevelDB及其派生类(RocksDB,HyperLevelDB)与Bolt相似,由于它们是捆绑到应用程序中的库,可是它们的底层结构是日志结构的合并树(LSM树)。LSM树经过使用预写日志和称为SSTables的多层排序文件来优化随机写入。Bolt在内部使用B +树,而且仅使用一个文件。两种方法都须要权衡。 若是您须要较高的随机写入吞吐量(> 10,000 w / sec),或者须要使用旋转磁盘,那么LevelDB多是一个不错的选择。若是您的应用程序是大量读取或进行大量范围扫描,那么Bolt多是一个不错的选择。 另外一个重要的考虑因素是LevelDB没有事务。它支持键/值对的批量写入,而且支持读取快照,但不能使您安全地执行比较和交换操做。Bolt支持彻底可序列化的ACID事务。
Bolt最初是LMDB的端口,所以在架构上类似。二者都使用B +树,具备ACID语义和彻底可序列化的事务,并支持使用单个写入器和多个读取器的无锁MVCC。 这两个项目有些分歧。LMDB专一于原始性能,而Bolt专一于简单性和易用性。例如,出于性能考虑,LMDB容许执行几种不安全的操做,例如直接写入。Bolt选择禁止可能使数据库处于损坏状态的操做。Bolt惟一的例外是DB.NoSync
。 API也有一些区别。打开LMDB时须要最大的mmap大小,mdb_env
而Bolt会自动处理增量mmap的大小。LMDB使用多个标志来重载getter和setter函数,而Bolt将这些特殊状况拆分为本身的函数。
选择合适的工具来完成这项工做很重要,而Bolt也不例外。在评估和使用Bolt时,须要注意如下几点:
Bolt很是适合读取密集型工做负载。顺序写入性能也很快,可是随机写入可能会很慢。您可使用DB.Batch()
或添加预写日志来帮助缓解此问题。\
Bolt在内部使用B + tree,所以能够有不少随机页面访问。与旋转磁盘相比,SSD能够显着提升性能。\
尝试避免长时间运行的读取事务。Bolt使用写时复制功能,所以在旧事务使用旧页时没法回收这些旧页。\
从Bolt返回的字节片仅在事务期间有效。一旦事务被提交或回滚,它们所指向的内存就能够被新页面重用,或者能够从虚拟内存中取消映射,unexpected fault address
访问时会出现恐慌。\
Bolt在数据库文件上使用排他写锁定,所以不能被多个进程共享。\
使用时要当心Bucket.FillPercent
。为具备随机插入的存储桶设置较高的填充百分比将致使数据库的页面利用率很是差。\
一般使用较大的水桶。较小的存储桶一旦超过页面大小(一般为4KB),就会致使页面利用率降低。\
批量加载大量随机写入新的存储桶可能很慢,由于在提交事务以前页面不会拆分。建议不要在单个事务中将100,000个以上的键/值对随机插入到一个新的存储桶中。\
Bolt使用内存映射文件,所以底层操做系统能够处理数据的缓存。一般,操做系统将在内存中缓存尽量多的文件,并根据须要将内存释放给其余进程。这意味着在使用大型数据库时,Bolt可能会显示很高的内存使用率。可是,这是预料之中的,操做系统将根据须要释放内存。只要Bolt的内存映射适合进程虚拟地址空间,它就能够处理比可用物理RAM大得多的数据库。在32位系统上可能会出现问题。\
Bolt数据库中的数据结构是内存映射的,所以数据文件将是特定于字节序的。这意味着您没法将Bolt文件从小字节序计算机复制到大字节序计算机并使其正常工做。对于大多数用户而言,这不是问题,由于大多数现代CPU的字节序都不多。\
因为页面在磁盘上的布局方式,Bolt没法截断数据文件并将可用页面返回到磁盘。取而代之的是,Bolt会在其数据文件中维护未使用页面的空闲列表。这些空闲页面能够被之后的事务重用。因为数据库一般会增加,所以这在许多用例中效果很好。可是,请务必注意,删除大块数据将不容许您回收磁盘上的该空间。 有关页面分配的更多信息,请参见此注释。\
对于嵌入式,可序列化的事务性键/值数据库,Bolt是一个相对较小的代码库(<5KLOC),所以对于那些对数据库的工做方式感兴趣的人来讲,Bolt多是一个很好的起点。
最佳起点是Bolt的主要切入点:
Open()
-初始化对数据库的引用。它负责建立数据库(若是不存在),得到文件的排他锁,读取元页面以及对文件进行内存映射。\
DB.Begin()
-根据writable
参数的值启动只读或读写事务。这须要短暂得到“元”锁以跟踪未结交易。一次只能存在一个读写事务,所以在读写事务期间将得到“ rwlock”。\
Bucket.Put()
-将键/值对写入存储桶。验证参数以后,使用光标将B +树遍历到将键和值写入的页面和位置。找到位置后,存储桶会将基础页面和页面的父页面具体化为“节点”到内存中。这些节点是在读写事务期间发生突变的地方。提交期间,这些更改将刷新到磁盘。\
Bucket.Get()
-从存储桶中检索键/值对。这使用光标移动到键/值对的页面和位置。在只读事务期间,键和值数据将做为对基础mmap文件的直接引用返回,所以没有分配开销。对于读写事务,此数据能够引用mmap文件或内存节点值之一。\
Cursor
-该对象仅用于遍历磁盘页或内存节点的B +树。它能够查找特定的键,移至第一个或最后一个值,也能够向前或向后移动。光标对最终用户透明地处理B +树的上下移动。\
Tx.Commit()
-将内存中的脏节点和可用页面列表转换为要写入磁盘的页面。而后写入磁盘分为两个阶段。首先,脏页被写入磁盘并fsync()
发生。其次,写入具备递增的事务ID的新元页面,而后fsync()
发生另外一个页面 。这两个阶段的写入操做确保崩溃时会忽略部分写入的数据页,由于指向它们的元页不会被写入。部分写入的元页面是无效的,由于它们是用校验和写入的。\
若是您还有其余可能对他人有用的注释,请经过请求请求将其提交。
如下是使用Bolt的公共开源项目的列表:
If you are using Bolt in a project please send a pull request to add it to the list.