Quantcast
Channel: CodeSection,代码区,数据库(综合) - CodeSec
Viewing all articles
Browse latest Browse all 6262

BoltDB之Bucket(二)

$
0
0

我们在前面的博客中详细描述了BoltDB的Bucket存储格式以及针对Bucket的一些关键动作。但在前讲我们关注的重点是Inline Bucket,也即Bucket内所有的KV记录都紧随Bucket的记录而存放,这样可以提高Bucket的搜寻效率。

但随着数据量的增长,一个Bucket无法永远保存Inline的特性,总会在某个点的时候,Inline Bucket终究还是要成为非Inline Bucket,在本篇博客中,我们就来关注一个Inline Bucket如何变成非Inline Bucket以及非Inline Bucket在BoltDB底层的存储形式。

Inline到非Inline

我们之前说过,一个Bucket在创建伊始一定是一个Inline Bucket,因为此时该Bucket尚未存储任何的KV记录。

接下来就要向该Bucket内插入新的或者更新已有的KV记录,这个过程我们也在前面文章做过详述,最后,在我们所有的更新都完成之后,我们需要做的是将本次Tx内所有的修改进行提交(Tx.Commit()),而提交内部就有一个很重要的任务tx.root.spill(),这里的root就是系统的root Bucket,见下:

func (tx *Tx) Commit() error {
// spill data onto dirty pages.
startTime = time.Now()
if err := tx.root.spill(); err != nil {
tx.rollback()
return err
}
......
} func (b *Bucket) spill() error {
// Spill all child buckets first.
for name, child := range b.buckets {
var value []byte
if child.inlineable() {
child.free()
value = child.write()
} else {
if err := child.spill(); err != nil {
return err
}
value = make([]byte, unsafe.Sizeof(bucket{}))
var bucket = (*bucket)(unsafe.Pointer(&value[0]))
*bucket = *child.bucket
}
if child.rootNode == nil {
continue
}
// Update parent node.
var c = b.Cursor()
k, _, flags := c.seek([]byte(name))
c.node().put([]byte(name), []byte(name), value, 0, bucketLeafFlag)
}
if b.rootNode == nil {
return nil
}
// Spill nodes.
if err := b.rootNode.spill(); err != nil {
return err
}
b.rootNode = b.rootNode.root()
......
b.root = b.rootNode.pgid
return nil
}

对root Bucket进行spill操作的第一步是对Bucket的孩子Bucket进行spill,这是一个递归过程。

对孩子Bucket进行spill之前判断,如果该child Bucket是可Inline存储的,那么比较简单。如果child Bucket是不可Inline存储的,那么对该child Bucket调用spill()。

那么如何判断一个Bucket是否可以进行Inline存储呢,以下条件满足任一即视该Bucket不可进行Inline存储:

Bucket下的KV记录有Bucket类型的:这是因为如果一个Bucket下兼有sub Bucket和普通的KV记录的话,会怎么样呢??? Bucket下的普通KV记录数占据空间已超过page大小的1/4

那么是怎么对每child Bucket进行spill呢?我们接着往下看:b.rootNode.spill(),知道,实际是工作是对最底层的child Bucket的rootNode进行spill()。这个rootNode是什么,又是怎么进行spill的呢?我们在下面娓娓道来。

非Inline Bucket存储

承接上面的话题,我们想要弄清楚非Inline Bucket底层的存储格式,就必须得搞清楚其rootNode的spill过程。这个过程将一个非Inline Bucket转变成一个Inline Bucket。

为了说清楚这个问题,我们假定一个场景,root Bucket下创建了一个Bucket post,post下插入了诸多KV记录,我们仔细研究post如何裂变。在插入记录后裂变之前,该Bucket在内存中的KV记录存储形式大概是这样的:


BoltDB之Bucket(二)

由于该Bucket当前还处于Inline状态,因此其所有插入的KV记录依然存放于rootNode的inodes数组中,每个inode代表了一个插入的KV记录。

node spill

