ITPub博客

首页 > > > BoltDB 事务流程

BoltDB 事务流程

作者:飘絮絮絮丶 时间:2022-03-10 14:31:34 0 删除 编辑
分析的 etcd-io/bbolt 的代码,同时基本不会改动了。 文章不会写上详细的信息,但是会留下链接供自己深入研究。

结构


首先看事务的结构体 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 >  {
        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) >  {
            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写入磁盘;
  5. 事务原子性的实现
  6. 若事务未提交时出错,因为 boltdb 的操作都是在内存中进行,不会对数据库造成影响。
  7. 若是在commit的过程中出错,如写入文件失败或机器崩溃,boltdb写入文件的顺序也保证了不会造成影响:
  8. 只需要保证 metapage 写入不会出错,用 checksum 和 两个版本 保证出错时会使用前一个正确的版本。
  9. 版本控制的实现
  10. 每一次写事务修改已有的数据的时候,回写磁盘不会覆盖
  11. 即 BoltDB 同时保持着两个版本的视图
  12. 看上篇文章可以看到图



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。


清晰大图



Refer
存储结构和并发事务特性解析:

源代码详细内在逻辑流程:

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

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69946831/viewspace-3119816/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2019-09-09

  • 博文量
    2646
  • 访问量
    42316