• 还在篮子里

    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 事务流程 · 语雀