深入理解etcd(六)--- 底层数据库引擎boltdb

323 阅读30分钟

1. 引言

boltdb是单机kv数据库,所有数据都保存在一个文件中,通过内存映射的方式进行数据读取和写入,存储结构采用类B+树的组织形式,支持一写多读的事务机制,支持bucket增删改查、bucket嵌套和k/v增删改查等功能。etcd的底层存储系统就是基于boltdb实现的,只不过etcd是fork了bolt项目并基于此进行了优化。

2. 特点

boltDB 具有以下特性

  • KV 存储:采用 B+ 树索引结构,提供高效的键值对存储与查询。
  • 命名空间支持:数据存储在 Bucket 中,多个 Bucket 可以存储相同的键值对,支持嵌套的 Bucket,灵活组织数据。
  • 事务支持(ACID) :提供事务机制,确保原子性、一致性、隔离性和持久性,支持一写多读的事务模型,优化并发性能。

3. 前置知识

3.1. write/pwrite/sync/fsnyc/fdatasync

五个函数的区别

函数同步对象是否等待 I/O 完成是否同步元数据说明
write/ pwrite数据(写入页缓存)不等待写数据到内存中的页缓存,不会等待磁盘写入,异步操作。
sync所有脏页不等待将所有脏页写入磁盘,但不会等待磁盘写入完成,可能数据丢失。
fsync数据和元数据等待同步指定文件的数据和所有元数据,确保数据和元数据都写入磁盘。等待所有磁盘I/O操作完成后返回。
fdatasync数据和必要的元数据等待同步指定文件的数据和必要的元数据,减少不必要的I/O操作,等待所有磁盘I/O操作完成后返回。

3.2. mmap

如下图,mmap是把磁盘文件映射到内存,之后对磁盘的操作就变为了对内存的操作,一定会依赖pageCache的

Q1:pageCache中的数据啥时候回刷新到磁盘?

  1. 用户进程调用sync()或者fsync()
  1. 系统调用空闲内存低于特定阈值
  1. 脏页的数据在内存中驻留的时间超过一个特定阈值

4. 读写策略

boltdb 的增删改流程

  • 打开对应的db文件
  • 使用读写事务
  • 打开bucket
  • 对bucket进行增删改
  • 提交
	// 打开或创建数据库
	db, err := bbolt.Open("my.db", 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()


    err := db.Update(func(tx *bbolt.Tx) error {
        bucket, err := tx.CreateBucketIfNotExists([]byte("example"))
        if err != nil {
            return err
        }
        // 执行写入操作,修改 key1
        err = bucket.Put([]byte("key1"), []byte("value1"))
        if err != nil {
            return err
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }

读写策略:

  • 读数据库文件时,通过mmap技术将其映射到内存中,避免频繁的设备I/O;不同的数据结构在不同时刻读取mmap内存空间的数据完成构建
  • 写数据库文件时,写入mmap内存空间外的page buffer中,然后在事务提交时将page buffer中的数据通过pwrite + fdatasync系统调用写入底层文件并确保写入的安全性

5. 数据存储

在BoltDB中,每个数据库实例对应于磁盘上的一个单独文件。为了高效地管理和访问这些数据,BoltDB采用页作为基本的存储和检索单元。具体来说,所有的数据在磁盘上均按照固定的页大小进行存储,这一大小默认设置为4KB,与操作系统的内存页大小相匹配。

在boltdb中,页可以分为四类:

  • 保存元数据的meta页
  • 空闲列表页
  • 数据页,因为是b+树组织,又可以分成分支节点页和叶子节点页
page页类型类型定义类型值用途
分支节点页branchPageFlag0x01存储索引信息(页号、元素key值)
叶子节点页leafPageFlag0x02存储数据信息(页号、插入的key值、插入的value值)
元数据页metaPageFlag0x04存储数据库的元信息,例如空闲列表页id、放置桶的根页等
空闲列表页freelistPageFlag0x10存储哪些页是空闲页,可以用来后续分配空间时,优先考虑分配

boltdb中的磁盘中的数据,是像下面一样组织的

内存中的数据可以通过上述磁盘里的文件进行构建。

如图上所示,一个db文件被划分为多个页,每一页都是由头部和数据组成

每一页头部的数据是由page 结构序列化后写入

type Pgid uint64

type Page struct {
	id       Pgid
	flags    uint16
	count    uint16
	overflow uint32
}

page data的位置是通过指针计算偏移得到的位置

5.1. meta

meta 存储了数据库的元信息,例如空闲列表页id、放置桶的根页等

type Meta struct {
	magic    uint32 // 数据库的文件开头值
	version  uint32 // 数据库的版本
	pageSize uint32 // 每一页的大小
	flags    uint32 // 页类型
	root     InBucket // 根bucket,类似于表
	freelist Pgid // 空闲页列表的首页id
	pgid     Pgid // 下一个分配的页id,即当前最大页id+1,用于mmap扩容时为新页编号
	txid     Txid // 下一个事务的id,全局单调递增
	checksum uint64 // 校验和
}

下面展示meta是如何写进db文件对应的page

// Write writes the meta onto a page.
func (m *Meta) Write(p *Page) {
	if m.root.root >= m.pgid {
		panic(fmt.Sprintf("root bucket pgid (%d) above high water mark (%d)", m.root.root, m.pgid))
	} else if m.freelist >= m.pgid && m.freelist != PgidNoFreelist {
		// TODO: reject pgidNoFreeList if !NoFreelistSync
		panic(fmt.Sprintf("freelist pgid (%d) above high water mark (%d)", m.freelist, m.pgid))
	}

	// Page id is either going to be 0 or 1 which we can determine by the transaction ID.
	// 设置page id 和页 类型
    p.id = Pgid(m.txid % 2)
	p.SetFlags(MetaPageFlag)

	// Calculate the checksum.
	m.checksum = m.Sum64()

	m.Copy(p.Meta())
}

下面展示了如何从db文件对应的page 读取meta

// Meta returns a pointer to the metadata section of the page.
func (p *Page) Meta() *Meta {
	return (*Meta)(UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))
}

5.2. 分支节点页 & 叶子节点页

branch pageleaf page是boltdb中用来保存B+树节点的页

B+树的分支节点仅用来保存索引(key),而叶子节点既保存索引,又保存值(value)

boltdb它支持任意长度的key和value,因此无法直接结构化保存key和value的列表。

为了解决这一问题,branch pageleaf pagePage Body起始处是一个由定长的索引(branchPageElementleafPageElement)组成的列表,第iii个索引记录了第iii个key或key/value的起始位置与key的长度或key/value各自的长度 。

分支节点页


// branchPageElement represents a node on a branch page.
type branchPageElement struct {
	pos   uint32
	ksize uint32
	pgid  pgid
}

// key returns a byte slice of the node key.
func (n *branchPageElement) key() []byte {
	buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
	return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos]))[:n.ksize]
}

