美文网首页
深度部析 boltdb 实现: 2 事务与一致性

深度部析 boltdb 实现: 2 事务与一致性

作者: 董泽润 | 来源:发表于2019-06-28 17:13 被阅读0次

    boltdb 是号称支持事务的,并且支持 mvcc,那就看一下细节实现吧。总体来讲,事务只是一种标准,具体实现和传统的 oltp db 还真不一样

    如何开启事务

    // 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
    }
    

    可以参考官网,第一步就是 Begin, 参数 true 或 false 来决定是否是写事务,拿到 tx 后开始操作,最后一定是 commit,如果出错就要回滚。这块和 mysql 比较像。

    db.Update(func(tx *bolt.Tx) error {
        b, err := tx.CreateBucket([]byte("MyBucket"))
        if err != nil {
            return fmt.Errorf("create bucket: %s", err)
        }
        return nil
    })
    

    还有一种写法,就是用闭包的形式,db.Update 用来操作写事务,db.View 用来操作只读事务。

    ACID 与 MVCC

    数据库,谈起事务首先想到的就是 ACID,其中 AD 是必须满足的,CI 看业务场景来妥协,弱一致强一致等等。除此之外,大部份主流数据库都支持 MVCC, 我们分别看下如何保证的。

    Atomicity

    写写是串行的,每个 tx 要么 commit 要么 rollback,不可能存在中间状态。这里面用到了 cow 技术,开启事务时 meta 信息复制一份,写操作是写到新的 page, 然后提交时更改 metadata, 如果 rollback 释放该页。

    // 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) // 复制一份 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)
        }
    }
    

    meta 在 tx 初始化时会拷贝一份,由于 meta 页里存储 b+tree 根节点,所以相当于做了一份快照。

    // writeMeta writes the meta to the disk.
    func (tx *Tx) writeMeta() error {
        // Create a temporary buffer for the meta page.
        buf := make([]byte, tx.db.pageSize)
        p := tx.db.pageInBuffer(buf, 0)
        tx.meta.write(p)
    
        // Write the meta page to file.
        if _, err := tx.db.ops.writeAt(buf, int64(p.id)*int64(tx.db.pageSize)); err != nil {
            return err
        }
        if !tx.db.NoSync || IgnoreNoSync {
            if err := fdatasync(tx.db); err != nil {
                return err
            }
        }
    
        // Update statistics.
        tx.stats.Write++
    
        return nil
    }
    

    提交时会调用 writeMeta 写脏页,写数据时 b+tree 会分裂,root 节点可能会变,所以需要 writeMeta 写回。

    Consistency

    传统数据库,比如 MySQL 通过 redo, undo, binlog 来保证事务一致性,当系统崩溃重启后,未提交的事务如果己经写 binlog 了,那么直接提交,如果未写 binlog,那么通过 undo 来回滚。boltdb 这块理解也比较简单,写写是串行的,每次都会写新 page, 提交后都刷盘。

    Isolation

    func (db *DB) Begin(writable bool) (*Tx, error) {
        if writable {
            return db.beginRWTx()
        }
        return db.beginTx()
    }
    

    首先看,开启事务,如果是写事务调用 db.beginRWTx, 读事务调用 db.beginTx, 深入实现细节,会发现,写事务多了一把锁 db.rwlock.Lock(),也就是说写写是一把全局锁,写读,读读可以并发。可以认为 隔离性(I),满足 serialization,所以写性能肯定很差。但是读属于快照读,下面 mvcc 再说。

    Durability

    boltdb 持久化比较简单粗暴,commit 时直接调用操作系统 write 写脏页,然后调用 fdatasync 刷盘,这些很可能都是随机写。对比下其它数据库 leveldb, 顺序写日志,然后更新内存 skiplist 后就完成。写到这其实都不想再看了,实现的这么挫哪个服务敢用啊~

    MVCC

    传统数据库大多用 mvcc, 但是实现差别蛮大的。

    1. oraclemysql 使用 undo, redo 来做多版本,表里只存一份记录,相反 postgresql 表里存各个版本的数据,并没有 redo,所以表比较容易膨胀,需要定期 vaccum. boltdb 比较像 postgresql,数据写到新的页中,回滚会回收脏页,提交会释放无用的页。这里的回收与释放都是添加到 freelist 里,而不是还给操作系统。
    2. 传统数据库有 当前读快照读 两种,但 boltdb 可以认为,永远是快照读,如果有写事务 commit,先前开启的读事务永远读不到最新的数据。

    小结

    有点偏理论了,先写这些,具体在各个流程再贴代码

    相关文章

      网友评论

          本文标题:深度部析 boltdb 实现: 2 事务与一致性

          本文链接:https://www.haomeiwen.com/subject/fzjnqctx.html