还在篮子里

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注