首页 > > > 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 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
在 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 的全部过程,主要包括:

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/ ,如需转载,请注明出处,否则将追究法律责任。