继续我们上面的讨论,Bucket.spill()最终是落实到了其Bucket.rootNode.spill(),而node的结构我们在上面也已描述清楚,我们接下来就看node如何进行spill。

func (n *node) spill() error {
var tx = n.bucket.tx
if n.spilled {
return nil
}
......
var nodes = n.split(tx.db.pageSize)
for _, node := range nodes {
if node.pgid > 0 {
tx.db.freelist.free(tx.meta.txid, tx.page(node.pgid))
node.pgid = 0
}
p, err := tx.allocate((node.size() / tx.db.pageSize) + 1)
if err != nil {
return err
}
// Write the node.
node.pgid = p.id
node.write(p)
node.spilled = true
// Insert into parent inodes.
if node.parent != nil {
var key = node.key
if key == nil {
key = node.inodes[0].key
}
node.parent.put(key, node.inodes[0].key, nil, node.pgid, 0)
node.key = node.inodes[0].key
}
// Update the statistics.
tx.stats.Spill++
}
if n.parent != nil && n.parent.pgid == 0 {
n.children = nil
return n.parent.spill()
}
return nil
}

spill()的目的是将将Bucket的所有KV记录写到合适的page上。这里主要只有两个关键步骤:

对node.inodes进行split,一个node分成多个node,因为单个page可能存放不了这么多的数据; 对步骤1中split出来的每个node分配page来写入。

split的过程比较自然,就是将1个分成多个,至于拆分的规则请自行阅读代码吧,有1点需要注意的是:由于1个拆成了多个,那必须要为这拆分后的多个node创建parent node,这其实便是索引node,假如我们例子中的rootNode可以被拆分成三个node,则形成的结构如下图所示:


BoltDB之Bucket(二)

弄清楚了node的分裂,接下来就是将分裂出来的node分别写入新分配的page。写入page过程无需多言,即将node里面的每个inode转化成item(item header + key + value),写入page。比较简单。

言既至此,我们其实已经完成了Inline Bucket到非Inline Bucket的九成工作量,但是还有两件事情没有做:

如何由Bucket索引到裂变后的记录page? 该Bucket记录怎么存?

先来回答第一个问题:

Inline Bucket的索引非常简单,KV记录与Bucket记录连续存放。

但是非Inline Bucket就不是这样了,且在我们上面的示例中还经历了node split过程,生成了一个parent node,原来Bucket的rootNode只是parent node一个孩子(可以参考上面的split图),所以接下来需要将这个索引改掉,改成parent node。

这在Bucket.spill()的最后来做,请看

func (b *Bucket) spill() error {
......
// 将rootNode设置为裂变后parentNode的root
b.rootNode = b.rootNode.root()
// 设置rootNode对应的page id
b.root = b.rootNode.pgid
return nil
}

再来看第二个问题:Inline Bucket的rootNode中记录有所有插入的KV,而裂变后的非Inline Bucket的这些记录已经被写入了新的page,因此该Bucket的KV记录也变得比较简单,下面这段代码就设置了Bucket裂变后的其自身的KV变化:

func (b *Bucket) spill() error {
for name, child := range b.buckets {
......
if child.inlineable() {
child.free()
value = child.write()
} else {
if err := child.spill(); err != nil {
return err
}
// child Bucket已经完成裂变
// child Bucket就无需在其KV记录中存储那么多的内容,只需要一个bucket header即可,且其root为裂变后的搜索路径上的第一个page id
value = make([]byte, unsafe.Sizeof(bucket{}))
var bucket = (*bucket)(unsafe.Pointer(&value[0]))
*bucket = *child.bucket
}
......
}

我们回忆下Inline Bucket的KV记录格式:


BoltDB之Bucket(二)

再来对比下分裂后的普通Bucket的KV记录格式:


BoltDB之Bucket(二)
非Inline Bucket存储

经过上面的Inline Bucket分裂过程的详细描述,我们已经可以很容易地总结出非Inline Bucket的存储形式,请看下图:


BoltDB之Bucket(二)

Viewing all articles
Browse latest Browse all 6262

Trending Articles