叶子节点页

// leafPageElement represents a node on a leaf page.
// 叶子节点既存储key,也存储value
type leafPageElement struct {
    flags uint32 //该值主要用来区分,是子桶叶子节点元素还是普通的key/value叶子节点元素。flags值为1时表示子桶。否则为key/value
    pos   uint32
    ksize uint32
    vsize uint32
}
// 叶子节点的key
// key returns a byte slice of the node key.
func (n *leafPageElement)     key() []byte {
    buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
    // pos~ksize
    return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos]))[:n.ksize:n.ksize]
}
// 叶子节点的value
// value returns a byte slice of the node value.
func (n *leafPageElement) value() []byte {
    buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
    // key:pos~ksize
    // value:pos+ksize~pos+ksize+vsize
    return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos+n.ksize]))[:n.vsize:n.vsize]
}

上述的组织方式是在磁盘中的组织方式,很明显在磁盘里面,这些b+树的节点数据都是经过序列化的,而在内存中的数据会磁盘的数据进行反序列化

在内存中,分支节点页和叶子节点页都是通过node来表示,只不过的区别是通过其node中的isLeaf这个字段来区分。下面和大家分析分支节点页page和内存中的node的转换关系。

  • 对叶子节点页而言,其没有children这个信息,同时也没有key信息。isLeaf字段为true,其上存储的key、value都保存在inodes中
  • 对于分支节点页而言,具有key信息,同时children也不一定为空。isLeaf字段为false,同时该节点上的数据保存在inode中
// node represents an in-memory, deserialized page.
// node 是内存中反序列化的页
type node struct {
    bucket     *Bucket // 该node所属的bucket指针,bucket 可以理解为mysql的表
    isLeaf     bool    // 当前node是否为叶子页还是分支页
    unbalanced bool		//  删除节点后,可能数据填充率过低,需要重新balance
    spilled    bool 	// 添加节点后,数据填充率可能过高,需要重新spill
    key        []byte	// 如果分支节点页,则为第一个key
    pgid       pgid		// 当前node在mmap内存中相应的页id
    parent     *node	// 父节点
    children   nodes	// 儿子节点
    inodes     inodes // 该node的内部节点,即该node所包含的元素
}

// Inode represents an internal node inside of a node.
// It can be used to point to elements in a page or point
// to an element which hasn't been added to a page yet.
// inode 表示节点页里面的数据
// 每一页都有多组分支节点 或者 页子节点
type inode struct {
    // 表示是否是子桶叶子节点还是普通叶子节点。如果flags值为1表示子桶叶子节点,否则为普通叶子节点
    flags uint32
    // 当inode为分支元素时,pgid才有值,为叶子元素时,则没值
    pgid  pgid
    key   []byte
    // 当inode为分支元素时,value为空,为叶子元素时,才有值
    value []byte
}

type inodes []inode

下面展示了node如何写入page

func (n *node) write(p *common.Page) {
	common.Assert(p.Count() == 0 && p.Flags() == 0, "node cannot be written into a not empty page")

	// 初始化页
	if n.isLeaf {
		p.SetFlags(common.LeafPageFlag)
	} else {
		p.SetFlags(common.BranchPageFlag)
	}
    
	if len(n.inodes) >= 0xFFFF {
		panic(fmt.Sprintf("inode overflow: %d (pgid=%d)", len(n.inodes), p.Id()))
	}
    // 设置页数
	p.SetCount(uint16(len(n.inodes)))

	// Stop here if there are no items to write.
	if p.Count() == 0 {
		return
	}

	common.WriteInodeToPage(n.inodes, p)

	// DEBUG ONLY: n.dump()
}

func WriteInodeToPage(inodes Inodes, p *Page) uint32 {
	// Loop over each item and write it to the page.
	// off tracks the offset into p of the start of the next data.
	off := unsafe.Sizeof(*p) + p.PageElementSize()*uintptr(len(inodes))
	isLeaf := p.IsLeafPage()
	for i, item := range inodes {
		Assert(len(item.Key()) > 0, "write: zero-length inode key")

		// Create a slice to write into of needed size and advance
		// byte pointer for next iteration.
		sz := len(item.Key()) + len(item.Value())
		b := UnsafeByteSlice(unsafe.Pointer(p), off, 0, sz)
		off += uintptr(sz)

		// Write the page element.
        // 将inode写入page
		if isLeaf {
			elem := p.LeafPageElement(uint16(i))
			elem.SetPos(uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem))))
			elem.SetFlags(item.Flags())
			elem.SetKsize(uint32(len(item.Key())))
			elem.SetVsize(uint32(len(item.Value())))
		} else {
			elem := p.BranchPageElement(uint16(i))
			elem.SetPos(uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem))))
			elem.SetKsize(uint32(len(item.Key())))
			elem.SetPgid(item.Pgid())
			Assert(elem.Pgid() != p.Id(), "write: circular dependency occurred")
		}

		// Write data for the element to the end of the page.
		l := copy(b, item.Key())
		copy(b[l:], item.Value())
	}

	return uint32(off)
}

下面展示了如何从page读取node

// read initializes the node from a page.
func (n *node) read(p *common.Page) {
	n.pgid = p.Id()
	n.isLeaf = p.IsLeafPage()
	n.inodes = common.ReadInodeFromPage(p)

	// Save first key, so we can find the node in the parent when we spill.
	if len(n.inodes) > 0 {
		n.key = n.inodes[0].Key()
		common.Assert(len(n.key) > 0, "read: zero-length node key")
	} else {
		n.key = nil
	}
}

5.3. 空闲列表

空闲列表的实现有两类:array 和 hashMap ,它的底层实现都是shared

type array struct {
	*shared

	ids []common.Pgid // all free and available free page ids.
}

type hashMap struct {
	*shared

	freePagesCount uint64                 // count of free pages(hashmap version)
	freemaps       map[uint64]pidSet      // key is the size of continuous pages(span), value is a set which contains the starting pgids of same size
	forwardMap     map[common.Pgid]uint64 // key is start pgid, value is its span size
	backwardMap    map[common.Pgid]uint64 // key is end pgid, value is its span size
}

type shared struct {
	Interface

	readonlyTXIDs []common.Txid               // all readonly transaction IDs.
	allocs        map[common.Pgid]common.Txid // mapping of Txid that allocated a pgid.
	cache         map[common.Pgid]struct{}    // fast lookup of all free and pending page ids.
	pending       map[common.Txid]*txPending  // mapping of soon-to-be free page ids by tx.
}

