boltdb转mysql_boltdb 学习笔记 (一)|牛气冲天新年征文

本文介绍了BoltDB的存储结构,包括meta、freelist、leaf和branch page,以及页面布局和数据读取。通过示例代码展示了如何从磁盘读取数据到内存中,形成B+树的映射。文章以BoltDB数据库实例的创建和etcd维护的bbolt工具为例,详细阐述了BoltDB的元数据、空闲链表页、分支页和叶子页的工作原理。
摘要由CSDN通过智能技术生成

前言

为什么看 boltdb ?起因是在学习 etcd 的时候,底层的存储以及 MVCC 事务控制就是交给 boltdb。之前也学过 Mysql,但是由于其底层源码复杂度,同时看到 boltdb 是纯 Go 编写,可以把 b+ tree 以及 MVCC 的实现复习一下。

下面就开始我们的探索!

import (

"log"

"time"

bolt "go.etcd.io/bbolt"

)

func main() {

db, err := bolt.Open("my.db", 0600, &bolt.Options{Timeout: 10 * time.Second})

if err != nil {

log.Fatal(err)

}

defer db.Close()

}

复制代码

以上是 etcd 中的 boltdb 最简单的入门代码。

在获取 go get github.com/boltdb/bolt 时候,一直down不下来。

所以改为 etcd 维护的 boltdb 。不过基本 API 功能是不变的。

另外:etcd get 完之后,会同时下载一个 bbolt 的 command line,便于分析 boltdb 的底层结构。

存储结构

在执行完上述代码,会在同级目录下生成一个 my.db 文件。这就是一个 boltdb 的数据库实例:包含了当前这个数据库的所有信息:metadata, index, data

先来看看 etcd 提供的 bbolt 的命令有什么提示:

❯ bbolt -h

Bolt is a tool for inspecting bolt databases.

Usage:

bolt command [arguments]

The commands are:

bench run synthetic benchmark against bolt

buckets print a list of buckets

check verifies integrity of bolt database

compact copies a bolt database, compacting it in the process

dump print a hexadecimal dump of a single page

get print the value of a key in a bucket

info print basic info

keys print a list of keys in a bucket

help print this screen

page print one or more pages in human readable format

pages print list of pages with their types

page-item print the key and value of a page item.

stats iterate over all pages and generate usage stats

Use "bolt [command] -h" for more information about a command.

复制代码

看到其中的 bblot pages ... :

❯ bbolt pages -h

usage: bolt pages PATH

Pages prints a table of pages with their type (meta, leaf, branch, freelist).

Leaf and branch pages will show a key count in the "items" column while the

freelist will show the number of free pages in the "items" column.

The "overflow" column shows the number of blocks that the page spills over

into. Normally there is no overflow but large keys and values can cause

a single page to take up multiple blocks.

复制代码

首先一个数据库实例在存储的时候是以 page 为单位组织的。每个 page 大小由当前文件系统的 PAGE_SIZE 决定,这是数据在磁盘和内存中活动的最小单位:

❯ getconf PAGE_SIZE

4096

#4096 = 4k

复制代码

Page

数据库实例初始化的页面分布:

❯ bbolt pages my.db

ID TYPE ITEMS OVRFLW

======== ========== ====== ======

0 meta 0

1 meta 0

2 freelist 0

3 leaf 0

复制代码

至于上述的 page 是什么?下面会解释每一种 page 的作业和分布。

Page Structure

不管是哪种 page,结构上都是 header + data body 构成的。

type pgid uint64

// page header {id, flags, count, overflow}

type page struct {

id pgid

flags uint16

count uint16

overflow uint32

ptr uintptr

}

复制代码

id:page id

flags:区分不同类型的页面。也就是上述的一些什么 meta/freelist page

count:该页面存储的数据数量。只有当页面为 branch/leaf page 才会存储数据

overflow:单个页面不够存储数据,该值存放溢出页数量

ptr:指向页表头数据的结尾,也就是页面数据起始的位置【页表头不包括 ptr】

d1f2d6b6391cb451d510d9051991ae3b.png

其实在 bbolt pages -h 命令下的对于 overflow 的解释很清楚。我这里翻译一下:

overflow 列显示页面溢出的块的数量。通常不会发生溢出,但是大的键和值可能会导致溢出一个页面要占用多个块。

Meta Page

meta page 存储数据库实例元数据。

type meta struct {

magic uint32

version uint32

pageSize uint32

flags uint32

root bucket

freelist pgid

pgid pgid

txid txid

checksum uint64

}

复制代码

magic:魔数,用来识别 boltdb db file

version:boltdb 版本

pageSize:db page 大小,os.Getpagesize() 可以获取

flags:不知道是干啥的

root:boltdb 中数据组织是以 b+ tree 形式,而整个树的 root node 就保存在这

freelist:数据删除时,这些 page 会被保存 freelist 中的 ids 中

pgid:下一个将要分配的 pgid【可以理解为当前 max page num,即目前 page 数量 】

txid:下一个将要分配 transaction id,和事务实现有关

checksum:校验码,用于校验 metapage 是否写完整

// meta returns a pointer to the metadata section of the page.

func (p *page) meta() *meta {

return (*meta)(unsafe.Pointer(&p.ptr))

}

复制代码

此处可以证明:ptr 是指向 page data ,同时这里基本上都涉及 unsafe 的指针操作

d871c835bce07562eebafcdc515ea0b7.png

freelist page

空闲链表页是 db file 中一组连续的页(1个甚至多个),保存 db 使用过程中的由于修改操作而释放的页的 id 列表。

// freelist represents a list of all pages that are available for allocation.

// It also tracks pages that have been freed but are still in use by open transactions.

