BoltDB 事务流程
分析的 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()
写事务会上锁控制单写,这里的 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()
读事务中会上 内存 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 的全部过程,主要包括:
- 从
root Bucket
开始,对访问过的Bucket
进行转换与分裂,让进行过插入与删除操作的 B+Tree 重新达到平衡状态; - 更新
freeList
页; - 将由当前 transaction 分配的页缓存写入磁盘,
- 将
metapage
写入磁盘;
- 事务原子性的实现
-
- 若事务未提交时出错,因为
boltdb
的操作都是在内存中进行,不会对数据库造成影响。 - 若是在
commit
的过程中出错,如写入文件失败或机器崩溃,boltdb
写入文件的顺序也保证了不会造成影响: - 只需要保证
metapage
写入不会出错,用 checksum 和 两个版本 保证出错时会使用前一个正确的版本。
- 若事务未提交时出错,因为
- 版本控制的实现
-
-
- 每一次写事务修改已有的数据的时候,回写磁盘不会覆盖
- 即 BoltDB 同时保持着两个版本的视图
- 看上篇文章可以看到图
-
此处为语雀文档,点击链接查看:https://www.yuque.com/techcats/database/bolt-page-layout
总结
着重看 txid 、 metapage 和 mmap,这就是 BoltDB MVCC机制的核心
来源: BoltDB 事务流程 · 语雀
0 条评论