freelist 从page 构建

func (t *shared) Read(p *common.Page) {
	if !p.IsFreelistPage() {
		panic(fmt.Sprintf("invalid freelist page: %d, page type is %s", p.Id(), p.Typ()))
	}

    // 获取所有的空闲页id
	ids := p.FreelistPageIds()

	// Copy the list of page ids from the freelist.
	if len(ids) == 0 {
		t.Init([]common.Pgid{})
	} else {
		// copy the ids, so we don't modify on the freelist page directly
		idsCopy := make([]common.Pgid, len(ids))
		copy(idsCopy, ids)
		// Make sure they're sorted.
		sort.Sort(common.Pgids(idsCopy))

		t.Init(idsCopy)
	}
}

freelist 写入page

如果freelist中的ids个数超过了0xFFFF,name会用第一个元素来存实际的个数,同时count置为0xFFFF。否则的话,ids的个数存储在page的头信息中的count中

func (t *shared) Write(p *common.Page) {
	// Combine the old free pgids and pgids waiting on an open transaction.

	// Update the header flag.
	p.SetFlags(common.FreelistPageFlag)

	// The page.count can only hold up to 64k elements so if we overflow that
	// number then we handle it by putting the size in the first element.
	l := t.Count()
	if l == 0 {
		p.SetCount(uint16(l))
	} else if l < 0xFFFF {
		p.SetCount(uint16(l))
		data := common.UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
		ids := unsafe.Slice((*common.Pgid)(data), l)
		t.Copyall(ids)
	} else {
		p.SetCount(0xFFFF)
		data := common.UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
		ids := unsafe.Slice((*common.Pgid)(data), l+1)
		ids[0] = common.Pgid(l)
		t.Copyall(ids[1:])
	}
}

5.4. bucket

在boltdb中,bucket 可以理解为mysql的表,它表示的是key-value的集合,但是可以嵌套,同时,每个bucket也是一颗完整的b+树。

在这里,我们不需要重新改变磁盘的数据组织方式。换个角度想一下,对于bucket的name,我们是不是可以当作key来使用。对于b+树的具体数据,我们是不是可以理解为value。所以说,name->b+树,本身就是kv的一种

bucket 分为两类:inline bucket 和 常规bucket

inline bucket 指的是b+树数据放在元数据后面,也就是放在leaf 节点的value 里面

注意: inline 内联桶,kv大小都是存储在leafPageElement里面

来一张更仔细的图,关于inline bucket的

// common/bucket.go
// bucket represents the on-file representation of a bucket.
// This is stored as the "value" of a bucket key. If the bucket is small enough,
// then its root page can be stored inline in the "value", after the bucket
// header. In the case of inline buckets, the "root" will be 0.
type bucket struct {
	root     pgid   // b+树根节点所在页,也就是每一个根bucket所在页
	sequence uint64 // monotonically incrementing, used by NextSequence()
}

// bucket.go
// Bucket represents a collection of key/value pairs inside the database.
// 一组key/value的集合,也就是一个b+树
type Bucket struct {
	*common.InBucket
	tx       *Tx                   // the associated transaction
	buckets  map[string]*Bucket    // subbucket cache
	page     *common.Page          // inline page reference
	rootNode *node                 // materialized node for the root page.
	nodes    map[common.Pgid]*node // node cache

	// Sets the threshold for filling nodes when they split. By default,
	// the bucket will fill to 50% but it can be useful to increase this
	// amount if you know that your write workloads are mostly append-only.
	//
	// This is non-persisted across transactions so it must be set in every Tx.
	FillPercent float64
}

创建bucket

CreateBucketIfNotExists()、CreateBucket() 会根据指定的key来创建一个Bucket,如果指定key的Bucket已经存在,则会报错。如果指定的key之前有插入过元素,也会报错。否则的话,会在当前的Bucket中找到合适的位置,然后新建一个Bucket插入进去(这里是插入到内存,真正提交要等到事务提交)最后返回给客户端

// CreateBucket creates a new bucket at the given key and returns the new bucket.
// Returns an error if the key already exists, if the bucket name is blank, or if the bucket name is too long.
// The bucket instance is only valid for the lifetime of the transaction.
func (b *Bucket) CreateBucket(key []byte) (rb *Bucket, err error) {
	if lg := b.tx.db.Logger(); lg != discardLogger {
		lg.Debugf("Creating bucket %q", key)
		defer func() {
			if err != nil {
				lg.Errorf("Creating bucket %q failed: %v", key, err)
			} else {
				lg.Debugf("Creating bucket %q successfully", key)
			}
		}()
	}
	if b.tx.db == nil {
		return nil, errors.ErrTxClosed
	} else if !b.tx.writable {
		return nil, errors.ErrTxNotWritable
	} else if len(key) == 0 {
		return nil, errors.ErrBucketNameRequired
	}

	// Insert into node.
	// Tip: Use a new variable `newKey` instead of reusing the existing `key` to prevent
	// it from being marked as leaking, and accordingly cannot be allocated on stack.
	newKey := cloneBytes(key)

	// Move cursor to correct position.
    // 拿到游标
	c := b.Cursor()
    //   开始遍历、找到合适的位置
	k, _, flags := c.seek(newKey)

	// Return an error if there is an existing key.
	if bytes.Equal(newKey, k) {
		if (flags & common.BucketLeafFlag) != 0 {
            // 是桶,已经存在了
			return nil, errors.ErrBucketExists
		}
        // 不是桶、但key已经存在了
		return nil, errors.ErrIncompatibleValue
	}

	// Create empty, inline bucket.
	var bucket = Bucket{
		InBucket:    &common.InBucket{},
		rootNode:    &node{isLeaf: true},
		FillPercent: DefaultFillPercent,
	}
    // 拿到bucket对应的value
    // 刚创建时都是inline bucket
	var value = bucket.write()
    // c.node()方法会在内存中建立这棵树,调用n.read(page)
    // 注意是内存,不是磁盘,磁盘要等事务提交
	c.node().put(newKey, newKey, value, 0, common.BucketLeafFlag)

	// Since subbuckets are not allowed on inline buckets, we need to
	// dereference the inline page, if it exists. This will cause the bucket
	// to be treated as a regular, non-inline bucket for the rest of the tx.
	b.page = nil

	return b.Bucket(newKey), nil
}