type freelist struct {

ids []pgid // all free and available free page ids.

pending map[txid][]pgid // mapping of soon-to-be free page ids by tx.

cache map[pgid]bool // fast lookup of all free and pending page ids.

}

复制代码

ids :保存当前空闲页面的 id array

pending:保存事务操作对应的 page id,key: trxid; value: []pgid,在事务操作完成之后会被释放。即是事务操作中的中间产物

cache:标记一个 pgid 可用,存放着 key: pgid; value: 标记当前都是闲置可分配使用

boltdb 通过 freelist page 来管理页面的分配。

开始说到 freelist 记录了哪些 page 是空闲的,在 删除 和 新数据分配 时都会涉及到:

删除大量数据,对应的 page 会释放 -> pgid 存储到 freelist 的 ids 中;

写入数据时,直接从空闲页 ids 中申请即可。

ef8f357e5a1a9ca0b32c805b417b62a1.png

上述例图中,ids 是事先排序好的。这个设计也是方便 freelist 分配 new page 。

branch page

前面提到 meta page 中的 root 字段,它是整个数据库实例数据组织 b+ tree 的 root node。

那其他的 node 呢?

index node -> branch page

leaf node -> leaf page

// branchPageElement retrieves the branch node by index

func (p *page) branchPageElement(index uint16) *branchPageElement {

return &((*[0x7FFFFFF]branchPageElement)(unsafe.Pointer(&p.ptr)))[index]

}

// branchPageElements retrieves a list of branch nodes.

func (p *page) branchPageElements() []branchPageElement {

if p.count == 0 {

return nil

}

return ((*[0x7FFFFFF]branchPageElement)(unsafe.Pointer(&p.ptr)))[:]

}

// 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]

}

复制代码

摘取了部分有关 branchPage 的代码,从上面可以看出以下几个特征:

branch page 的数据部分:存储由此分支开始的每一个 index node 元数据信息

index data 采取空间换时间,分为:

branchPageElement:存储分支下的每个 branch node 元数据

key:真正的数据【也只是可以找下一个 node 的 key】

branchPageElement 字段含义:

pos:page data 部分的偏移量,可以找到相应的 key

ksize:key 的大小

pgid:下一个 branch page 的 page id

从 key() 函数看出:

是从 element 的 pos 位置开始,读取 e.kszie 偏移量。

可以注意到:先获取当前 ele 的 Pointer address ,因为整个数据段都是连续的数组,所以可以通过 ele.pos ,加上这个偏移量,就直接找到对应的 key

branchPageElement() 中也是从 page 的 []branchPageElement 读取对应index。从而说明 branch page 存储的是一个连续的 branchPageElement。

但是 etcd 版中,key() 被改写成:reflect.SliceHeader

// key returns a byte slice of the node key.

func (n *branchPageElement) key() []byte {

return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{

Data: uintptr(unsafe.Pointer(n)) + uintptr(n.pos),

Len: int(n.ksize),

Cap: int(n.ksize),

}))

}

复制代码

这个就是转换为 []byte 的 zero-copy 做法:

先拿到结构体指针指向的地址,+ 对应偏移量 => 得到 data

key slice len和cap都是其 ele 的 ksize

8c775244801ad776fab21ee0e2e25342.png

而上述代码也是在 page 到 node ,也就是把数据从磁盘读到内存中。当然其中重要的就是 read():

// read initializes the node from a page.

func (n *node) read(p *page) {

n.pgid = p.id

n.isLeaf = ((p.flags & leafPageFlag) != 0)

n.inodes = make(inodes, int(p.count))

for i := 0; i < int(p.count); i++ {

inode := &n.inodes[i]

if n.isLeaf {

elem := p.leafPageElement(uint16(i))

inode.flags = elem.flags

inode.key = elem.key()

inode.value = elem.value()

} else {

elem := p.branchPageElement(uint16(i))

inode.pgid = elem.pgid

inode.key = elem.key()

}

_assert(len(inode.key) > 0, "read: zero-length inode key")

}

...

}

复制代码

read() 就可以很明显看到 branch page 的页面布局,再来整体梳理一下:

从 page 中获取 pgid,标明当前 node 来源哪个 page

[]inodes 长度为 page.count ,也就是当前node有多少个 子node

isLeaf 由 page.flags 决定【也决定后面的 if 分支的赋值】

由 p.count 的数量,循环每一个 element,同时对应 n.inodes 的每一个赋值

这样就把磁盘的 b+ tree 映射到内存中。此部分内容还会在后面继续深入的。

leaf page

根据上面的 read() 也可以看到针对 leaf node 字段的赋值:

type leafPageElement struct {

flags uint32

pos uint32

ksize uint32

vsize uint32

}

复制代码

flags:区分 subbucket和 普通 leaf node【关于 bucket 后续再讲】

pos:与 branch page 的一致,距离对应 leafPageElement 的偏移量

ksize/vsize:key/value 存储大小

5c39b081279b24d2593dcef9d9019728.png

其中 value 的定位在 read() 中体现了:

// read initializes the node from a page.

func (n *node) read(p *page) {

...

for i := 0; i < int(p.count); i++ {

inode := &n.inodes[i]

if n.isLeaf {

elem := p.leafPageElement(uint16(i))

inode.flags = elem.flags

inode.key = elem.key()

inode.value = elem.value() // 正是这行。。。

} else {

...

}

_assert(len(inode.key) > 0, "read: zero-length inode key")

}

...

}

// value returns a byte slice of the node value.

func (n *leafPageElement) value() []byte {

buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))

return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos+n.ksize]))[:n.vsize:n.vsize]

}

复制代码

key:&element + pos == &key

value:&leafPageElement + pos + ksize == &val

总结

本文从整体的页面布局上了解了 boltdb 在磁盘中的结构。下一篇来看看 boltdb 在内存中的操作和设计思路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值