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

2019-06-28  本文已影响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,先前开启的读事务永远读不到最新的数据。

小结

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

上一篇下一篇

猜你喜欢

热点阅读