// node returns the node that the cursor is currently positioned on.
func (c *Cursor) node() *node {
	common.Assert(len(c.stack) > 0, "accessing a node with a zero-length cursor stack")

	// If the top of the stack is a leaf node then just return it.
	if ref := &c.stack[len(c.stack)-1]; ref.node != nil && ref.isLeaf() {
		return ref.node
	}

	// Start from root and traverse down the hierarchy.
	var n = c.stack[0].node
	if n == nil {
		n = c.bucket.node(c.stack[0].page.Id(), nil)
	}
    // 非叶子节点
	for _, ref := range c.stack[:len(c.stack)-1] {
		common.Assert(!n.isLeaf, "expected branch node")
		n = n.childAt(ref.index)
	}
	common.Assert(n.isLeaf, "expected leaf node")
	return n
}

// put inserts a key/value.
// 如果put的是一个key、value的话,不需要指定pgid。
// 如果put的一个树枝节点,则需要指定pgid,不需要指定value
func (n *node) put(oldKey, newKey, value []byte, pgId common.Pgid, flags uint32) {
	if pgId >= n.bucket.tx.meta.Pgid() {
		panic(fmt.Sprintf("pgId (%d) above high water mark (%d)", pgId, n.bucket.tx.meta.Pgid()))
	} else if len(oldKey) <= 0 {
		panic("put: zero-length old key")
	} else if len(newKey) <= 0 {
		panic("put: zero-length new key")
	}

	// Find insertion index.
	index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].Key(), oldKey) != -1 })

	// Add capacity and shift nodes if we don't have an exact match and need to insert.
	exact := len(n.inodes) > 0 && index < len(n.inodes) && bytes.Equal(n.inodes[index].Key(), oldKey)
	if !exact {
		n.inodes = append(n.inodes, common.Inode{})
		copy(n.inodes[index+1:], n.inodes[index:])
	}

	inode := &n.inodes[index]
	inode.SetFlags(flags)
	inode.SetKey(newKey)
	inode.SetValue(value)
	inode.SetPgid(pgId)
	common.Assert(len(inode.Key()) > 0, "put: zero-length inode key")
}

获取bucket

// Bucket retrieves a nested bucket by name.
// Returns nil if the bucket does not exist.
// The bucket instance is only valid for the lifetime of the transaction.
func (b *Bucket) Bucket(name []byte) *Bucket {
	if b.buckets != nil {
		if child := b.buckets[string(name)]; child != nil {
			return child
		}
	}

	// Move cursor to key.
    // 通过游标找到对应的key,value,flag
	c := b.Cursor()
	k, v, flags := c.seek(name)

	// Return nil if the key doesn't exist or it is not a bucket.
	if !bytes.Equal(name, k) || (flags&common.BucketLeafFlag) == 0 {
		return nil
	}

	// Otherwise create a bucket and cache it.
    // 根据找到的value来打开桶
	var child = b.openBucket(v)
    // 缓存
	if b.buckets != nil {
		b.buckets[string(name)] = child
	}

	return child
}

// Helper method that re-interprets a sub-bucket value
// from a parent into a Bucket
func (b *Bucket) openBucket(value []byte) *Bucket {
	var child = newBucket(b.tx)

	// Unaligned access requires a copy to be made.
	const unalignedMask = unsafe.Alignof(struct {
		common.InBucket
		common.Page
	}{}) - 1
	unaligned := uintptr(unsafe.Pointer(&value[0]))&unalignedMask != 0
	if unaligned {
		value = cloneBytes(value)
	}

	// If this is a writable transaction then we need to copy the bucket entry.
	// Read-only transactions can point directly at the mmap entry.
	if b.tx.writable && !unaligned {
		child.InBucket = &common.InBucket{}
		*child.InBucket = *(*common.InBucket)(unsafe.Pointer(&value[0]))
	} else {
		child.InBucket = (*common.InBucket)(unsafe.Pointer(&value[0]))
	}

	// Save a reference to the inline page if the bucket is inline.
    // inline bucket
	if child.RootPage() == 0 {
		child.page = (*common.Page)(unsafe.Pointer(&value[common.BucketHeaderSize]))
	}

	return &child
}

删除bucket

DeleteBucket()方法用来删除一个指定key的Bucket。其内部实现逻辑是先递归的删除其子桶。然后再释放该Bucket的page,并最终从叶子节点中移除

// DeleteBucket deletes a bucket at the given key.
// Returns an error if the bucket does not exist, or if the key represents a non-bucket value.
func (b *Bucket) DeleteBucket(key []byte) (err error) {
	if lg := b.tx.db.Logger(); lg != discardLogger {
		lg.Debugf("Deleting bucket %q", key)
		defer func() {
			if err != nil {
				lg.Errorf("Deleting bucket %q failed: %v", key, err)
			} else {
				lg.Debugf("Deleting bucket %q successfully", key)
			}
		}()
	}

	if b.tx.db == nil {
		return errors.ErrTxClosed
	} else if !b.Writable() {
		return errors.ErrTxNotWritable
	}

	newKey := cloneBytes(key)

	// Move cursor to correct position.
	c := b.Cursor()
	k, _, flags := c.seek(newKey)

	// Return an error if bucket doesn't exist or is not a bucket.
	if !bytes.Equal(newKey, k) {
		return errors.ErrBucketNotFound
	} else if (flags & common.BucketLeafFlag) == 0 {
		return errors.ErrIncompatibleValue
	}

	// Recursively delete all child buckets.
	child := b.Bucket(newKey)
	err = child.ForEachBucket(func(k []byte) error {
		if err := child.DeleteBucket(k); err != nil {
			return fmt.Errorf("delete bucket: %s", err)
		}
		return nil
	})
	if err != nil {
		return err
	}

	// Remove cached copy.
	delete(b.buckets, string(newKey))

	// Release all bucket pages to freelist.
	child.nodes = nil
	child.rootNode = nil
	child.free()

	// Delete the node if we have a matching key.
	c.node().del(newKey)

	return nil
}

插入kv

// Put sets the value for a key in the bucket.
// If the key exist then its previous value will be overwritten.
// Supplied value must remain valid for the life of the transaction.
// Returns an error if the bucket was created from a read-only transaction, if the key is blank, if the key is too large, or if the value is too large.
func (b *Bucket) Put(key []byte, value []byte) (err error) {
	if lg := b.tx.db.Logger(); lg != discardLogger {
		lg.Debugf("Putting key %q", key)
		defer func() {
			if err != nil {
				lg.Errorf("Putting key %q failed: %v", key, err)
			} else {
				lg.Debugf("Putting key %q successfully", key)
			}
		}()
	}
	if b.tx.db == nil {
		return errors.ErrTxClosed
	} else if !b.Writable() {
		return errors.ErrTxNotWritable
	} else if len(key) == 0 {
		return errors.ErrKeyRequired
	} else if len(key) > MaxKeySize {
		return errors.ErrKeyTooLarge
	} else if int64(len(value)) > MaxValueSize {
		return errors.ErrValueTooLarge
	}

	// Insert into node.
	// Tip: Use a new variable `newKey` instead of reusing the existing `key` to prevent
	// it from being marked as leaking, and accordingly cannot be allocated on stack.
	newKey := cloneBytes(key)

	// Move cursor to correct position.
	c := b.Cursor()
	k, _, flags := c.seek(newKey)

	// Return an error if there is an existing key with a bucket value.
	if bytes.Equal(newKey, k) && (flags&common.BucketLeafFlag) != 0 {
		return errors.ErrIncompatibleValue
	}

	// gofail: var beforeBucketPut struct{}

	c.node().put(newKey, newKey, value, 0, 0)

	return nil
}

