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中的数据啥时候回刷新到磁盘?
- 用户进程调用sync()或者fsync()
- 系统调用空闲内存低于特定阈值
- 脏页的数据在内存中驻留的时间超过一个特定阈值
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页类型 | 类型定义 | 类型值 | 用途 |
---|---|---|---|
分支节点页 | branchPageFlag | 0x01 | 存储索引信息(页号、元素key值) |
叶子节点页 | leafPageFlag | 0x02 | 存储数据信息(页号、插入的key值、插入的value值) |
元数据页 | metaPageFlag | 0x04 | 存储数据库的元信息,例如空闲列表页id、放置桶的根页等 |
空闲列表页 | freelistPageFlag | 0x10 | 存储哪些页是空闲页,可以用来后续分配空间时,优先考虑分配 |
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 page与leaf page是boltdb中用来保存B+树节点的页
B+树的分支节点仅用来保存索引(key),而叶子节点既保存索引,又保存值(value)
boltdb它支持任意长度的key和value,因此无法直接结构化保存key和value的列表。
为了解决这一问题,branch page和leaf page的Page Body起始处是一个由定长的索引(branchPageElement
或leafPageElement
)组成的列表,第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()
}