• 还在篮子里

    BoltDB Page Layout · 语雀

    BoltDB Page Layout

    尝试在尽量简洁的语言中讲解清楚,boltdb 的存储过程,分页算法。结合图片。同时,讲清楚这个,整个 boltdb 就完全清楚了,剩下的不过是 把数据 key value 放在 每一个 page 中。其他 boltdb 的部分可以自己看源码和看别人的博客了解

    BoltDB 作为无临时文件,只在内存中写的数据库,是如何通过页面分配支持完整的事务特性(ACID)使用 MVCC并发控制。

    首先是一点概念,BoltDB 是一个 K/V 数据库,他把数据都储存在文件系统的一个文件上,都是以字节切片的格式存储的,在运行中,文件在内存中组织成 B+ 树,K/V 数据储存在 名叫 Bucket 的桶中,每一个 Bucket 都是一个完整的 B+ 树。B+ 树的每一个 node 存储着数据,对应着文件中的一个(或多个连续) page,而 page 是由操作系统的页面大小决定的,一般为 4k。

    page.svg

    page-allocate.svg

    Refer

    存储结构和并发事务特性解析:https://youjiali1995.github.io/storage/boltdb/

    源代码详细内在逻辑流程:https://www.jianshu.com/p/b86a69892990

    来源: BoltDB Page Layout · 语雀

  • 还在篮子里

    BoltDB 事务流程 · 语雀

    BoltDB 事务流程

    分析的 etcd-io/bbolt 的代码,同时基本不会改动了。

    文章不会写上详细的信息,但是会留下链接供自己深入研究。

    结构

    Wiki-事务

    首先看事务的结构体 Tx

    // txid represents the internal transaction identifier.
    type txid uint64
    
    // Tx represents a read-only or read/write transaction on the database.
    // Read-only transactions can be used for retrieving values for keys and creating cursors.
    // Read/write transactions can create and remove buckets and create and remove keys.
    type Tx struct {
        writable       bool
        managed        bool
        db             *DB
        meta           *meta
        root           Bucket
        pages          map[pgid]*page
        stats          TxStats
        commitHandlers []func()
    
        // WriteFlag specifies the flag for write-related methods like WriteTo().
        // Tx opens the database file with the specified flag to copy the data.
        WriteFlag int
    }

    Update

    通常使用 db.Update 开启事务

    // Update executes a function within the context of a read-write managed transaction.
    // If no error is returned from the function then the transaction is committed.
    // If an error is returned then the entire transaction is rolled back.
    // Any error that is returned from the function or returned from the commit is
    // returned from the Update() method.
    //
    // Attempting to manually commit or rollback within the function will cause a panic.
    func (db *DB) Update(fn func(*Tx) error) error {
        t, err := db.Begin(true)
        if err != nil {
            return err
        }
    
        // Make sure the transaction rolls back in the event of a panic.
        defer func() {
            if t.db != nil {
                t.rollback()
            }
        }()
    
        // Mark as a managed tx so that the inner function cannot manually commit.
        t.managed = true
    
        // If an error is returned from the function then rollback and return error.
        err = fn(t)
        t.managed = false
        if err != nil {
            _ = t.Rollback()
            return err
        }
    
        return t.Commit()
    }

    注意到 Rollback()Commit()

    Begin

    事务可以使用

    • db.Update()db.View()) 开启。(包装的 db.Begin())
    • 手动使用 db.Begin() 开启。传入参数控制是否写事务。
    // Begin starts a new transaction.
    // Multiple read-only transactions can be used concurrently but only one
    // write transaction can be used at a time. Starting multiple write transactions
    // will cause the calls to block and be serialized until the current write
    // transaction finishes.
    func (db *DB) Begin(writable bool) (*Tx, error) {
        if writable {
            return db.beginRWTx()
        }
        return db.beginTx()
    }

    写事务的 beginRWTx()

    beginRWTx()

    写事务会上锁控制单写,这里的 rwlock 不是真的读写锁,是一个互斥锁,不然文件锁上啥事也做不了。

    // Obtain writer lock. This is released by the transaction when it closes.
    // This enforces only one writer transaction at a time.
    db.rwlock.Lock()

    注意到 init 函数初始化了事务

    t.init(db)

    读事务的 beginTx()

    beginTx()

    读事务中会上 内存 mmap 的锁,这会阻塞需要 remmap 的写事务。写事务在 Commit 时需要 更多的 page 映射在 内存里就会 remmap。

    // Obtain a read-only lock on the mmap. When the mmap is remapped it will
    // obtain a write lock so all transactions must finish before it can be
    // remapped.
    db.mmaplock.RLock()

    这意味着,耗时的读操作会降低性能,同时应尽量避免写事务频繁 remmap,

    参数

    可以通过设置初始化最初 map 的大小避免最开始的 mmap 内存映射操作。

    mmap 的 remmap 的增长逻辑是翻倍,直到内存中 mmap 大于 1g 时,每次 remmap 申请 1g

    由此可以看出, BoltDB 只适合少写多读的场景。

    Init

    detail:事务初始化信息

    // init initializes the transaction.
    func (tx *Tx) init(db *DB) {
        tx.db = db
        tx.pages = nil
    
        // Copy the meta page since it can be changed by the writer.
        tx.meta = &meta{}
        db.meta().copy(tx.meta)
    
        // Copy over the root bucket.
        tx.root = newBucket(tx)
        tx.root.bucket = &bucket{}
        *tx.root.bucket = tx.meta.root
    
        // Increment the transaction id and add a page cache for writable transactions.
        if tx.writable {
            tx.pages = make(map[pgid]*page)
            tx.meta.txid += txid(1)
        }
    }

    这里拷贝了 db.meta 实现了写事务的版本控制,每一次写事务会更新 两个 metapage 之一,同时版本号 加一

    Commit

    • B+

    commit 的过程中才会发生 b+ 树的分裂,平衡等操作。

    完成在内存中对 数据库信息 的操作之后提交。

    // Commit writes all changes to disk and updates the meta page.
    // Returns an error if a disk write error occurs, or if Commit is
    // called on a read-only transaction.
    func (tx *Tx) Commit() error {
        _assert(!tx.managed, "managed tx commit not allowed")
        if tx.db == nil {
            return ErrTxClosed
        } else if !tx.writable {
            return ErrTxNotWritable
        }
    
        // Rebalance nodes which have had deletions.
        var startTime = time.Now()
        tx.root.rebalance()
        if tx.stats.Rebalance > 0 {
            tx.stats.RebalanceTime += time.Since(startTime)
        }
    
        // spill data onto dirty pages.
        startTime = time.Now()
        if err := tx.root.spill(); err != nil {
            tx.rollback()
            return err
        }
        tx.stats.SpillTime += time.Since(startTime)
    
        // Free the old root bucket.
        tx.meta.root.root = tx.root.root
    
        // Free the old freelist because commit writes out a fresh freelist.
        if tx.meta.freelist != pgidNoFreelist {
            tx.db.freelist.free(tx.meta.txid, tx.db.page(tx.meta.freelist))
        }
    
        if !tx.db.NoFreelistSync {
            err := tx.commitFreelist()
            if err != nil {
                return err
            }
        } else {
            tx.meta.freelist = pgidNoFreelist
        }
    
        // Write dirty pages to disk.
        startTime = time.Now()
        if err := tx.write(); err != nil {
            tx.rollback()
            return err
        }
    
        // If strict mode is enabled then perform a consistency check.
        // Only the first consistency error is reported in the panic.
        if tx.db.StrictMode {
            ch := tx.Check()
            var errs []string
            for {
                err, ok := <-ch
                if !ok {
                    break
                }
                errs = append(errs, err.Error())
            }
            if len(errs) > 0 {
                panic("check fail: " + strings.Join(errs, "\n"))
            }
        }
    
        // Write meta to disk.
        if err := tx.writeMeta(); err != nil {
            tx.rollback()
            return err
        }
        tx.stats.WriteTime += time.Since(startTime)
    
        // Finalize the transaction.
        tx.close()
    
        // Execute commit handlers now that the locks have been removed.
        for _, fn := range tx.commitHandlers {
            fn()
        }
    
        return nil
    }

    Transaction Commit 的全部过程,主要包括:

    1. root Bucket 开始,对访问过的 Bucket 进行转换与分裂,让进行过插入与删除操作的 B+Tree 重新达到平衡状态;
    2. 更新freeList页;
    3. 将由当前 transaction 分配的页缓存写入磁盘,
    4. metapage写入磁盘;
    • 事务原子性的实现
      • 若事务未提交时出错,因为 boltdb 的操作都是在内存中进行,不会对数据库造成影响。
      • 若是在commit的过程中出错,如写入文件失败或机器崩溃,boltdb写入文件的顺序也保证了不会造成影响:
      • 只需要保证 metapage 写入不会出错,用 checksum 和 两个版本 保证出错时会使用前一个正确的版本。
    • 版本控制的实现
        1. 每一次写事务修改已有的数据的时候,回写磁盘不会覆盖
        2. 即 BoltDB 同时保持着两个版本的视图
        3. 看上篇文章可以看到图image.png
    此处为语雀文档,点击链接查看:https://www.yuque.com/techcats/database/bolt-page-layout

    总结

    着重看 txid 、 metapage 和 mmap,这就是 BoltDB MVCC机制的核心

    来源: BoltDB 事务流程 · 语雀

  • 还在篮子里

    How boltdb Write its Data? · 语雀

    How boltdb Write its Data?

    A Ha!

    Here’re three questions during reading the source code of BoltDB. I’ll explain our testing procedure to dive into the core of BoltDB writing machanism?

    Code link: yhyddr/quicksilver

    First

    • First, mentor asked me :”Did BoltDB have a temporary file when starting a read/write transaction”

    Following the quickstart, all data is stored in a file on disk.

    so have this question , what happened when we store or view data on this file.

    mentor meaned that some methods to change file like tempory file method ,have a copy file to update. When some operations end , it file would replace origin file to realize store/commit.

    Let’s check if boltdb use this method.

    FileSystem Notify

    I heard it from my mentor about how to watch files or diretories by simple go file.

    use this package fsnotify/fsnotify on github.

    I write a small go binary file to log current directory’s file’s change.

    package main
    
    import (
      "log"
    
      "github.com/fsnotify/fsnotify"
    )
    
    func main() {
      hang := make(chan bool)
      watcher, err := fsnotify.NewWatcher()
      if err != nil {
        log.Fatal(err)
      }
    
      watcher.Add("./")
    
      go func() {
        for {
          select {
          case e := <-watcher.Events:
            log.Println(e.Op.String(), e.Name)
          case err := <-watcher.Errors:
            log.Println(err)
          }
        }
      }()
    
      <-hang
    }
    

    use upon code and go build then we have fsnotify (I rename my binary file this name)

    it could tell us what happened on current directory.If you know how to wirte go code , you could change it to watch any file or directory you like by yoursely.

    I’ll show how it works

    Update Boltdb file

    then we need write code to store some data.This way we update boltdb file and know if there has a tempory file to replace origin file. Because it’s obviously that boltdb as simple kv database seems do not have access on system directory but just current directory and the database file.

    We Write code below to insert data :

    package main
    
    import (
      "encoding/binary"
      "log"
      "os"
      "time"
    
      bolt "go.etcd.io/bbolt"
    )
    
    var InsertNum int = 15000
    
    func main() {
      hang := make(chan bool)
      db, err := bolt.Open("./data.db", 0600, nil)
      if err != nil {
        log.Fatal(err)
      }
    
      go func() {
        times := time.Now()
        for {
          db.Update(func(tx *bolt.Tx) error {
            b, err := tx.CreateBucketIfNotExists([]byte("cats"))
            if err != nil {
              return err
            }
    
            num, err := b.NextSequence()
            log.Println(num)
            byteid := make([]byte, 8)
            binary.BigEndian.PutUint64(byteid, num)
    
            b.Put(byteid, byteid)
    
            if num == InsertNum {
              log.Println(time.Now().Sub(times))
              os.Exit(0)
            }
            return nil
          })
        }
    
      }()
    
      <-hang
    }

    attention I use variable InsertNum controll how many data could be inserted.

    then we run it.(sure you need run fsnotify first.)

    image.png

    we inserted 900 data and it cost us 20s.find that it’s just write data.db file. It means that we seems to have anwser to the first question : No!

    In addition , we also could get the answer by source code.

    Second

    We have known the first question’s answer, mentor quickly asked me the second.

    • How much data or Which size could a boltdb file store ?

    we want know how much.

    let’s find it from source code. BoltDB declare these sizes on the head of go file

    image.png

    and we got the miniFillPercent and maxFillPercent are 0.1 and 1.0, in db.go we also know about boltdb page size is determined by OS page size.

    image.png

    It means a page may be used some part.

    important that maxMmanpstep = 1 <<30 is 1 GB for remapping.

    we have know there’s no temporary file that means BoltDB use memory or called Mmap to host temporary data.

    use Mmap means , your BoltDB File should not bigger than your assignable memory space.

    Third

    From upon questions , we know a lot about bolddb. But mentor still have a question:

    • in a node , author set 50% capcity limit otherwise spill it to two nodes.Do our datafile just use 50% space what a boltdb database file hold?

    image.png

    It’s because the stragecy about node spill method to controll size of node do not overflow OS page size.

    that question need we know about the disk layout of BoltDB file . I’ll detailed explanation in my next article. Of course, we know the space ratio should not over 0.5.

    and now I just give some data for you.

    I first insert 100000 data which is auto increment key and the same value.

    1 -> 100000 total 977784 b because of BoltDB store in file as bytes.

    and we found our data file use 8.5m space.

    then I insert 100000 again.

    image.png

    100000 -> 200000 and file expand to 25.2m when I insert nearly 110000th data.

    And at last file is 25.2m when hosted 200000 data.

    image.png

    来源: How boltdb Write its Data? · 语雀

  • 还在篮子里

    BoltDB 使用入门实践 – 知乎

    BoltDB 使用入门实践

    Getting Started

    安装

    go get go.etcd.io/bbolt/...

    会 get 两项

    1. go package -> $GOPATH
    2. 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 = 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
        })
    }

    Delete

    使用 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() 将其拷贝到其他的字节切片

    Tricks

    桶的自增键

    使用 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 种方法进行迭代

    1. First() Move to the first key.
    2. Last() Move to the last key.
    3. Seek() Move to a specific key.
    4. Next() Move to the next key.
    5. Prev() Move to the previous key.

    每一个方法都返回 (key []byte, value []byte) 两个值 当方法所指值不存在时返回 两个 nil 值,发生在以下情况:

    1. 迭代到最后一个键值对时,再一次调用 Cursor.Next()
    2. 当前所指为第一个键值对时,调用 Cursor.Prev()
    3. 当使用 4.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

    在桶中有值的情况下,可以使用 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()到事务外才能在事务外使用

    Advance

    Backup

    boltdb 是一个单一的文件,所以很容易备份。你可以使用Tx.writeto()函数写一致的数据库。如果从只读事务调用这个函数,它将执行热备份,而不会阻塞其他数据库的读写操作。

    默认情况下,它将使用一个常规文件句柄,该句柄将利用操作系统的页面缓存。

    有关优化大于RAM数据集的信息,请参见Tx文档。

    一个常见的用例是在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

    或者你可以打开你的浏览器以localhost/backup,它会自动下载。

    如果你想备份到另一个文件,你可以使用TX.copyfile()辅助功能。

    Statistics

    数据库对运行的许多内部操作保持一个运行计数,这样您就可以更好地了解发生了什么。通过捕捉两个时间点数据的快照,我们可以看到在这个时间范围内执行了哪些操作。

    例如,我们可以用一个 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
        }
    }()

    将这些信息通过管道输出到监控也很有用。

    Read-Only Mode

    可以开启只读模式防止错误更改

    db, err := bolt.Open("my.db", 0666, &bolt.Options{ReadOnly: true})
    if err != nil {
        log.Fatal(err)
    }

    现在使用 db.Update() 等开启读写事务 将会阻塞

    Mobile Use

    移动端支持由 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):

    Android

    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)

    iOS

    - (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:

    与其他数据库的比较

    Postgres,MySQL和其他关系数据库

    关系数据库将数据组织成行,并且只能通过使用SQL进行访问。这种方法在存储和查询数据方面提供了灵活性,但是在解析和计划SQL语句时也会产生开销。Bolt通过字节切片键访问所有数据。这使得Bolt可以快速地通过键读取和写入数据,但是不提供将值连接在一起的内置支持。 大多数关系数据库(SQLite除外)都是独立于应用程序运行的独立服务器。这使您的系统具有将多个应用程序服务器连接到单个数据库服务器的灵活性,但同时也增加了在网络上序列化和传输数据的开销。Bolt作为应用程序中包含的库运行,因此所有数据访问都必须经过应用程序的过程。这使数据更接近您的应用程序,但限制了对数据的多进程访问。

    LevelDB,RocksDB

    LevelDB及其派生类(RocksDB,HyperLevelDB)与Bolt类似,因为它们是捆绑到应用程序中的库,但是它们的底层结构是日志结构的合并树(LSM树)。LSM树通过使用预写日志和称为SSTables的多层排序文件来优化随机写入。Bolt在内部使用B +树,并且仅使用一个文件。两种方法都需要权衡。 如果您需要较高的随机写入吞吐量(> 10,000 w / sec),或者需要使用旋转磁盘,那么LevelDB可能是一个不错的选择。如果您的应用程序是大量读取或进行大量范围扫描,那么Bolt可能是一个不错的选择。 另一个重要的考虑因素是LevelDB没有事务。它支持键/值对的批量写入,并且支持读取快照,但不能使您安全地执行比较和交换操作。Bolt支持完全可序列化的ACID事务。

    LMDB

    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的公共开源项目的列表:

    • Algernon – A HTTP/2 web server with built-in support for Lua. Uses BoltDB as the default database backend.
    • Bazil – A file system that lets your data reside where it is most convenient for it to reside.
    • bolter – Command-line app for viewing BoltDB file in your terminal.
    • boltcli – the redis-cli for boltdb with Lua script support.
    • BoltHold – An embeddable NoSQL store for Go types built on BoltDB
    • BoltStore – Session store using Bolt.
    • Boltdb Boilerplate – Boilerplate wrapper around bolt aiming to make simple calls one-liners.
    • BoltDbWeb – A web based GUI for BoltDB files.
    • bleve – A pure Go search engine similar to ElasticSearch that uses Bolt as the default storage backend.
    • btcwallet – A bitcoin wallet.
    • buckets – a bolt wrapper streamlining simple tx and key scans.
    • cayley – Cayley is an open-source graph database using Bolt as optional backend.
    • ChainStore – Simple key-value interface to a variety of storage engines organized as a chain of operations.
    • Consul – Consul is service discovery and configuration made easy. Distributed, highly available, and datacenter-aware.
    • DVID – Added Bolt as optional storage engine and testing it against Basho-tuned leveldb.
    • dcrwallet – A wallet for the Decred cryptocurrency.
    • drive – drive is an unofficial Google Drive command line client for *NIX operating systems.
    • event-shuttle – A Unix system service to collect and reliably deliver messages to Kafka.
    • Freehold – An open, secure, and lightweight platform for your files and data.
    • Go Report Card – Go code quality report cards as a (free and open source) service.
    • GoWebApp – A basic MVC web application in Go using BoltDB.
    • GoShort – GoShort is a URL shortener written in Golang and BoltDB for persistent key/value storage and for routing it’s using high performent HTTPRouter.
    • gopherpit – A web service to manage Go remote import paths with custom domains
    • Gitchain – Decentralized, peer-to-peer Git repositories aka “Git meets Bitcoin”.
    • InfluxDB – Scalable datastore for metrics, events, and real-time analytics.
    • ipLocator – A fast ip-geo-location-server using bolt with bloom filters.
    • ipxed – Web interface and api for ipxed.
    • Ironsmith – A simple, script-driven continuous integration (build – > test -> release) tool, with no external dependencies
    • Kala – Kala is a modern job scheduler optimized to run on a single node. It is persistent, JSON over HTTP API, ISO 8601 duration notation, and dependent jobs.
    • Key Value Access Langusge (KVAL) – A proposed grammar for key-value datastores offering a bbolt binding.
    • LedisDB – A high performance NoSQL, using Bolt as optional storage.
    • lru – Easy to use Bolt-backed Least-Recently-Used (LRU) read-through cache with chainable remote stores.
    • mbuckets – A Bolt wrapper that allows easy operations on multi level (nested) buckets.
    • MetricBase – Single-binary version of Graphite.
    • MuLiFS – Music Library Filesystem creates a filesystem to organise your music files.
    • NATS – NATS Streaming uses bbolt for message and metadata storage.
    • Operation Go: A Routine Mission – An online programming game for Golang using Bolt for user accounts and a leaderboard.
    • photosite/session – Sessions for a photo viewing site.
    • Prometheus Annotation Server – Annotation server for PromDash & Prometheus service monitoring system.
    • reef-pi – reef-pi is an award winning, modular, DIY reef tank controller using easy to learn electronics based on a Raspberry Pi.
    • Request Baskets – A web service to collect arbitrary HTTP requests and inspect them via REST API or simple web UI, similar to RequestBin service
    • Seaweed File System – Highly scalable distributed key~file system with O(1) disk read.
    • stow – a persistence manager for objects backed by boltdb.
    • Storm – Simple and powerful ORM for BoltDB.
    • SimpleBolt – A simple way to use BoltDB. Deals mainly with strings.
    • Skybox Analytics – A standalone funnel analysis tool for web analytics.
    • Scuttlebutt – Uses Bolt to store and process all Twitter mentions of GitHub projects.
    • tentacool – REST api server to manage system stuff (IP, DNS, Gateway…) on a linux server.
    • torrent – Full-featured BitTorrent client package and utilities in Go. BoltDB is a storage backend in development.
    • Wiki – A tiny wiki using Goji, BoltDB and Blackfriday.

    If you are using Bolt in a project please send a pull request to add it to the list.

    来源: BoltDB 使用入门实践 – 知乎