获取kv

// Get retrieves the value for a key in the bucket.
// Returns a nil value if the key does not exist or if the key is a nested bucket.
// The returned value is only valid for the life of the transaction.
// The returned memory is owned by bbolt and must never be modified; writing to this memory might corrupt the database.
func (b *Bucket) Get(key []byte) []byte {
	k, v, flags := b.Cursor().seek(key)

	// Return nil if this is a bucket.
	if (flags & common.BucketLeafFlag) != 0 {
		return nil
	}

	// If our target node isn't the same key as what's passed in then return nil.
	if !bytes.Equal(key, k) {
		return nil
	}
	return v
}

删除kv

// Delete removes a key from the bucket.
// If the key does not exist then nothing is done and a nil error is returned.
// Returns an error if the bucket was created from a read-only transaction.
func (b *Bucket) Delete(key []byte) error {
    if b.tx.db == nil {
        return ErrTxClosed
    } else if !b.Writable() {
        return ErrTxNotWritable
    }
    // Move cursor to correct position.
    c := b.Cursor()
    _, _, flags := c.seek(key)
    // Return an error if there is already existing bucket value.
    if (flags & bucketLeafFlag) != 0 {
        return ErrIncompatibleValue
    }
    // Delete the node if we have a matching key.
    c.node().del(key)
    return nil
}

还要一个b+树的平衡

boltdb只有在需要将B+Tree写入到文件时才需要调整B+Tree的结构,因此put和del不需要调整B+Tree的结构,实现非常简单。

boltdb的B+Tree实现中,用来调整B+Tree结构的方法有两个:rebalance和spill。rebalance用于检查node是否由于删除了inode而导致数据填充率低于阈值,并将数据填充率低于阈值的node与其兄弟节点合并,rebalance还会将只有一个孩子的根节点与该其唯一的孩子合并。spill则可以进一步分为两个步骤,spill首先会检查并将填充率过高的节点拆分为多个小节点(split),并维护B+Tree的结构,然后将更新后的节点写到新的page中。因此,在事务提交时,boltdb会先对B+Tree执行rebalance操作再执行spill操作。

5.5. cursor

Bucket的增删改查都需要依赖游标Cursor,游标cursor是Bucket用来遍历B+Tree寻找key/value的工具

前面讲的,一个Bucket逻辑上是一颗b+树,那就意味着我们可以对其进行遍历。对bucket的set、get操作,首先是要在Bucket上先找到合适的位置,然后再进行操作。而“找”这个操作(对Bucket这颗b+树的遍历)就是交由Cursor来完成的。一个cursor关联一个bucket。

type Cursor struct {
	bucket *Bucket
	stack  []elemRef
}

// Cursor creates a cursor associated with the bucket.
// The cursor is only valid as long as the transaction is open.
// Do not use a cursor after the transaction is closed.
func (b *Bucket) Cursor() *Cursor {
	// Update transaction statistics.
	b.tx.stats.IncCursorCount(1)

	// Allocate and return a cursor.
	return &Cursor{
		bucket: b,
		stack:  make([]elemRef, 0),
	}
}

cursor的操作可以分为三类:

  • 定位到某一个元素的位置
  • 在当前位置从前往后找
  • 在当前位置从后往前找
// First moves the cursor to the first item in the bucket and returns its key and value.
// If the bucket is empty then a nil key and value are returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) First() (key []byte, value []byte)
// Last moves the cursor to the last item in the bucket and returns its key and value.
// If the bucket is empty then a nil key and value are returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) Last() (key []byte, value []byte)
// Next moves the cursor to the next item in the bucket and returns its key and value.
// If the cursor is at the end of the bucket then a nil key and value are returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) Next() (key []byte, value []byte)
// Prev moves the cursor to the previous item in the bucket and returns its key and value.
// If the cursor is at the beginning of the bucket then a nil key and value are returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) Prev() (key []byte, value []byte)
// Delete removes the current key/value under the cursor from the bucket.
// Delete fails if current key/value is a bucket or if the transaction is not writable.
func (c *Cursor) Delete() error
// Seek moves the cursor to a given key and returns it.
// If the key does not exist then the next key is used. If no keys
// follow, a nil key is returned.
// The returned key and value are only valid for the life of the transaction.
func (c *Cursor) Seek(seek []byte) (key []byte, value []byte)

这里主要分析一下seek方法,其他自己看源码,不难

Seek()方法内部主要调用了seek()私有方法

func (c *Cursor) Seek(seek []byte) (key []byte, value []byte) {
	common.Assert(c.bucket.tx.db != nil, "tx closed")
    // 调用seek
	k, v, flags := c.seek(seek)

	// If we ended up after the last element of a page then move to the next one.
	if ref := &c.stack[len(c.stack)-1]; ref.index >= ref.count() {
		k, v, flags = c.next()
	}

	if k == nil {
		return nil, nil
	} else if (flags & uint32(common.BucketLeafFlag)) != 0 {
		return k, nil
	}
	return k, v
}

seek()方法的实现,该方法有三个返回值,前两个为key、value、第三个为叶子节点的类型。

前面提到在boltdb中,叶子节点元素有两种类型:一种是嵌套的子桶、一种是普通的key/value,而这二者就是通过flags来区分的。如果叶子节点元素为嵌套的子桶时,返回的flags为1,也就是bucketLeafFlag取值

// seek moves the cursor to a given key and returns it.
// If the key does not exist then the next key is used.
func (c *Cursor) seek(seek []byte) (key []byte, value []byte, flags uint32) {
	// Start from root page/node and traverse to correct page.
	c.stack = c.stack[:0]
    // 开始根据seek的key值搜索root 
    // 执行完搜索后,stack中保存了所遍历的路径
	c.search(seek, c.bucket.RootPage())

	// If this is a bucket then return a nil value.
    // 返回值
	return c.keyValue()
}

// keyValue returns the key and value of the current leaf element.
func (c *Cursor) keyValue() ([]byte, []byte, uint32) {
    //  最后一个节点为叶子节点
	ref := &c.stack[len(c.stack)-1]

	// If the cursor is pointing to the end of page/node then return nil.
	if ref.count() == 0 || ref.index >= ref.count() {
		return nil, nil, 0
	}

	// Retrieve value from node.
    // 尝试从内存取
	if ref.node != nil {
		inode := &ref.node.inodes[ref.index]
		return inode.Key(), inode.Value(), inode.Flags()
	}

	// Or retrieve value from page.
    // 从page取
	elem := ref.page.LeafPageElement(uint16(ref.index))
	return elem.Key(), elem.Value(), elem.Flags()
}

搜索的相关函数

// search recursively performs a binary search against a given page/node until it finds a given key.
func (c *Cursor) search(key []byte, pgId common.Pgid) {
    // 如果存在pgid对应的节点,则返回节点
    // 否则返回页
    // 如果是inline bucket 也返回页
	p, n := c.bucket.pageNode(pgId)
	if p != nil && !p.IsBranchPage() && !p.IsLeafPage() {
		panic(fmt.Sprintf("invalid page type: %d: %x", p.Id(), p.Flags()))
	}
	e := elemRef{page: p, node: n}
	c.stack = append(c.stack, e)

	// If we're on a leaf page/node then find the specific node.
	if e.isLeaf() {
		// 如果是叶子节点,直接搜索
		c.nsearch(key)
		return
	}

	if n != nil {
        // 节点不为空,搜索节点
		c.searchNode(key, n)
		return
	}
    // 否则搜索页
	c.searchPage(key, p)
}

func (c *Cursor) searchNode(key []byte, n *node) {
	var exact bool
	index := sort.Search(len(n.inodes), func(i int) bool {
		// TODO(benbjohnson): Optimize this range search. It's a bit hacky right now.
		// sort.Search() finds the lowest index where f() != -1 but we need the highest index.
		ret := bytes.Compare(n.inodes[i].Key(), key)
		if ret == 0 {
			exact = true
		}
		return ret != -1
	})
	if !exact && index > 0 {
		index--
	}
	c.stack[len(c.stack)-1].index = index

	// Recursively search to the next page.
	c.search(key, n.inodes[index].Pgid())
}

func (c *Cursor) searchPage(key []byte, p *common.Page) {
	// Binary search for the correct range.
	inodes := p.BranchPageElements()

	var exact bool
	index := sort.Search(int(p.Count()), func(i int) bool {
		// TODO(benbjohnson): Optimize this range search. It's a bit hacky right now.
		// sort.Search() finds the lowest index where f() != -1 but we need the highest index.
		ret := bytes.Compare(inodes[i].Key(), key)
		if ret == 0 {
			exact = true
		}
		return ret != -1
	})
	if !exact && index > 0 {
		index--
	}
	c.stack[len(c.stack)-1].index = index

	// Recursively search to the next page.
	c.search(key, inodes[index].Pgid())
}

5.6. 事务

boltdb 支持完整的事务特性(ACID),使用 MVCC 并发控制,允许多个读事务和一个写事务并发执行,但是读事务有可能会阻塞写事务。它的特点如下:

持久性: 写事务提交时,会为该事务修改的数据(dirty page)分配新的 page,写入文件。

原子性: 未提交的写事务操作都在内存中进行;提交的写事务会按照 B+ 树数据、freelist、metadata 的顺序写入文件,只有 metadata 写入成功,整个事务才算完成,只写入前两个数据对数据库无影响。

隔离性: 每个读事务开始时会获取一个版本号,读事务涉及到的 page 不会被写事务覆盖;提交的写事务会更新数据库的版本号。

写事务流程:

  • 初始化事务:基于数据库(db)创建写事务,包括复制元数据(metadata),初始化根bucket,并自增事务ID(txid)。
  • 操作B+树:从根bucket开始遍历B+树执行所需的操作,所有修改仅在内存中进行。
  • 提交事务:
    • 平衡B+树:调整B+树结构以保持平衡,在节点分裂时为每个被修改的节点分配新的页(page)。
    • 更新freelist:为freelist分配新页以管理空闲空间。
    • 持久化数据:首先将B+树的数据和freelist的数据写入文件。
    • 写入元数据:最后更新并写入元数据到文件,确保事务的持久性和一致性。

读事务流程:

  • 初始化事务:基于数据库(db)创建读事务,复制当前的元数据并初始化根bucket。
  • 注册事务:将当前读事务添加到数据库的活跃事务列表(db.txs)中。
  • 执行查找:从根bucket开始遍历B+树进行查找操作。
  • 结束事务:查找完成后,从数据库的活跃事务列表(db.txs)中移除该读事务。

boltdb 所有操作都会分配一个事务 Tx,结构如下:

// 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.
//
// IMPORTANT: You must commit or rollback transactions when you are done with
// them. Pages can not be reclaimed by the writer until no more transactions
// are using them. A long running read transaction can cause the database to
// quickly grow.
type Tx struct {
	writable       bool // 当前事务是否为读写事务
	managed        bool // 当前事务是否由boltdb 自动管理
	db             *DB
	meta           *common.Meta                 // 当前事务创建时的meta拷贝
	root           Bucket                       // 当前事务所见的root bucket的Bucket实例
	pages          map[common.Pgid]*common.Page //索引当前事务所使用的dirty page(page buffer)
	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.
	//
	// By default, the flag is unset, which works well for mostly in-memory
	// workloads. For databases that are much larger than available RAM,
	// set the flag to syscall.O_DIRECT to avoid trashing the page cache.
	WriteFlag int
}

5.6.1. 持久性

在写事务 commit 时,会为脏 node 分配新的 page,同时将之前使用的 page 释放。freelist 中维护了当前文件中的空闲 page id,分配时会寻找合适的 page

这是freelist 底层用的数据结构

type shared struct {
	Interface

	readonlyTXIDs []common.Txid               // all readonly transaction IDs.
	allocs        map[common.Pgid]common.Txid // mapping of Txid that allocated a pgid.
	cache         map[common.Pgid]struct{}    // fast lookup of all free and pending page ids.
	pending       map[common.Txid]*txPending  // mapping of soon-to-be free page ids by tx.
}

5.6.2. 原子性

事务要求完全执行或完全不执行:

若事务未提交时出错,因为 boltdb 的操作都是在内存中进行,不会对数据库造成影响。

若是在 commit 的过程中出错,如写入文件失败或机器崩溃,boltdb 写入文件的顺序也保证了不会造成影响:

  • 先写入 B+ 树数据和 freelist 数据;
  • 后写入 metadata。

因为 db 的信息如 root bucket 的位置、freelist 的位置等都保存在 metadata 中,只有成功写入 metadata 事务才算成功。 如果第一步时出错,因为数据会写在新的 page 不会覆盖原来的数据,且此时的 metadata 不变,后面的事务仍会访问之前的完整一致的数据

关键就是要保证 metadata 写入出错也不会影响数据库:

  • meta.checksum 用于检测 metadata 的 corruption。
  • metadata 交替保存在文件前2个 page 中,当发现一个新写入的 metadata 出错时会使用另一个

5.6.3. 隔离性

boltdb 支持多个读事务与一个写事务同时执行,写事务提交时会释放旧的 page,分配新的 page,只要确保分配的新 page 不会是其他读事务使用到的就能实现 Isolation

在写事务提交时,释放的老 page 有可能还会被其他读事务访问到,不能立即用于下次分配,所以放在 freelist.pending 中, 只有确保没有读事务会用到时,才将相应的 pending page 放入 freelist.ids 中用于分配。

这是freelist 底层用的数据结构

type shared struct {
	Interface

	readonlyTXIDs []common.Txid               // all readonly transaction IDs.
	allocs        map[common.Pgid]common.Txid // mapping of Txid that allocated a pgid.
	cache         map[common.Pgid]struct{}    // fast lookup of all free and pending page ids.
	pending       map[common.Txid]*txPending  // mapping of soon-to-be free page ids by tx.
}
  • share.pending: 维护了每个写事务释放的 page id。
  • share.ids: 维护了可以用于分配的 page id。

每个事务都有一个 txid,db.meta.txid 保存了最大的已提交的写事务 id:

  • 读事务: txid == db.meta.txid。
  • 写事务:txid == db.meta.txid + 1。
  • 当写事务成功提交时,会更新 metadata,也就更新了 db.meta.txid。

txid 相当于版本号,当没有读事务的 txid 比该写事务的 txid 小时,就能释放 pending page 用于分配。

读事务维护:

db会维护了正在进行的读事务

  • 创建读事务时,会追加到 db.txs:
// Keep track of transaction until it closes.
db.txs = append(db.txs, t)
  • 当读事务 rollback 时(boltdb 的读事务完成要调用 Tx.Rollback()),会从中移除:
tx.db.removeTx(tx)

写事务维护

创建写事务时,会找到 db.txs 中最小的 txid,释放 share.pending 中所有 txid 小于它的 pending page

// Free any pages associated with closed read-only transactions.
var minid txid = 0xFFFFFFFFFFFFFFFF
for _, t := range db.txs {
    if t.meta.txid < minid {
        minid = t.meta.txid
    }
}
if minid > 0 {
    // 这里传入的是 minid - 1,传入 minid 应该就行,读该版本数据的事务不会访问该版本写事务释放的 page
    db.freelist.release(minid - 1) // 会将 pending 中 txid 小于 minid - 1 的事务释放的 page 合入 ids
}

5.6.4. 事务开启

Begin方法会根据事务是否可写,调用beginRWTx方法或beginTx方法

func (db *DB) Begin(writable bool) (t *Tx, err error) {
	if lg := db.Logger(); lg != discardLogger {
		lg.Debugf("Starting a new transaction [writable: %t]", writable)
		defer func() {
			if err != nil {
				lg.Errorf("Starting a new transaction [writable: %t] failed: %v", writable, err)
			} else {
				lg.Debugf("Starting a new transaction [writable: %t] successfully", writable)
			}
		}()
	}

	if writable {
		return db.beginRWTx()
	}
	return db.beginTx()
}

func (db *DB) beginTx() (*Tx, error) {
	// Lock the meta pages while we initialize the transaction. We obtain
	// the meta lock before the mmap lock because that's the order that the
	// write transaction will obtain them.
	db.metalock.Lock()

	// 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()

	// Exit if the database is not open yet.
	if !db.opened {
		db.mmaplock.RUnlock()
		db.metalock.Unlock()
		return nil, ErrDatabaseNotOpen
	}

	// Create a transaction associated with the database.
	t := &Tx{}
	t.init(db)

	// Keep track of transaction until it closes.
    // 维护db的事务列表
	db.txs = append(db.txs, t)
	n := len(db.txs)

	// Unlock the meta pages.
	db.metalock.Unlock()

	// Update the transaction stats.
	db.statlock.Lock()
	db.stats.TxN++
	db.stats.OpenTxN = n
	db.statlock.Unlock()

	return t, nil
}

// 只读事务
func (db *DB) beginTx() (*Tx, error) {
	// Lock the meta pages while we initialize the transaction. We obtain
	// the meta lock before the mmap lock because that's the order that the
	// write transaction will obtain them.
	db.metalock.Lock()

	// 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()

	// Exit if the database is not open yet.
	if !db.opened {
		db.mmaplock.RUnlock()
		db.metalock.Unlock()
		return nil, berrors.ErrDatabaseNotOpen
	}

	// Exit if the database is not correctly mapped.
	if db.data == nil {
		db.mmaplock.RUnlock()
		db.metalock.Unlock()
		return nil, berrors.ErrInvalidMapping
	}

	// Create a transaction associated with the database.
	t := &Tx{}
	t.init(db)

	// Keep track of transaction until it closes.
	db.txs = append(db.txs, t)
	n := len(db.txs)
	if db.freelist != nil {
		db.freelist.AddReadonlyTXID(t.meta.Txid())
	}

	// Unlock the meta pages.
	db.metalock.Unlock()

	// Update the transaction stats.
	db.statlock.Lock()
	db.stats.TxN++
	db.stats.OpenTxN = n
	db.statlock.Unlock()

	return t, nil
}

init方法初始化了Tx的一些字段。因为boltdb支持事务读写并发,所以其深拷贝了事务创建时的meta数据与root bucket的元数据,以避免只读事务读取到后续读写事务更新过的元数据。

// 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 = &common.Meta{}
    // 元数据拷贝
	db.meta().Copy(tx.meta)

	// Copy over the root bucket.
	tx.root = newBucket(tx)
	tx.root.InBucket = &common.InBucket{}
	*tx.root.InBucket = *(tx.meta.RootBucket())

	// Increment the transaction id and add a page cache for writable transactions.
	if tx.writable {
		tx.pages = make(map[common.Pgid]*common.Page)
		tx.meta.IncTxid()
	}
}

5.6.5. 提交事务

boltdb的用户可以通过Tx的Commit方法提交非隐式事务;而隐式事务的提交则由boltdb调用该方法实现(在调用前会将其managed字段置为false以避免返回错误)。在提交前,用户还可以通过OnCommit方法注册事务的回调方法

func (tx *Tx) Commit() (err error) {
	txId := tx.ID()
	lg := tx.db.Logger()
	if lg != discardLogger {
		lg.Debugf("Committing transaction %d", txId)
		defer func() {
			if err != nil {
				lg.Errorf("Committing transaction failed: %v", err)
			} else {
				lg.Debugf("Committing transaction %d successfully", txId)
			}
		}()
	}

	common.Assert(!tx.managed, "managed tx commit not allowed")
	if tx.db == nil {
		// 是否已关闭
		return berrors.ErrTxClosed
	} else if !tx.writable {
		// 是否读写事务
		return berrors.ErrTxNotWritable
	}

	// TODO(benbjohnson): Use vectorized I/O to write out dirty pages.

	// Rebalance nodes which have had deletions.
	// 执行rebalance 和 spill
	var startTime = time.Now()
	tx.root.rebalance()
	if tx.stats.GetRebalance() > 0 {
		tx.stats.IncRebalanceTime(time.Since(startTime))
	}

	opgid := tx.meta.Pgid()

	// spill data onto dirty pages.
	startTime = time.Now()
	if err = tx.root.spill(); err != nil {
		lg.Errorf("spilling data onto dirty pages failed: %v", err)
		tx.rollback()
		return err
	}
	tx.stats.IncSpillTime(time.Since(startTime))

	// Free the old root bucket.
	// 将当前事务meta中root bucket的pgid指向copy-on-write后新的root bucket
	tx.meta.RootBucket().SetRootPage(tx.root.RootPage())

	// Free the old freelist because commit writes out a fresh freelist.
	// 释放旧freelist所在page,并为其分配新page,将其写入相应的page buffer中
	if tx.meta.Freelist() != common.PgidNoFreelist {
		tx.db.freelist.Free(tx.meta.Txid(), tx.db.page(tx.meta.Freelist()))
	}

	if !tx.db.NoFreelistSync {
		err = tx.commitFreelist()
		if err != nil {
			lg.Errorf("committing freelist failed: %v", err)
			return err
		}
	} else {
		tx.meta.SetFreelist(common.PgidNoFreelist)
	}

	// If the high water mark has moved up then attempt to grow the database.
	// 检查当前已使用的空间大小是否超过了底层数据库文件大小,
	// 如果超过了该大小需要通过grow方法增大数据库文件大小
	if tx.meta.Pgid() > opgid {
		_ = errors.New("")
		// gofail: var lackOfDiskSpace string
		// tx.rollback()
		// return errors.New(lackOfDiskSpace)
		if err = tx.db.grow(int(tx.meta.Pgid()+1) * tx.db.pageSize); err != nil {
			lg.Errorf("growing db size failed, pgid: %d, pagesize: %d, error: %v", tx.meta.Pgid(), tx.db.pageSize, err)
			tx.rollback()
			return err
		}
	}

	// Write dirty pages to disk.
	startTime = time.Now()
	// 调用Tx的write方法,通过pwrite+fdatasync系统调用将dirty page写入的层文件
	if err = tx.write(); err != nil {
		lg.Errorf("writing data failed: %v", err)
		tx.rollback()
		return err
	}

	// If strict mode is enabled then perform a consistency check.
	// 如果数据库处于严格模式StructMode,调用Tx的Check方法对数据库进行完整性检查
	if tx.db.StrictMode {
		ch := tx.Check()
		var errs []string
		for {
			chkErr, ok := <-ch
			if !ok {
				break
			}
			errs = append(errs, chkErr.Error())
		}
		if len(errs) > 0 {
			panic("check fail: " + strings.Join(errs, "\n"))
		}
	}

	// Write meta to disk.
	// 调用Tx的writeMeta方法,通过pwrite+fdatasync系统调用将meta page写入的层文件
	if err = tx.writeMeta(); err != nil {
		lg.Errorf("writeMeta failed: %v", err)
		tx.rollback()
		return err
	}
	tx.stats.IncWriteTime(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
}

func (tx *Tx) close() {
	if tx.db == nil {
		return
	}
	if tx.writable {
		// Grab freelist stats.
		var freelistFreeN = tx.db.freelist.FreeCount()
		var freelistPendingN = tx.db.freelist.PendingCount()
		var freelistAlloc = tx.db.freelist.EstimatedWritePageSize()

		// Remove transaction ref & writer lock.
		tx.db.rwtx = nil
		tx.db.rwlock.Unlock()

		// Merge statistics.
		tx.db.statlock.Lock()
		tx.db.stats.FreePageN = freelistFreeN
		tx.db.stats.PendingPageN = freelistPendingN
		tx.db.stats.FreeAlloc = (freelistFreeN + freelistPendingN) * tx.db.pageSize
		tx.db.stats.FreelistInuse = freelistAlloc
		tx.db.stats.TxStats.add(&tx.stats)
		tx.db.statlock.Unlock()
	} else {
        // 移除事务
		tx.db.removeTx(tx)
	}

	// Clear all references.
	tx.db = nil
	tx.meta = nil
	tx.root = Bucket{tx: tx}
	tx.pages = nil
}

// removeTx removes a transaction from the database.
func (db *DB) removeTx(tx *Tx) {
	// Release the read lock on the mmap.
	db.mmaplock.RUnlock()

	// Use the meta lock to restrict access to the DB object.
	db.metalock.Lock()

	// Remove the transaction.
	for i, t := range db.txs {
		if t == tx {
			last := len(db.txs) - 1
			db.txs[i] = db.txs[last]
			db.txs[last] = nil
			db.txs = db.txs[:last]
			break
		}
	}
	n := len(db.txs)
	if db.freelist != nil {
        // 释放不需要的page
		db.freelist.RemoveReadonlyTXID(tx.meta.Txid())
	}

	// Unlock the meta pages.
	db.metalock.Unlock()

	// Merge statistics.
	db.statlock.Lock()
	db.stats.OpenTxN = n
	db.stats.TxStats.add(&tx.stats)
	db.statlock.Unlock()
}

5.6.6. 回滚事务

// Rollback closes the transaction and ignores all previous updates. Read-only
// transactions must be rolled back and not committed.
func (tx *Tx) Rollback() error {
	common.Assert(!tx.managed, "managed tx rollback not allowed")
	if tx.db == nil {
		return berrors.ErrTxClosed
	}
	tx.nonPhysicalRollback()
	return nil
}

// nonPhysicalRollback is called when user calls Rollback directly, in this case we do not need to reload the free pages from disk.
func (tx *Tx) nonPhysicalRollback() {
	if tx.db == nil {
		return
	}
	if tx.writable {
        // 会释放掉不需要的页
		tx.db.freelist.Rollback(tx.meta.Txid())
	}
	tx.close()
}