数据库的介绍
- OLTP 在线事务处理
- OLAP 在线分析处理
- HTAP OLTP+OLAP的结合
根据发展时代分类
- mysql: 以行为单位进行存储数据。
- no sql: 不再以行为单位去存储,文档数据库(mo go db) kv数据库 (re di s)
图数据库、时序数据库、列式存储 - newsql: TiDB 、OceanBase
分布式数据库+单机数据库等。
数据结构
基础数据结构
在计算机中。数据结构就分为俩大类
数据和链表
在物理结构上是连续的或分散的。
对于数组,查询/修改是很快的,增加删除是慢的。
对于链表,正好相反。
哈希类数据结构
-
哈希表: 是存储 key-value的结构。哈希函数的输入就是key,然后计算出 下标。
。时间复杂度是O1. 要处理哈希冲突。
1、开放地址法,数组后面直接挂的链表。 当时冲突多了,查找就慢了,可以使用搜索二叉树,红黑树。
2、当前位置如果有值了,依次往后移位置,直到数组结尾,说明慢了,然后扩容。 -
位图: 通过bit 位,节省空间,置0置1操作。和1 & | 。还可以增加比特位,来表示更多的信息。
-
布隆过滤器: 来判断一个key 是否存在与系统中,提前拦截,当时有可能误杀, 通过多个哈希函数来标记。描黑。
二叉树类结构
-
搜索二叉树,左子树 < 根节点<右子树。这样就二分了,时间复杂度就是logN了。当时有可能出现最坏的情况,成为了单链表。
-
平衡二叉树,防止左右子树的高度不平衡,在插入数据的时候要左旋右旋来保证 平衡,旋转比较耗时。这样的强平衡 开销大于收益。
-
红黑树,多叉树,2-3树进化而来,每个节可能包含2个值,三个子节点。从平衡二叉树这样严格平衡向弱平衡。
插入数据为了保证这样的结构,分裂合并比较麻烦。
红黑树是通过标记来决定2-节点和3-节点。满足的条件:红色的节点都是左子树节点,没有连续相连的俩个红节点,子节点到根节点的路径上黑色节点的数量一致。 2-3-4树是广泛意义的红黑树。条件更宽松。
应用场景,高效的查找、插入、删除性能。E poll, linux内存管理。
- 跳表 红黑树比较麻烦,边界条件较多,跳表可以保证和红黑树同样的读写性能,工程上的实现更简单。
在单链表的基础上,加上了层高索引。而且可以范围查找。这是红黑树没有的功能。
-
B树 每个节点可以存储M个值,如果不是叶子节点,至少有2个子树,每个节点存储了n个key,就有n+1个子树。每个叶子节点都有n个key,
特点:单点查询性能高,可以更好的在内存/磁盘上维护数据结构。想要范围查询,只能通过中序遍历,在内存中很好处理,当时在硬盘中就不好了。会增加多次IO. -
B+树 为了更好的支持在硬盘中的范围查询,在B树的基础上,有如下区别限制
1、指向子树的最大值或者最小值。
2、每个叶子节点之间也有序连接。
3、B树当前节点有n个关键字,一定有n+1个子树。B+树没有这个限制。
4、B树中叶子节点和非叶子节点都存储数据,没有重复的,而B+树,非叶子节点只存储key,,索引,叶子节点才存储全部信息。
B树因为每个节点存储的key多。可以很明显的降低树的高度。查询更快。
数据存储介质
cpu 高速缓存、内存、机械硬盘、固态硬盘。
- 虚拟内存管理:时间局部性和空间局部性。页面置换、页大小
- 机械硬盘 数据定位; 1、寻道、扇片旋转、传输。
寻道是机械运动,所以比较慢,顺序写比随机写快,就是因为减少了寻道时间。
磁盘管理机制
- 文件的 逻辑结构与磁盘的存储设备无关,文件的物理结构有很大的相关性。
文件需要有存储与检索功能。
- 文件根据逻辑结构分类,是根据文件之间的逻辑结构。
0、无结构的文件,以字节为单位。只能顺序检索,一般是二进制文件。
1、有结构的文件。记录的长度可以是定长 也可以是变长的。
有结构的文件继续分类 - 顺序文件,根据某种顺序来保存,可以是时间顺序,或者关键字顺序。对于定长的记录,可以快速定位,对于变长的记录,需要顺序遍历。
- 索引文件,解决顺序文件变长记录访问的问题。 给变长记录的记录建立索引。索引表项有记录的编号、位置、长度。
- 索引顺序文件,对记录进行分组,然后索引文件来记录 每个组的起始位置,然后开始顺序遍历查找。
- 直接文件/哈希文件,根据key 去计算存储的地址。无序且有哈希冲突。
文件的管理---目录
文件系统中一般存在大量的文件,由目录来管理这些文件。目录最基本的功能就是根据文件名来检索到文件。
- FCB 文件控制快 ,文件目录就是FCB的组合。
FCB 的主要信息
1、 基本信息:文件名、文件物理位置、文件逻辑结构、文件物理结构
-
文件名:文件的唯一标识符。
-
文件物理位置: 存放文件的设备名、起始盘块号、占用的盘块数、文件长度等。
-
文件逻辑结构:流文件还是记录文件,定长还是变长。
-
文件的物理结构:是顺序、索引还是链接。
2 、控制信息:权限
3、使用信息:创建时间、最近更新时间等。 -
索引节点
目录存放在磁盘上,文件很多时,目录会非常大, 当时文件只需要按需加载到内存,所以可以把文件名和文件描述信息分开,文件描述信息使用单独的一个结构来存储就是索引节点。简称i节点。
文件目录项 由 文件名和索引节点的指针构成。
文件打开的时候,磁盘索引节点会被加载到内存索引节点,多了引用计数、状态等信息。
目录检索方式:用户输入 文件的路径,os 根据文件名找到目录项,然后找到索引节点,然后就可以加载文件到内存。
1、 线性检索:线性表维护目录项,
2、哈希检索:建立1个哈希检索目录,
文件的物理结构
文件的物理结构主要涉及 给文件分配了哪些扇区,还有哪些空闲的扇区。
- 连续分配:给文件分配物理空间的时候。是一段连续的扇区(磁盘块)。访问文件不需要更换磁道,访问快,删除后会有碎片。动态变化的文件不友好
- 链接分配:磁盘块有下一个磁盘块的信息。隐士链接分配:目录项维护文件的第一个磁盘块和最后一个磁盘块号,显示链接分配:有专门的链接表。 缺点:需要维护链接表,大文件有很多块。
- 索引分配:为每个文件加一个索引块,索引块包含了该文件使用的磁盘块号,索引块可以分级。目录项只需要保存该索引块。
加速磁盘访问--读
思路:减少磁盘访问IO的次数来访问到数据。
1、Mmap: 磁盘空间的数据和内存有映射,
2、磁盘预读:操作系统读取磁盘是按照扇区的大小来读的。
3、缓存:在缓存中找不到再到磁盘。
加速磁盘写
1、顺序批量写:减少磁头的随机移动。WAL日志就是顺序批量写。
2、延迟异步写:数据先写入到内存中,结合WAL 日子,当内存数据累积到一定大小再罗落盘。
读多写少 的 情况 下 为啥选择B+树
宏观角度
存储引擎需要支持排序和范围查询。并且 在内存中与磁盘中需要保持一样的数据结构可以减少从硬盘到内存的转换,内存排好序之后,然后根据一定的阈值刷新到磁盘。 非叶子节点的索引项 不保存数据可以尽可能的减少树的层高,减少IO.
微观角度
- B+树在内存和磁盘中的映射
叶子节点存储索引项+数据。内存中的页大小和磁盘中的块大小保持一致,一一对应。如果内存中的某一个节点不存在,根据索引项从磁盘中加载。 - 读操作
从根节点开始,二分法查找 key,确定下一个节点,直到叶子节点,然后在二分查找,直到key相等。
范围查询也一样。叶子节点之间可以双向指针连接,升序降序都可以范围查找。 - 写操作
写操作包括增、改、删
1、增加数据,节点空间足够,不需要页分裂。
定位到待插入的节点位置,如果不是当前节点的最后一个。需要把后面的数据后移,然后插入当前节点的数据和索引。 当前节点就和磁盘的数据不一致了,就会标记为脏页。
2、增加数据,节点空间不够。需要页分裂
会在当前叶子节点取一个新的页索引项,到父节点,如果父节点也要分页依次向上处理。直到根节点,如果根节点也需要分页,就是增加层高的特殊场景,一般比较少。
3、删除数据,页不合并。
定位到当前节点,将索引项和数据项删除就好,如果是逻辑上的删除,那就只是更新操作,如果待删除的是边界值,需要更新父节点的索引项。
4、删除数据,页合并
删除之后,当前页数据太少或者为空不满足要求是,需要将兄弟节点合并,并移除兄弟节点的父索引项。如果合并之后由超出页大小,按照页分裂的逻辑继续处理。
- 异常情况处理
数据写入的流程
1、数据读取到内存-> 2、在内存中修改数据->3、数据写入到操作系统->4、操作系统刷盘到硬盘->5、刷盘后返回给应用程序 成功的标志
在步骤1、2、发生异常,硬盘的数据没被修改。所以不需要特殊的额外处理操作。
在步骤3 、4 数据写入到操作系统和在刷盘的时候异常,数据可能没更新,部分更新,全部更新。这时候就需要额外操作恢复。
在步骤5 ,数据已经落盘,硬盘的数据已经保持一致性了。只是没给客户返回成功的标志。如果有其他手段通知到客户已经写入成功,可以不做回滚处理。否则需要回滚。
- 数据部分写入的异常处理
根据数据的用途分类
1、存储引擎的运行时数据,比如空闲页等。
2、节点的实际数据
3、元数据,全局事物号等。
写入数据的顺序也是1 2 3 。每个数据写完都会刷盘。
处理异常的思路就是 备份。然后用备份的数据恢复。
写入运行时数据异常,那么就使用上一次写入成功的恢复。
写入节点的数据异常,运行时数据已经写入,可以根据上面的恢复,节点的数据需要在内存中提前记录好 修改前的数据,增加删除了啥,然后恢复,比如 redolog等。从磁盘中日志恢复数据。
写入元数据异常,前面俩个可以根据上面的恢复。存储引擎的系统的元信息做了物理备份。保证数据恢复到写操作之前的状态。
事务 Transaction
为了保证数据的一致性,需要通过隔离、原子、持久来保证。
事务中的隔离级别
- 读未提交,事务A读到了事务B未提交的数据
- 读已提交,事务A 读到了事务B 已提交的数据
- 可重复读,事务A每次读取的数据一致。
- 串行化,解决了幻读问题,幻读是范围类的查询。数据类的条目个数不一致。
解决这些的方式就是锁,上锁的范围和强度。
并发控制
根据悲观者和乐观者的分类
悲观者
-
基于锁的并发控制
读锁共享锁,写锁排它锁。
在锁的上升阶段只获取锁,不释放锁。
在锁的缩减阶段,只释放锁,不获取锁,来避免死锁。 -
基于时间戳的并发控制
每行记录都有读写的最新时间,每个事务也有时间,通过比较时间戳来判断是否需要回滚。
乐观者
- 基于有效性检测的并发控制
存储引擎有读集合RS 和 写集合 WS
一个事务开始的时候把自己的RS 和WS 告知存储引擎。
- 读阶段:事务读取RS 的元素,如果是写事务,在局部空间计算要写入的值,不执行真正的写入,只是计算
- 有效性检查阶段: 比较改事务和其他的事务的读写集合来确认事务的有效性。检查失败则回滚。
- 写阶段: 写入WS 中的值,
有效性检查需要三个 时间戳。
start(T): 事务开始的时间。
validation(T): 开始有效性检查的时间。
finish(T): 事务完成写阶段的时间。
缺点:如果真的是写多并发高的情况下。性能会很低。
多版本并发控制
前面的并发控制,发生冲突只能回滚或者延迟读写。是因为没有在同一个时间点对每一个记录保存值。
每个事务对数据进行操作的时候,会创建一个新的版本来。
- 基于时间戳的多版本并发控制
每条数据都维护一个版本序列<x1,x2,x3,x4...xn>.每个版本都有value,wt, rt三个属性。
优点:事务的读操作不会失败。 - 基于锁的多版本并发控制
事务分为读事务和读写事务俩类。
读写事务按照俩个阶段锁来执行。只有当事务被提交才会释放锁。事务可以按照他们提交的顺序来保持串行化。
每条数据还是会维护一个版本序列。可以根据时间戳或者计数实现。计数器会在事务提交就增加。
对于只读事务,事务开始时,存储引擎会将当前计数器的值设置为该锁的TS。只读事务获取数据时,系统返回小于当前事务TS的所有版本中时间戳最大的版本对应的数据。
当要写某条数据,会获取该条数据上的排它锁,申请成功之后为该数据创建一个新的版本,将新的数据写入这个版本,最新版本的时间戳是无穷大,时间戳会在提交时更新。
只读事务不阻塞。前面创建的只读事务读取的是老数据。后续的读事务读取的最新的数据。
何时清除老版本的数据,小于最小时间戳的只读事务就可以清除了。
- 快照隔离
上述俩个只能保值读已提交。不能保证可重复读。
每个事务开始前复制一份当前时刻系统的快照。
事务冲突的时候,可以先提交者获胜,第一个事务提交成功,第二个事务提交失败。
先更新者获胜,需要采用锁机制,获取锁成功,如果其他事务已经更新了,,那么就回滚,如果没有更新过,就更新并提交。
获取锁失败:那么就只能等待释放。获取到之后,如果前面的事务更新了数据,那么只能回滚。
BoltDB源码分析
源码:
分为三层,用户称、内存层、硬盘层。
硬盘块->page 页 ->node 节点
所有的信息都是以页为基础进行存储的。是一个文件。
页可以分为元数据页、空闲页、分支页、叶子页。
前四个页是固定的。meta0、meta1、freelist、Bucket的根节点。
page 结构
type page struct {
id pgid //page id
flags uint16 // page 类型
count uint16 // 个数,每个页存储的 空闲页个数或者分支节点个数或者叶子节点个数
overflow uint32 // 溢出页数
ptr uintptr // 实际的数据。不定长。
}
- 元数据页
page对象有个meta()方法 获取具体的元信息
meta 结构
type meta struct {
magic uint32 // 魔数
version uint32 // 版本
pageSize uint32 // 页大小
flags uint32 // 保留值
root bucket // bucket 根
freelist pgid // 空闲列表页ID
pgid pgid // 元数据页ID 书籍描述错误,其实是页的最大ID,新增页时递增。
txid txid // 最大事务ID
checksum uint64
}
meta.copy()将元信息复制给page页的ptr.
meta.wrtie()
// 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 {
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.
p.id = pgid(m.txid % 2) // BoltDB 使用 双缓冲元数据(meta0 和 meta1),通过 m.txid % 2 决定当前元数据写入哪个页:
p.flags |= metaPageFlag // 设置页类型为元数据页
// Calculate the checksum.
m.checksum = m.sum64() // 计算 meta 结构体的 CRC64 校验和(不包括 checksum 字段自身,避免循环依赖)
m.copy(p.meta()) // 通过 unsafe.Pointer 将页的 ptr 字段转换为 *meta 类型, 将内存中的 meta 结构体逐字段拷贝到目标页的 ptr 区域。
}
- 空闲列表页
运行过程中,会产生空闲的页,如何管理。
// 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. 当前可立即分配的页 ID 列表(未被任何事务占用)。
pending map[txid][]pgid // mapping of soon-to-be free page ids by tx. 按事务 ID 分组的待释放页(事务提交后才会转移到 ids)。
cache map[pgid]bool // fast lookup of all free and pending page ids. 所有空闲页的快速查询表(包括 ids 和 pending 中的页),用于去重和快速判断某页是否空闲。
}
// size returns the size of the page after serialization.
func (f *freelist) size() int {
n := f.count() // // 获取实际需要存储的空闲页数量
if n >= 0xFFFF { // 处理数量超过 16 位最大值的情况
// The first element will be used to store the count. See freelist.write.
n++ // // 预留一个位置存储溢出计数
}
return pageHeaderSize + (int(unsafe.Sizeof(pgid(0))) * n) // 页头固定大小 + 每个 pgid 占用的空间
}
// count returns count of pages on the freelist
func (f *freelist) count() int {
return f.free_count() + f.pending_count()
}
将freelist 写入page
// write writes the page ids onto a freelist page. All free and pending ids are
// saved to disk since in the event of a program crash, all pending ids will
// become free.
func (f *freelist) write(p *page) error {
// Combine the old free pgids and pgids waiting on an open transaction.
// Update the header flag.
p.flags |= 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.
lenids := f.count()
if lenids == 0 {
p.count = uint16(lenids) //无空闲页,只有页头
} else if lenids < 0xFFFF {
p.count = uint16(lenids) //空闲页的个数小于65535
f.copyall(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[:]) // ptr 指向 空闲页列表ids+pending列表。 排序 + 去重 + 压缩。 1-6
} else {
p.count = 0xFFFF //表示溢出, 超多65535. ptr 的第一个元素来保存溢出的个数
((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0] = pgid(lenids)
f.copyall(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[1:])
}
return nil
}
// type pgids []pgid
func (f *freelist) copyall(dst []pgid) {
m := make(pgids, 0, f.pending_count())
for _, list := range f.pending {
m = append(m, list...)
}
sort.Sort(m)
mergepgids(dst, f.ids, m)
}
从page 读取 freelist
// read initializes the freelist from a freelist page.
func (f *freelist) read(p *page) {
// If the page.count is at the max uint16 value (64k) then it's considered
// an overflow and the size of the freelist is stored as the first element.
idx, count := 0, int(p.count)
if count == 0xFFFF {
idx = 1 // 有溢出,跳过第一个8字节。
count = int(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0])
}
// Copy the list of page ids from the freelist.
if count == 0 {
f.ids = nil
} else {
ids := ((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[idx:count]
f.ids = make([]pgid, len(ids))
copy(f.ids, ids)
// Make sure they're sorted.
sort.Sort(pgids(f.ids))
}
// Rebuild the page cache.
f.reindex()
}
// reindex rebuilds the free cache based on available and pending free lists.
func (f *freelist) reindex() {
f.cache = make(map[pgid]bool, len(f.ids))
for _, id := range f.ids {
f.cache[id] = true
}
for _, pendingIDs := range f.pending {
for _, pendingID := range pendingIDs {
f.cache[pendingID] = true
}
}
}
freelist 分配空闲页
// allocate returns the starting page id of a contiguous list of pages of a given size.
// If a contiguous block cannot be found then 0 is returned.
func (f *freelist) allocate(n int) pgid {
if len(f.ids) == 0 {
return 0
}
var initial, previd pgid
for i, id := range f.ids {
if id <= 1 {
//0 1 是元数据页。
panic(fmt.Sprintf("invalid page allocation: %d", id))
}
// Reset initial page if this is not contiguous.
if previd == 0 || id-previd != 1 {
// 如果不连续了,就重置起始位置的ID。
initial = id
}
// If we found a contiguous block then remove it and return it.
if (id-initial)+1 == pgid(n) { //找到了连续的N个空闲页
// If we're allocating off the beginning then take the fast path
// and just adjust the existing slice. This will use extra memory
// temporarily but the append() in free() will realloc the slice
// as is necessary.
if (i + 1) == n {
// 头部分配
f.ids = f.ids[i+1:]
} else {
// 中部/尾部分配:
使用 copy 将后续元素向前移动,覆盖待分配的页 ID,然后缩容切片。
copy(f.ids[i-n+1:], f.ids[i+1:])
f.ids = f.ids[:len(f.ids)-n]
}
// Remove from the free cache.
for i := pgid(0); i < pgid(n); i++ {
delete(f.cache, initial+i) // 从缓存表删除
}
return initial // 返回起始的空闲页号
}
previd = id
}
return 0
}
- 分支节点页
branch主要用来构建索引,来提高检索效率。
一个分支页会存储多个分支页元素
// branchPageElement represents a node on a branch page.
type branchPageElement struct {
pos uint32 // 存储的位置距离当前的元信息的偏移量, 键的起始位置(相对于页内数据的偏移量,单位:字节)
ksize uint32 // 该元素指向的 页 最小的key的值。 键的大小(单位:字节),用于确定键的结束位置。
pgid pgid // 页号,子页的 ID,指向下一层的页(可以是分支页或叶子页)。
}
// key returns a byte slice of the node key.
func (n *branchPageElement) key() []byte {
// // 通过 pos 和 ksize 从页的数据区提取键
buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos]))[:n.ksize]
}
// branchPageElement retrieves the branch node by index
func (p *page) branchPageElement(index uint16) *branchPageElement {
//根据索引 index 获取分支页中第 index 个元素(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
}
// 返回分支页中所有元素的切片([]branchPageElement)
return ((*[0x7FFFFFF]branchPageElement)(unsafe.Pointer(&p.ptr)))[:]
}
页面布局
| Page Header (16B) | branchPageElement1 (12B) | branchPageElement2 (12B) | ... | Key1 Data | Key2 Data | ... |
内存中 分支节点页和叶子节点页统一使用node 。
- 将内存分支节点写入磁盘页(分支节点页)
// write writes the items onto one or more pages.
func (n *node) write(p *page) {
// Initialize page.
if n.isLeaf {
p.flags |= leafPageFlag
} else {
p.flags |= branchPageFlag
}
// 叶子节点在这里不会分裂。
if len(n.inodes) >= 0xFFFF {
panic(fmt.Sprintf("inode overflow: %d (pgid=%d)", len(n.inodes), p.id))
}
p.count = uint16(len(n.inodes))
// Stop here if there are no items to write.
if p.count == 0 {
return
}
// Loop over each item and write it to the page.
b := (*[maxAllocSize]byte)(unsafe.Pointer(&p.ptr))[n.pageElementSize()*len(n.inodes):]
for i, item := range n.inodes {
_assert(len(item.key) > 0, "write: zero-length inode key")
// Write the page element.
if n.isLeaf {
elem := p.leafPageElement(uint16(i))
elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem)))
elem.flags = item.flags
elem.ksize = uint32(len(item.key))
elem.vsize = uint32(len(item.value))
} else {
elem := p.branchPageElement(uint16(i))
elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem)))
elem.ksize = uint32(len(item.key))
elem.pgid = item.pgid
_assert(elem.pgid != p.id, "write: circular dependency occurred")
}
// If the length of key+value is larger than the max allocation size
// then we need to reallocate the byte array pointer.
//
// See: https://github.com/boltdb/bolt/pull/335
klen, vlen := len(item.key), len(item.value)
if len(b) < klen+vlen {
b = (*[maxAllocSize]byte)(unsafe.Pointer(&b[0]))[:]
}
// Write data for the element to the end of the page.
copy(b[0:], item.key)
b = b[klen:]
copy(b[0:], item.value)
b = b[vlen:]
}
// DEBUG ONLY: n.dump()
}
- 从磁盘页读取信息到node
// 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")
}
// 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
_assert(len(n.key) > 0, "read: zero-length node key")
} else {
n.key = nil
}
}
- 叶子节点页
读写和分支节点页一致。就是数据结构不同。
// leafPageElement represents a node on a leaf page.
type leafPageElement struct {
flags uint32 // 标志位(如是否为子桶、是否已删除等)
pos uint32 // key 的起始位置,相对于当前 leafPageElement 结构体的内存地址偏移。
ksize uint32 // key的长度
vsize uint32 // value 的长度
}
// key returns a byte slice of the node key.
func (n *leafPageElement) key() []byte {
//将 leafPageElement 指针转换为字节数组 buf(覆盖整个结构体及后续数据区)。
buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos]))[:n.ksize:n.ksize]·
}
// value returns a byte slice of the node value.
func (n *leafPageElement) value() []byte {
//键的结束位置是 pos + ksize,值的起始位置为 pos + ksize。
buf := (*[maxAllocSize]byte)(unsafe.Pointer(n))
return (*[maxAllocSize]byte)(unsafe.Pointer(&buf[n.pos+n.ksize]))[:n.vsize:n.vsize]
}
node 解析
// node represents an in-memory, deserialized page.
type node struct {
bucket *Bucket // 关联一个桶
isLeaf bool
unbalanced bool // 为true,需要考虑页合并
spilled bool // 值为true,需要考虑页分裂,书籍描述错误,此处为true代表已经操作了 分裂,spill方法 有判断这个值。
key []byte // 保留最小的key
pgid pgid // 页ID
parent *node // 父节点
children nodes // 子节点
inodes inodes // 索引数据。
}
type inode struct {
flags uint32 // 表示是子桶叶子节点还是普通叶子节点
pgid pgid //inode 为分支元素时,pgid才有值。
key []byte // 分支节点指向下一页的最小key.
value []byte // 只有叶子节点才有值。
}
内存中的node 一一对应 磁盘中的page 。
元信息 和真实 数据分离存储。有助于二分定位。前面的元信息长度固定。
node 增删改查
// put inserts a key/value.
func (n *node) put(oldKey, newKey, value []byte, pgid 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. 二分定位插入的位置
//找到第一个 key >= oldKey 的位置,确保 inodes 始终有序。
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.
// 判断key是否存在。
exact := (len(n.inodes) > 0 && index < len(n.inodes) && bytes.Equal(n.inodes[index].key, oldKey))
if !exact {
// 不存在。
n.inodes = append(n.inodes, inode{}) //扩容一个inode
copy(n.inodes[index+1:], n.inodes[index:]) //后移,腾出index
}
// 赋值
inode := &n.inodes[index]
inode.flags = flags
inode.key = newKey
inode.value = value
inode.pgid = pgid
_assert(len(inode.key) > 0, "put: zero-length inode key")
}
| 设计决策 | 说明 |
|---|---|
| 二分查找优化 | 利用有序性快速定位,时间复杂度 O(log n)。 |
| 原地插入/覆盖 | 通过切片操作避免频繁内存分配,提升性能。 |
| 隐式有序维护 | 不显式检查 newKey 的位置,依赖调用者保证,减少冗余计算。 |
| 防御性校验 | 通过 panic 防止非法状态(如空键、非法 pgid),确保内部一致性。 |
// del removes a key from the node.
func (n *node) del(key []byte) {
// Find index of key.
index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].key, key) != -1 })
// Exit if the key isn't found.
if index >= len(n.inodes) || !bytes.Equal(n.inodes[index].key, key) {
return
}
// Delete inode from the node.
n.inodes = append(n.inodes[:index], n.inodes[index+1:]...)
// Mark the node as needing rebalancing.
n.unbalanced = true // 删除元素需要考虑 node(页)合并
}
node (页)分裂
分裂可能由于 inode 个数,转化成page 的页大小。
. 功能概述
核心作用:将内存中的 B+ 树节点(node)分裂为适合磁盘页大小的块,并持久化到脏页中。
递归处理:先处理子节点,再处理自身,最后处理父节点,确保整棵树自底向上完成分裂。
空间管理:释放旧页(若存在),分配新页,更新父节点导航信息。
// spill writes the nodes to dirty pages and splits nodes as it goes.
// Returns an error if dirty pages cannot be allocated.
func (n *node) spill() error {
var tx = n.bucket.tx
if n.spilled {
return nil
}
// Spill child nodes first. Child nodes can materialize sibling nodes in
// the case of split-merge so we cannot use a range loop. We have to check
// the children size on every loop iteration.
sort.Sort(n.children) // 子node 排序
// 子节点优先:先递归处理子节点,确保子节点完成分裂后再处理当前节点。
for i := 0; i < len(n.children); i++ {
// 遍历 每个 子node,进行分裂
if err := n.children[i].spill(); err != nil {
return err
}
}
// We no longer need the child list because it's only used for spill tracking.
n.children = nil
// Split nodes into appropriate sizes. The first node will always be n.
var nodes = n.split(tx.db.pageSize) //根据磁盘页大小分裂
for _, node := range nodes {
// Add node's page to the freelist if it's not new.// 释放旧页(若已分配过 pgid)
if node.pgid > 0 {
tx.db.freelist.free(tx.meta.txid, tx.page(node.pgid)) //加入到 freelist的pending集合中
node.pgid = 0
}
// Allocate contiguous space for the node. 分配新页
p, err := tx.allocate((node.size() / tx.db.pageSize) + 1)
if err != nil {
return err
}
// Write the node.
if p.id >= tx.meta.pgid {
panic(fmt.Sprintf("pgid (%d) above high water mark (%d)", p.id, tx.meta.pgid))
}
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
_assert(len(node.key) > 0, "spill: zero-length node key")
}
// Update the statistics.
tx.stats.Spill++
}
// If the root node split and created a new root then we need to spill that
// as well. We'll clear out the children to make sure it doesn't try to respill.
// 递归处理父节点
if n.parent != nil && n.parent.pgid == 0 {
n.children = nil
return n.parent.spill()
}
return nil
}
// split breaks up a node into multiple smaller nodes, if appropriate.
// This should only be called from the spill() function.
func (n *node) split(pageSize int) []*node {
var nodes []*node
node := n
for {
// Split node into two.
a, b := node.splitTwo(pageSize)
nodes = append(nodes, a)
// If we can't split then exit the loop.
if b == nil {
break
}
// Set node to b so it gets split on the next iteration.
node = b
}
return nodes
}
// splitTwo breaks up a node into two smaller nodes, if appropriate.
// This should only be called from the split() function.
func (n *node) splitTwo(pageSize int) (*node, *node) {
// Ignore the split if the page doesn't have at least enough nodes for
// two pages or if the nodes can fit in a single page.
if len(n.inodes) <= (minKeysPerPage*2) || n.sizeLessThan(pageSize) {
return n, nil
}
// Determine the threshold before starting a new node.
var fillPercent = n.bucket.FillPercent
if fillPercent < minFillPercent {
fillPercent = minFillPercent
} else if fillPercent > maxFillPercent {
fillPercent = maxFillPercent
}
threshold := int(float64(pageSize) * fillPercent)
// Determine split position and sizes of the two pages.
splitIndex, _ := n.splitIndex(threshold)
// Split node into two separate nodes.
// If there's no parent then we'll need to create one.
if n.parent == nil {
n.parent = &node{bucket: n.bucket, children: []*node{n}}
}
// Create a new node and add it to the parent.
next := &node{bucket: n.bucket, isLeaf: n.isLeaf, parent: n.parent}
n.parent.children = append(n.parent.children, next)
// Split inodes across two nodes.
next.inodes = n.inodes[splitIndex:]
n.inodes = n.inodes[:splitIndex]
// Update the statistics.
n.bucket.tx.stats.Split++
return n, next
}
node(页)合并
// rebalance attempts to combine the node with sibling nodes if the node fill
// size is below a threshold or if there are not enough keys.
func (n *node) rebalance() {
if !n.unbalanced {
return
}
n.unbalanced = false
// Update statistics.
n.bucket.tx.stats.Rebalance++
// Ignore if node is above threshold (25%) and has enough keys.
var threshold = n.bucket.tx.db.pageSize / 4
if n.size() > threshold && len(n.inodes) > n.minKeys() {
return
}
// Root node has special handling. 根节点 特殊处理
if n.parent == nil {
// If root node is a branch and only has one node then collapse it.
// 根节点压缩:当根节点为分支节点且仅剩一个子节点时,将子节点提升为根,减少树高度。
if !n.isLeaf && len(n.inodes) == 1 {
// Move root's child up.
child := n.bucket.node(n.inodes[0].pgid, n)
n.isLeaf = child.isLeaf
n.inodes = child.inodes[:]
n.children = child.children
// Reparent all child nodes being moved.
for _, inode := range n.inodes {
if child, ok := n.bucket.nodes[inode.pgid]; ok {
child.parent = n
}
}
// Remove old child.
child.parent = nil
delete(n.bucket.nodes, child.pgid)
child.free()
}
return
}
// If node has no keys then just remove it. 没有孩子节点
if n.numChildren() == 0 {
// 空节点删除
n.parent.del(n.key)
n.parent.removeChild(n)
delete(n.bucket.nodes, n.pgid)
n.free()
n.parent.rebalance()
return
}
_assert(n.parent.numChildren() > 1, "parent must have at least 2 children")
// 兄弟节点合并
// Destination node is right sibling if idx == 0, otherwise left sibling.
var target *node
var useNextSibling = (n.parent.childIndex(n) == 0)
if useNextSibling {
target = n.nextSibling()
} else {
target = n.prevSibling()
}
// If both this node and the target node are too small then merge them.
if useNextSibling {
// Reparent all child nodes being moved.
// 与右兄弟合并
for _, inode := range target.inodes {
if child, ok := n.bucket.nodes[inode.pgid]; ok {
child.parent.removeChild(child)
child.parent = n
child.parent.children = append(child.parent.children, child)
}
}
// Copy over inodes from target and remove target.
n.inodes = append(n.inodes, target.inodes...)
n.parent.del(target.key)
n.parent.removeChild(target)
delete(n.bucket.nodes, target.pgid)
target.free()
} else {
// 与左兄弟合并
// Reparent all child nodes being moved.
for _, inode := range n.inodes {
if child, ok := n.bucket.nodes[inode.pgid]; ok {
child.parent.removeChild(child)
child.parent = target
child.parent.children = append(child.parent.children, child)
}
}
// Copy over inodes to target and remove node.
target.inodes = append(target.inodes, n.inodes...)
n.parent.del(n.key)
n.parent.removeChild(n)
delete(n.bucket.nodes, n.pgid)
n.free()
}
// Either this node or the target node was deleted from the parent so rebalance it.
n.parent.rebalance() //递归处理
}
// childIndex returns the index of a given child node.
func (n *node) childIndex(child *node) int {
index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].key, child.key) != -1 })
return index
}
// numChildren returns the number of children.
func (n *node) numChildren() int {
return len(n.inodes)
}
// nextSibling returns the next node with the same parent.
func (n *node) nextSibling() *node {
if n.parent == nil {
return nil
}
index := n.parent.childIndex(n)
if index >= n.parent.numChildren()-1 {
return nil
}
return n.parent.childAt(index + 1)
}
// prevSibling returns the previous node with the same parent.
func (n *node) prevSibling() *node {
if n.parent == nil {
return nil
}
index := n.parent.childIndex(n)
if index == 0 {
return nil
}
return n.parent.childAt(index - 1)
}
说明:兄弟节点合并有可能超过磁盘页,当时最终刷盘的时候,还会有一次spill 。
Bucket
// Bucket represents a collection of key/value pairs inside the database.
// 逻辑结构
type Bucket struct {
*bucket。// 嵌入的持久化数据(如根页 ID 和序列号)
tx *Tx // the associated transaction 关联的事务,确保操作在事务内进行。
buckets map[string]*Bucket // subbucket cache 子桶缓存,加速子桶访问
page *page // inline page reference 内联页引用(小桶直接存储数据,无需独立页)。
rootNode *node // materialized node for the root page. B+ 树根节点(内存中的树结构)。
nodes map[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 // 节点填充率阈值(默认 0.5,控制分裂行为)
}
// 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 // page id of the bucket's root-level page。根页 ID(若为内联桶,则为 0)
sequence uint64 // monotonically incrementing, used by NextSequence() 序列号生成器(用于 NextSequence())。
}
- 内联桶(Inline Bucket)
条件:当桶的键值对数据量小于父页剩余空间时,直接内联存储。
实现:
bucket.root 设为 0,数据序列化到父页的值部分。
Bucket.page 指向该内联页,无需分配独立页。
- 树结构管理
根节点加载:
首次访问桶时,若 bucket.root 非零,从磁盘加载对应页并构建 rootNode(B+ 树根节点)。
节点缓存:
通过 nodes 缓存已加载的页,减少磁盘 I/O(例如频繁访问的页驻留内存)。
- 填充因子(FillPercent)
分裂控制:
节点插入数据时,若占用空间超过 页大小 * FillPercent,触发分裂。
优化场景:
高写入:设为 0.8~0.9,减少分裂次数。
均衡读写:默认 0.5,平衡空间和查询效率。
- 子桶管理
存储方式:
子桶以键值对形式存储在父桶中,键为子桶名,值为序列化的 bucket 结构。
缓存加速:
父桶的 buckets 字段缓存已打开的子桶实例,避免重复解析。
协作流程示例
打开桶:
从父页的值部分读取 bucket 结构。
初始化 Bucket 结构,加载内联页或根节点。
插入数据:
写入操作通过 rootNode 更新 B+ 树。
若节点超过填充因子,调用 spill() 分裂并分配新页。
访问子桶:
检查 buckets 缓存,命中则直接返回。
未命中则从父桶的键值对加载 bucket 结构,创建子桶实例。
事务提交:
递归调用 spill() 分裂所有脏节点。
更新 bucket.root 和序列号,写入磁盘。
总结
组件 职责 关键设计
Bucket 内存中的桶管理,缓存和事务关联 通过 nodes 缓存页,FillPercent 控制分裂逻辑。
bucket 磁盘上的桶元数据 内联存储优化小桶,root 指向 B+ 树根页。
通过逻辑与物理分离的设计,BoltDB 在保证 ACID 的同时,实现了高效的键值存储和灵活的桶嵌套管理。
当父桶(Parent Bucket)中创建一个子桶时,父桶的 叶子节点 中会存储一个特殊的键值对:
Key:子桶的名称(例如 "subbucket1")。
Value:序列化的 bucket 结构体(即磁盘上的元数据),包含:
go
type bucket struct {
root pgid // 子桶的根页 ID(若为内联桶,root=0)
sequence uint64 // 子桶的序列号(用于唯一 ID 生成)
}
该结构体会被编码为二进制数据,存储在父桶叶子节点的值部分。
如果子桶的数据量足够小,可能以内联方式(inline bucket)直接存储在父页中,无需分配独立页。
- 物理存储结构
a. 非内联子桶
父桶的叶子节点:
键:子桶名称(如 "subbucket1")。
值:序列化的 bucket 结构体(包含 root 和 sequence)。
子桶的独立存储:
根据 bucket.root 找到子桶的根页(可能是分支页或叶子页)。
子桶内部的数据通过独立的 B+Tree 结构存储在自己的根页下。
┌───────────────────────┐ ┌───────────────────────┐
│ 父桶的叶子页 │ │ 子桶的独立根页 │
│ │ │ │
│ Key: "subbucket1" │─────────▶ 类型: branch/leaf │
│ Value: { │ │ 存储子桶的B+Tree数据 │
│ root: 0x1234, │ │ │
│ sequence: 5 │ └───────────────────────┘
│ } (bucket结构体) │
│ │
│ 其他键值对... │
└───────────────────────┘
特点:
父桶叶子页中存储子桶元数据(root 指向独立页)。
子桶数据通过独立的根页(0x1234)组织成 B+Tree。
适用于数据量较大的子桶。
b. 内联子桶
父桶的叶子节点:
键:子桶名称(如 "small_bucket")。
值:序列化的 bucket 结构体(root=0) + 内联页数据。
内联页:
子桶的所有键值对直接存储在父页中(无需额外页分配),类似普通键值对,但受父页大小限制。
┌───────────────────────┐
│ 父桶的叶子页 │
│ │
│ Key: "subbucket2" │
│ Value: { │
│ root: 0, │──────┐
│ sequence: 2 │ │
│ } (bucket结构体) │ │
│ + 内联页数据 │◀─────┘
│ (直接存储键值对) │
│ │
│ 其他键值对... │
└───────────────────────┘
Cursor
// 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.CursorCount++
// Allocate and return a cursor.
return &Cursor{
bucket: b,
stack: make([]elemRef, 0),
}
}
// Cursor represents an iterator that can traverse over all key/value pairs in a bucket in sorted order.
// Cursors see nested buckets with value == nil.
// Cursors can be obtained from a transaction and are valid as long as the transaction is open.
//
// Keys and values returned from the cursor are only valid for the life of the transaction.
//
// Changing data while traversing with a cursor may cause it to be invalidated
// and return unexpected keys and/or values. You must reposition your cursor
// after mutating data.
type Cursor struct {
bucket *Bucket // 关联的 Bucket。根节点访问
stack []elemRef // 遍历路径栈(记录从根到当前节点的路径)
}
// elemRef represents a reference to an element on a given page/node.
type elemRef struct {
page *page。// 当前页(磁盘页的直接引用)
node *node // 当前节点(内存中的树节点)
index int // 当前元素在页/节点中的索引
}
// isLeaf returns whether the ref is pointing at a leaf page/node.
func (r *elemRef) isLeaf() bool {
if r.node != nil {
return r.node.isLeaf
}
return (r.page.flags & leafPageFlag) != 0
}
// count returns the number of inodes or page elements.
func (r *elemRef) count() int {
if r.node != nil {
return len(r.node.inodes)
}
return int(r.page.count)
}
Cursor 对外提供的接口
- 定位到某一个元素的位置
- 在当前位置从前往后找
- 在当前位置从后往前找
Seek() Next() Prev() First() Last()
Seek()
// 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.
目标:在 B+Tree 中找到 seek 键的位置。若键不存在,返回下一个更大的键(按升序)。
返回:
键值对(若键存在)。
若键不存在且无更大键,返回 nil。
若键对应子桶(Bucket),值返回 nil。
Seek(seek) 主流程:
1. 调用 c.seek(seek) 定位到目标键或下一个键。
│
├── 重置栈(c.stack = c.stack[:0])。
├── 从根页递归搜索(c.search(seek, root))。
│ │
│ ├── 分支节点:二分查找键,递归到子页。
│ └── 叶子节点:二分查找键,记录索引。
│
└── 检查是否超出页末尾,若超出则调用 c.next()。
2. 处理结果:
├── 若找到子桶(flags & bucketLeafFlag),返回 (key, nil)。
└── 否则返回键值对或 nil。
func (c *Cursor) Seek(seek []byte) (key []byte, value []byte) {
// 1. 调用内部 seek 方法定位到键
k, v, flags := c.seek(seek)
// If we ended up after the last element of a page then move to the next one.
// 2. 检查是否超出当前页末尾
if ref := &c.stack[len(c.stack)-1]; ref.index >= ref.count() {
k, v, flags = c.next()
}
// 3. 处理子桶标志或空键
if k == nil {
// 若键不存在且无更大键,返回 nil。
return nil, nil
} else if (flags & uint32(bucketLeafFlag)) != 0 {
// 若键对应子桶(Bucket),值返回 nil。
return k, nil
}
return k, v
}
// 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) {
_assert(c.bucket.tx.db != nil, "tx closed")
// Start from root page/node and traverse to correct page.
// 1. 清空栈,从根页开始搜索
c.stack = c.stack[:0]
c.search(seek, c.bucket.root)
// 2. 检查是否超出页末尾
ref := &c.stack[len(c.stack)-1]
// If the cursor is pointing to the end of page/node then return nil.
if ref.index >= ref.count() {
//若搜索后索引超出页末尾,返回空。
return nil, nil, 0
}
// If this is a bucket then return a nil value.
// 3. 返回键值对(处理子桶标志)
return c.keyValue()
}
// search recursively performs a binary search against a given page/node until it finds a given key.
func (c *Cursor) search(key []byte, pgid pgid) {
// // 1. 获取页或内存节点
p, n := c.bucket.pageNode(pgid) //
if p != nil && (p.flags&(branchPageFlag|leafPageFlag)) == 0 {
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.
// 2. 判断节点类型
if e.isLeaf() {
c.nsearch(key) // 叶子节点:直接搜索键
return
}
// 3. 分支节点:继续递归搜索
if n != nil {
c.searchNode(key, n) // 内存节点
return
}
c.searchPage(key, p) // 磁盘页
}
func (c *Cursor) searchNode(key []byte, n *node) {
// 内存分支节点搜索
// // 1. 二分查找键的位置
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 // 找到第一个 >= key 的索引
})
// 2. 调整索引以选择左子节点 因分支节点的键是右子节点的最小值
if !exact && index > 0 {
index--
}
// 3. 记录索引并递归搜索子页
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 *page) {
// 类似 searchNode,但操作磁盘页元素
// 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)
}
// nsearch searches the leaf node on the top of the stack for a key.
func (c *Cursor) nsearch(key []byte) {
// 叶子节点搜索
e := &c.stack[len(c.stack)-1]
p, n := e.page, e.node
// If we have a node then search its inodes. 在叶子节点中二分查找键
if n != nil { // 内存页
index := sort.Search(len(n.inodes), func(i int) bool {
return bytes.Compare(n.inodes[i].key, key) != -1
})
e.index = index // index 指向第一个 >= key 的键位置。若键不存在,可能指向页末尾(index == count)
return
}
// If we have a page then search its leaf elements. // 磁盘页
inodes := p.leafPageElements()
index := sort.Search(int(p.count), func(i int) bool {
return bytes.Compare(inodes[i].key(), key) != -1
})
e.index = index
}
假如B+树如下
Root (Branch Page)
├── [Key1, Child1] → Branch Page
│ ├── [KeyA, ChildA] → Leaf Page
│ └── [KeyB, ChildB] → Leaf Page
└── [Key2, Child2] → Leaf Page
当 Cursor 定位到 KeyB 时,其 stack 的状态为:
stack = [
{page: RootPage, node: nil, index: 0}, // 根分支页,指向 Child1
{page: Child1Page, node: nil, index: 1}, // 子分支页,指向 KeyB
{page: ChildBPage, node: nil, index: 0} // 叶子页,KeyB 对应的键值对
]
Root (Branch Page)
/ \
[Key1, Child1] [Key2, Child2]
|
▼
Child1 (Branch Page)
/
[KeyA, ChildA] [KeyB, ChildB]
|
▼
ChildB (Leaf Page)
┌───────────────┐
│ KeyB: ValueB │ ← Cursor 当前位置 (index=0)
└───────────────┘
b. 定位到键 (Seek)
从根页开始,逐层向下查找。
在分支页中根据键比较选择子页。
到达叶子页后,找到第一个 >= seek 的键。
如果超出当前页,尝试跳转到下一个页(c.next())。
c. 移动到下一个键 (Next)
增加当前 index。
若 index >= count(),弹出栈顶,回溯到父节点并移动到下一个子页。
若当前是叶子页,直接返回下一个键值对。
First()
目标:将游标移动到当前 Bucket 的 第一个键值对(按升序排列)。
First() 主流程:
- 重置栈,从根页开始。
- 递归深入最左侧分支,直到叶子节点。
- 处理空页(调用 next())。
- 返回键值对或 nil。
// 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) {
_assert(c.bucket.tx.db != nil, "tx closed")
c.stack = c.stack[:0] // 1. 重置游标栈
p, n := c.bucket.pageNode(c.bucket.root) // 2. 获取根页或内存节点
c.stack = append(c.stack, elemRef{page: p, node: n, index: 0}) // 3. 将根节点压入栈(索引初始化为0)
c.first() // 4. 递归深入最左侧分支,直到叶子节点
// If we land on an empty page then move to the next value. // 5. 处理空页(例如删除后遗留的空页)
// https://github.com/boltdb/bolt/issues/450
if c.stack[len(c.stack)-1].count() == 0 {
c.next()
}
k, v, flags := c.keyValue()
if (flags & uint32(bucketLeafFlag)) != 0 {
return k, nil // 若第一个键是子桶(Bucket),返回 (key, nil)。
}
return k, v // 第一个键值对(若存在)。
}
// first moves the cursor to the first leaf element under the last page in the stack.
func (c *Cursor) first() {
for {
// Exit when we hit a leaf page.
var ref = &c.stack[len(c.stack)-1] // 1. 获取栈顶元素
if ref.isLeaf() {
break // 2. 如果是叶子节点,终止循环
}
// Keep adding pages pointing to the first element to the stack.
var pgid pgid // 3. 获取子页的 pgid(分支节点的第一个子页)
if ref.node != nil {
pgid = ref.node.inodes[ref.index].pgid
} else {
pgid = ref.page.branchPageElement(uint16(ref.index)).pgid
}
p, n := c.bucket.pageNode(pgid) // 4. 加载子页或节点,压入栈(索引初始化为0)
c.stack = append(c.stack, elemRef{page: p, node: n, index: 0})
}
}
Next()
目标:将游标移动到当前键的下一个键值对(按升序)。
// 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) {
_assert(c.bucket.tx.db != nil, "tx closed")
k, v, flags := c.next()
if (flags & uint32(bucketLeafFlag)) != 0 {
return k, nil. // 若下一个键是子桶,返回 (key, nil)。
}
return k, v
}
初始状态:
stack = [
{page: RootPage, index: 0},
{page: Child1Page, index: 0},
{page: LeafPage, index: 3} // 当前键位置
]
操作:
1. 叶子节点索引加1 → index=4。
2. 若 index >= count(),弹出栈顶,回溯到父节点。
3. 父节点索引加1,继续向下查找。
初始栈状态(叶子节点已满):
stack = [
{page: Root, index: 0}, // 分支页,有2个子页
{page: Child1, index: 1}, // 分支页,有3个子页(索引1是最后一个)
{page: LeafA, index: 2} // 叶子页,count=3(索引2是最后一个)
]
循环找到 Root 节点(i=0),递增 Root.index 从 0 → 1。
// next moves to the next leaf element and returns the key and value.
// If the cursor is at the last leaf element then it stays there and returns nil.
func (c *Cursor) next() (key []byte, value []byte, flags uint32) {
for {
// Attempt to move over one element until we're successful.
// Move up the stack as we hit the end of each page in our stack.
// 从当前叶子节点(栈顶)向根节点方向遍历。
var i int
for i = len(c.stack) - 1; i >= 0; i-- {
elem := &c.stack[i]
if elem.index < elem.count()-1 {. // 找到第一个可递增索引的分支或叶子节点(elem.index < count-1)。
elem.index++
break
}
}
// If we've hit the root page then stop and return. This will leave the
// cursor on the last element of the last page.
if i == -1 {. // // 所有节点已遍历完
return nil, nil, 0
}
// Otherwise start from where we left off in the stack and find the
// first element of the first leaf page.
c.stack = c.stack[:i+1]. // // 保留到可递增节点的路径
c.first(). // 深入该节点的最左叶子
// If this is an empty page then restart and move back up the stack.
// https://github.com/boltdb/bolt/issues/450
if c.stack[len(c.stack)-1].count() == 0 {
continue. // 跳过空页,重新查找
}
return c.keyValue()
}
}
Bucket 增删改查
Bucket(name) 根据名称获取当前 Bucket 下的嵌套子桶(Subbucket)。
// 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 {
// b.buckets 是父桶的子桶缓存(map[string]*Bucket)。 检查缓存
if b.buckets != nil {
if child := b.buckets[string(name)]; child != nil {
return child
}
}
// Move cursor to key. 创建游标 c,用于遍历当前 Bucket 的键值对。
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&bucketLeafFlag) == 0 {
return nil
}
// Otherwise create a bucket and cache it.
var child = b.openBucket(v)
if b.buckets != nil {
b.buckets[string(name)] = child //若父桶的 buckets 已初始化,将子桶加入缓存。
}
return child
}
// 存储的子桶元数据(序列化的字节数据)转换为内存中的 Bucket 对象
func (b *Bucket) openBucket(value []byte) *Bucket {
var child = newBucket(b.tx) // 创建子桶并关联事务
// If unaligned load/stores are broken on this arch and value is
// unaligned simply clone to an aligned byte array. 处理非对齐内存访问
unaligned := brokenUnaligned && uintptr(unsafe.Pointer(&value[0]))&3 != 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. 解析 bucket 元数据
if b.tx.writable && !unaligned {
child.bucket = &bucket{} // 可写事务(需数据隔离) 创建新的 bucket 结构体,将 value 中的元数据复制到新对象。 可写事务可能修改数据,直接引用父页(mmap 内存)会破坏事务隔离性
*child.bucket = *(*bucket)(unsafe.Pointer(&value[0]))
} else {
child.bucket = (*bucket)(unsafe.Pointer(&value[0])) // 直接引用 value 中的元数据(指针转换),避免复制开销。
}
// Save a reference to the inline page if the bucket is inline. 内联桶标识:child.root == 0 表示子桶数据直接内联存储在父页中。
if child.root == 0 {
child.page = (*page)(unsafe.Pointer(&value[bucketHeaderSize]))
}
return &child
}
CreateBucket
在当前 Bucket 下创建一个新的子桶(Subbucket)
关键操作:
错误检查(事务状态、键合法性)。
键冲突校验。
创建子桶元数据并插入到 B+Tree。
处理内联桶转换。
// 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) (*Bucket, error) {
if b.tx.db == nil {
return nil, ErrTxClosed. // 事务是否关闭
} else if !b.tx.writable {
return nil, ErrTxNotWritable // 事务是否可写
} else if len(key) == 0 {
return nil, ErrBucketNameRequired // 键是否为空
}
// Move cursor to correct position. 游标定位
c := b.Cursor()
k, _, flags := c.seek(key)
// Return an error if there is an existing key. 键冲突检查
if bytes.Equal(key, k) {
if (flags & bucketLeafFlag) != 0 {
return nil, ErrBucketExists
}
return nil, ErrIncompatibleValue
}
// Create empty, inline bucket. 创建子桶元数据
var bucket = Bucket{
bucket: &bucket{},
rootNode: &node{isLeaf: true},
FillPercent: DefaultFillPercent,
}
var value = bucket.write(). // 将子桶元数据序列化为字节流 value。
// Insert into node. 插入键值对
key = cloneBytes(key)
c.node().put(key, key, value, 0, 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. // 若父桶原本是内联桶(数据存储在父页中),创建子桶后需转为普通桶(使用独立页). 设置 b.page = nil 强制后续操作使用 B+Tree 节点(rootNode),而非内联页。
return b.Bucket(key), nil.
}
DeleteBucket 删除指定名称的子桶(Subbucket)及其所有嵌套子桶。
关键操作:
错误检查(事务状态、键合法性)。
递归删除所有嵌套子桶。
释放子桶占用的物理页。
更新父桶的 B+Tree 和缓存。
// DeleteBucket deletes a bucket at the given key.
// Returns an error if the bucket does not exists, or if the key represents a non-bucket value.
func (b *Bucket) DeleteBucket(key []byte) error {
if b.tx.db == nil {
return ErrTxClosed
} else if !b.Writable() {
return ErrTxNotWritable
}
// Move cursor to correct position.
c := b.Cursor()
k, _, flags := c.seek(key)
// Return an error if bucket doesn't exist or is not a bucket.
if !bytes.Equal(key, k) {
return ErrBucketNotFound
} else if (flags & bucketLeafFlag) == 0 {
return ErrIncompatibleValue
}
// Recursively delete all child buckets.
child := b.Bucket(key)
err := child.ForEach(func(k, v []byte) error {
if v == nil { // 若值为 nil → 递归调用 DeleteBucket 删除嵌套子桶。
if err := child.DeleteBucket(k); err != nil {
return fmt.Errorf("delete bucket: %s", err)
}
}
return nil. // 普通键值对(v != nil)无需处理,子桶删除后自动清除。
})
if err != nil {
return err
}
// Remove cached copy.
delete(b.buckets, string(key)). // 移除父桶缓存
// 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(key). // 从 B+Tree 中删除键
return nil
}
BoltDB上层接口 增删改查
// 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.
func (b *Bucket) Get(key []byte) []byte {
k, v, flags := b.Cursor().seek(key)
// Return nil if this is a bucket.
if (flags & 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
}
// 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) error {
if b.tx.db == nil {
return ErrTxClosed
} else if !b.Writable() {
return ErrTxNotWritable
} else if len(key) == 0 {
return ErrKeyRequired
} else if len(key) > MaxKeySize {
return ErrKeyTooLarge
} else if int64(len(value)) > MaxValueSize {
return ErrValueTooLarge
}
// Move cursor to correct position.
c := b.Cursor()
k, _, flags := c.seek(key)
// Return an error if there is an existing key with a bucket value.
if bytes.Equal(key, k) && (flags&bucketLeafFlag) != 0 {
return ErrIncompatibleValue
}
// Insert into node.
key = cloneBytes(key)
c.node().put(key, key, value, 0, 0)
return nil
}
// 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
}
// ForEach executes a function for each key/value pair in a bucket.
// If the provided function returns an error then the iteration is stopped and
// the error is returned to the caller. The provided function must not modify
// the bucket; this will result in undefined behavior.
func (b *Bucket) ForEach(fn func(k, v []byte) error) error {
if b.tx.db == nil {
return ErrTxClosed
}
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
if err := fn(k, v); err != nil {
return err
}
}
return nil
}
Bucket 的分裂 合并
spill() 方法的作用是将当前桶(Bucket)及其所有子桶的数据持久化到脏页中。
这是一种“写回”操作,用于将内存中的数据结构同步到磁盘,以确保数据一致性和持久性。
// spill writes all the nodes for this bucket to dirty pages.
func (b *Bucket) spill() error {
// Spill all child buckets first. // 持久化所有子桶
for name, child := range b.buckets {
// If the child bucket is small enough and it has no child buckets then
// write it inline into the parent bucket's page. Otherwise spill it
// like a normal bucket and make the parent value a pointer to the page.
var value []byte
if child.inlineable() {
child.free()
value = child.write() // 对于小型且不包含子桶的桶,BoltDB 允许将其数据内联存储,即直接将数据写入父桶的页中,而不是为其分配独立的页面。这种方式可以减少页面的分配和管理开销,提高性能。
} else {
if err := child.spill(); err != nil {
return err
}
// Update the child bucket header in this bucket.
value = make([]byte, unsafe.Sizeof(bucket{}))
var bucket = (*bucket)(unsafe.Pointer(&value[0]))
*bucket = *child.bucket
}
// Skip writing the bucket if there are no materialized nodes.
if child.rootNode == nil {
continue
}
// Update parent node.
var c = b.Cursor()
k, _, flags := c.seek([]byte(name))
if !bytes.Equal([]byte(name), k) {
panic(fmt.Sprintf("misplaced bucket header: %x -> %x", []byte(name), k))
}
if flags&bucketLeafFlag == 0 {
panic(fmt.Sprintf("unexpected bucket header flag: %x", flags))
}
c.node().put([]byte(name), []byte(name), value, 0, bucketLeafFlag)
}
// Ignore if there's not a materialized root node.
if b.rootNode == nil {
return nil
}
// Spill nodes. // 递归地将根节点的数据持久化
if err := b.rootNode.spill(); err != nil {
return err
}
b.rootNode = b.rootNode.root()
// Update the root node for this bucket.
if b.rootNode.pgid >= b.tx.meta.pgid {
panic(fmt.Sprintf("pgid (%d) above high water mark (%d)", b.rootNode.pgid, b.tx.meta.pgid))
}
b.root = b.rootNode.pgid
return nil
}
// rebalance attempts to balance all nodes.
func (b *Bucket) rebalance() {
for _, n := range b.nodes {
n.rebalance()
}
for _, child := range b.buckets {
child.rebalance()
}
}
Tx解析
BoltDB支持 只读事务 和读写事务这俩种。根据不同的底层使用不同的锁。
type Tx struct {
writable bool. //指示事务是否为可写事务。
managed bool. // 标识事务是否由数据库管理。
db *DB. // 指向所属数据库实例的指针。
meta *meta. // 事务的元数据,包括事务 ID 和页面 ID 等信息。
root Bucket // 根桶,作为事务的起始数据结构。
pages map[pgid]*page. // 页面缓存,用于存储事务期间修改的页面。
stats TxStats. //事务统计信息。
commitHandlers []func(). // 提交时执行的回调函数列表。
// WriteFlag specifies the flag for write-related methods like WriteTo().
// Tx opens the database file with the specified flag to copy the data.
//
// 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. // 指定写操作相关方法的标志位,例如 WriteTo()。
}
// init initializes the transaction.
func (tx *Tx) init(db *DB) {
// 1. 设置数据库实例
tx.db = db
tx.pages = nil
// Copy the meta page since it can be changed by the writer.
// 2. 复制元数据页
tx.meta = &meta{}
db.meta().copy(tx.meta)
// Copy over the root bucket.
// 3. 复制根桶信息
tx.root = newBucket(tx)
tx.root.bucket = &bucket{}
*tx.root.bucket = tx.meta.root
// Increment the transaction id and add a page cache for writable transactions.
// 4. 增加事务 ID 并为可写事务添加页面缓存
if tx.writable {
tx.pages = make(map[pgid]*page)
tx.meta.txid += txid(1)
}
}
Commit
// Commit writes all changes to disk and updates the meta page.
// Returns an error if a disk write error occurs, or if Commit is
// called on a read-only transaction.
func (tx *Tx) Commit() error {
_assert(!tx.managed, "managed tx commit not allowed")
if tx.db == nil {
return ErrTxClosed
} else if !tx.writable {
return ErrTxNotWritable
}
// TODO(benbjohnson): Use vectorized I/O to write out dirty pages.
// Rebalance nodes which have had deletions.
var startTime = time.Now()
tx.root.rebalance() // 节点重平衡
if tx.stats.Rebalance > 0 {
tx.stats.RebalanceTime += time.Since(startTime)
}
// spill data onto dirty pages. 数据写入脏页面
startTime = time.Now()
if err := tx.root.spill(); err != nil {
tx.rollback()
return err
}
tx.stats.SpillTime += time.Since(startTime)
// Free the old root bucket. 更新根桶信息
tx.meta.root.root = tx.root.root // 将当前根桶的页 ID 更新到元数据中。
opgid := tx.meta.pgid
// Free the freelist and allocate new pages for it. This will overestimate
// the size of the freelist but not underestimate the size (which would be bad).
// 释放当前事务使用的页面,分配新的空闲页面,更新元数据。
tx.db.freelist.free(tx.meta.txid, tx.db.page(tx.meta.freelist))
p, err := tx.allocate((tx.db.freelist.size() / tx.db.pageSize) + 1)
if err != nil {
tx.rollback()
return err
}
if err := tx.db.freelist.write(p); err != nil {
tx.rollback()
return err
}
tx.meta.freelist = p.id
// If the high water mark has moved up then attempt to grow the database.
// 如果数据库的页 ID 超过当前文件大小,尝试扩展数据库文件以容纳更多数据。
if tx.meta.pgid > opgid {
if err := tx.db.grow(int(tx.meta.pgid+1) * tx.db.pageSize); err != nil {
tx.rollback()
return err
}
}
// Write dirty pages to disk.
// 将所有脏页面写入磁盘,确保数据持久化。
startTime = time.Now()
if err := tx.write(); err != nil {
tx.rollback()
return err
}
// If strict mode is enabled then perform a consistency check.
// Only the first consistency error is reported in the panic.
// 在严格模式下,执行一致性检查,确保数据库结构的完整性。
if tx.db.StrictMode {
ch := tx.Check()
var errs []string
for {
err, ok := <-ch
if !ok {
break
}
errs = append(errs, err.Error())
}
if len(errs) > 0 {
panic("check fail: " + strings.Join(errs, "\n"))
}
}
// Write meta to disk. 将更新后的元数据写入磁盘,完成事务的持久化。
if err := tx.writeMeta(); err != nil {
tx.rollback()
return err
}
tx.stats.WriteTime += time.Since(startTime)
// Finalize the transaction. 关闭事务,释放相关资源。
tx.close()
// Execute commit handlers now that the locks have been removed.
// 执行所有在事务提交时注册的回调函数。
for _, fn := range tx.commitHandlers {
fn()
}
return nil
}
脏页 刷盘
遍历当前事务中的所有脏页面,将它们按页 ID 升序排序后,分批次写入磁盘。 写入过程中,采用最大分配大小限制,以优化性能并减少系统调用的次数。 写入完成后,执行 fdatasync() 确保数据已刷新到磁盘。 最后,将小页面(即未溢出的页面)返回页面池,以便重用。
// write writes any dirty pages to disk.
func (tx *Tx) write() error {
// Sort pages by id. // 将脏页面按 ID 排序
pages := make(pages, 0, len(tx.pages))
for _, p := range tx.pages {
pages = append(pages, p)
}
// Clear out page cache early.
tx.pages = make(map[pgid]*page) // 清空页面缓存
sort.Sort(pages) // 排序
// Write pages to disk in order. // 遍历每个页面,按顺序写入磁盘
for _, p := range pages {
size := (int(p.overflow) + 1) * tx.db.pageSize. // 计算页面总大小
offset := int64(p.id) * int64(tx.db.pageSize) // 计算页面在文件中的偏移量
// Write out page in "max allocation" sized chunks.
// 分块写入页面数据,避免单次写入过大
ptr := (*[maxAllocSize]byte)(unsafe.Pointer(p))
for {
// Limit our write to our max allocation size.
sz := size
if sz > maxAllocSize-1 {
sz = maxAllocSize - 1
}
// Write chunk to disk.
buf := ptr[:sz]
if _, err := tx.db.ops.writeAt(buf, offset); err != nil {
return err
}
// Update statistics. // 更新写入统计
tx.stats.Write++
// Exit inner for loop if we've written all the chunks.
size -= sz
if size == 0 {
break
}
// Otherwise move offset forward and move pointer to next chunk.
offset += int64(sz)
ptr = (*[maxAllocSize]byte)(unsafe.Pointer(&ptr[sz]))
}
}
// Ignore file sync if flag is set on DB. // 如果数据库设置了 NoSync 或 IgnoreNoSync 标志,则跳过文件同步
if !tx.db.NoSync || IgnoreNoSync {
if err := fdatasync(tx.db); err != nil {
return err
}
}
// Put small pages back to page pool. // 将小页面(未溢出)返回页面池
for _, p := range pages {
// Ignore page sizes over 1 page.
// These are allocated using make() instead of the page pool.
if int(p.overflow) != 0 {
continue
}
buf := (*[maxAllocSize]byte)(unsafe.Pointer(p))[:tx.db.pageSize]
// See https://go.googlesource.com/go/+/f03c9202c43e0abb130669852082117ca50aa9b1
for i := range buf {
buf[i] = 0
}
tx.db.pagePool.Put(buf)
}
return nil
}
RollBack 关闭当前事务,忽略所有之前的更新。如果事务是可写的,它会调用 rollback() 方法来撤销更改
// Rollback closes the transaction and ignores all previous updates. Read-only
// transactions must be rolled back and not committed.
func (tx *Tx) Rollback() error {
_assert(!tx.managed, "managed tx rollback not allowed")
if tx.db == nil {
return ErrTxClosed
}
tx.rollback()
return nil
}
func (tx *Tx) rollback() {
if tx.db == nil {
return
}
if tx.writable {
tx.db.freelist.rollback(tx.meta.txid) //将事务 ID 对应的更改从 freelist 中撤销。
tx.db.freelist.reload(tx.db.page(tx.db.meta().freelist)) // 重新加载 freelist 页面。
}
tx.close() // 关闭事务
}
// 释放事务占用的资源,如页面缓存和事务元数据。
func (tx *Tx) close() {
if tx.db == nil {
return
}
if tx.writable {
// Grab freelist stats.
var freelistFreeN = tx.db.freelist.free_count()
var freelistPendingN = tx.db.freelist.pending_count()
var freelistAlloc = tx.db.freelist.size()
// 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
}
DB 解析
DB 是bucket 的集合
// DB represents a collection of buckets persisted to a file on disk.
// All data access is performed through transactions which can be obtained through the DB.
// All the functions on DB will return a ErrDatabaseNotOpen if accessed before Open() is called.
type DB struct {
// When enabled, the database will perform a Check() after every commit.
// A panic is issued if the database is in an inconsistent state. This
// flag has a large performance impact so it should only be used for
// debugging purposes.
StrictMode bool
// Setting the NoSync flag will cause the database to skip fsync()
// calls after each commit. This can be useful when bulk loading data
// into a database and you can restart the bulk load in the event of
// a system failure or database corruption. Do not set this flag for
// normal use.
//
// If the package global IgnoreNoSync constant is true, this value is
// ignored. See the comment on that constant for more details.
//
// THIS IS UNSAFE. PLEASE USE WITH CAUTION.
NoSync bool
// When true, skips the truncate call when growing the database.
// Setting this to true is only safe on non-ext3/ext4 systems.
// Skipping truncation avoids preallocation of hard drive space and
// bypasses a truncate() and fsync() syscall on remapping.
//
// https://github.com/boltdb/bolt/issues/284
NoGrowSync bool
// If you want to read the entire database fast, you can set MmapFlag to
// syscall.MAP_POPULATE on Linux 2.6.23+ for sequential read-ahead.
MmapFlags int
// MaxBatchSize is the maximum size of a batch. Default value is
// copied from DefaultMaxBatchSize in Open.
//
// If <=0, disables batching.
//
// Do not change concurrently with calls to Batch.
MaxBatchSize int
// MaxBatchDelay is the maximum delay before a batch starts.
// Default value is copied from DefaultMaxBatchDelay in Open.
//
// If <=0, effectively disables batching.
//
// Do not change concurrently with calls to Batch.
MaxBatchDelay time.Duration
// AllocSize is the amount of space allocated when the database
// needs to create new pages. This is done to amortize the cost
// of truncate() and fsync() when growing the data file.
AllocSize int
path string // 文件存储路径
file *os.File //真实的存储数据的磁盘文件
lockfile *os.File // windows only
dataref []byte // mmap'ed readonly, write throws SEGV
data *[maxMapSize]byte // 通过mmap 映射进来的地址
datasz int
filesz int // current on disk file size
meta0 *meta
meta1 *meta
pageSize int
opened bool
rwtx *Tx
txs []*Tx
freelist *freelist
stats Stats
pagePool sync.Pool
batchMu sync.Mutex
batch *batch
rwlock sync.Mutex // Allows only one writer at a time. 读写锁
metalock sync.Mutex // Protects meta page access.
mmaplock sync.RWMutex // Protects mmap access during remapping.
statlock sync.RWMutex // Protects stats access.
ops struct {
writeAt func(b []byte, off int64) (n int, err error)
}
// Read only mode.
// When true, Update() and Begin(true) return ErrDatabaseReadOnly immediately.
readOnly bool
}
Open
// Open creates and opens a database at the given path.
// If the file does not exist then it will be created automatically.
// Passing in nil options will cause Bolt to open the database with the default options.
func Open(path string, mode os.FileMode, options *Options) (*DB, error) {
var db = &DB{opened: true}
// Set default options if no options are provided.
if options == nil {
options = DefaultOptions
}
db.NoGrowSync = options.NoGrowSync
db.MmapFlags = options.MmapFlags
// Set default values for later DB operations.
db.MaxBatchSize = DefaultMaxBatchSize
db.MaxBatchDelay = DefaultMaxBatchDelay
db.AllocSize = DefaultAllocSize
flag := os.O_RDWR
if options.ReadOnly {
flag = os.O_RDONLY
db.readOnly = true
}
// Open data file and separate sync handler for metadata writes.
db.path = path
var err error
if db.file, err = os.OpenFile(db.path, flag|os.O_CREATE, mode); err != nil { // 打开数据库文件
_ = db.close()
return nil, err
}
// Lock file so that other processes using Bolt in read-write mode cannot
// use the database at the same time. This would cause corruption since
// the two processes would write meta pages and free pages separately.
// The database file is locked exclusively (only one process can grab the lock)
// if !options.ReadOnly.
// The database file is locked using the shared lock (more than one process may
// hold a lock at the same time) otherwise (options.ReadOnly is set).
if err := flock(db, mode, !db.readOnly, options.Timeout); err != nil {
_ = db.close()
return nil, err
}
// Default values for test hooks
db.ops.writeAt = db.file.WriteAt
// Initialize the database if it doesn't exist.
if info, err := db.file.Stat(); err != nil {
return nil, err
} else if info.Size() == 0 { // 若文件大小为 0(新数据库)
// Initialize new files with meta pages.
// 初始化新数据库
if err := db.init(); err != nil {
return nil, err
}
} else {
// Read the first meta page to determine the page size.
var buf [0x1000]byte
if _, err := db.file.ReadAt(buf[:], 0); err == nil {
m := db.pageInBuffer(buf[:], 0).meta()
if err := m.validate(); err != nil {
// If we can't read the page size, we can assume it's the same
// as the OS -- since that's how the page size was chosen in the
// first place.
//
// If the first page is invalid and this OS uses a different
// page size than what the database was created with then we
// are out of luck and cannot access the database.
db.pageSize = os.Getpagesize()
} else {
db.pageSize = int(m.pageSize)
}
}
}
// Initialize page pool.
db.pagePool = sync.Pool{
New: func() interface{} {
return make([]byte, db.pageSize)
},
}
// Memory map the data file.
if err := db.mmap(options.InitialMmapSize); err != nil {
_ = db.close()
return nil, err
}
// Read in the freelist.
db.freelist = newFreelist()
db.freelist.read(db.page(db.meta().freelist)) // 从元数据页中读取空闲页列表,用于高效管理数据页的分配与回收。
// Mark the database as opened and return.
return db, nil
}
// init creates a new database file and initializes its meta pages.
// 负责创建并初始化一个新数据库文件的核心结构,包括 元页面(Meta Pages)、空闲列表(Freelist) 和 根桶(Root Bucket),为后续数据库操作奠定基础。
func (db *DB) init() error {
// Set the page size to the OS page size.
db.pageSize = os.Getpagesize()
// Create two meta pages on a buffer.
//分配 4个页 的缓冲区,用于暂存初始页面:
// 页 0 和 1:元页面(Meta Pages)
// 页 2:空闲列表(Freelist)
// 页 3:根桶(Root Bucket)
buf := make([]byte, db.pageSize*4)
for i := 0; i < 2; i++ {
p := db.pageInBuffer(buf[:], pgid(i))
p.id = pgid(i)
p.flags = metaPageFlag
// Initialize the meta page.
m := p.meta()
m.magic = magic
m.version = version
m.pageSize = uint32(db.pageSize)
m.freelist = 2 // 空闲列表起始页为页2
m.root = bucket{root: 3} // 根桶起始页为页3
m.pgid = 4 // 下一个可分配的页ID
m.txid = txid(i) // 事务ID(页0为0,页1为1
m.checksum = m.sum64()
}
// Write an empty freelist at page 3. freelistPageFlag,count=0 表示初始数据库无空闲页(所有页已分配)
p := db.pageInBuffer(buf[:], pgid(2))
p.id = pgid(2)
p.flags = freelistPageFlag
p.count = 0
// Write an empty leaf page at page 4. 标记为 leafPageFlag,表示存储键值对的叶子页,count=0 表示根桶无数据
p = db.pageInBuffer(buf[:], pgid(3))
p.id = pgid(3)
p.flags = leafPageFlag
p.count = 0
// Write the buffer to our data file.
if _, err := db.ops.writeAt(buf, 0); err != nil {
return err
}
if err := fdatasync(db); err != nil {
return err
}
return nil
}
Begin
Begin 函数用于启动一个数据库事务,支持 只读事务 和 读写事务。Bolt 通过多级锁机制确保事务的隔离性与并发安全性,其核心设计包括:
读写互斥:允许多个读事务并行,但同一时间仅允许一个写事务。
锁顺序控制:避免死锁,确保资源访问顺序一致。
旧页回收:写事务触发空闲页释放,优化存储利用率。
// Begin starts a new transaction.
// Multiple read-only transactions can be used concurrently but only one
// write transaction can be used at a time. Starting multiple write transactions
// will cause the calls to block and be serialized until the current write
// transaction finishes.
//
// Transactions should not be dependent on one another. Opening a read
// transaction and a write transaction in the same goroutine can cause the
// writer to deadlock because the database periodically needs to re-mmap itself
// as it grows and it cannot do that while a read transaction is open.
//
// If a long running read transaction (for example, a snapshot transaction) is
// needed, you might want to set DB.InitialMmapSize to a large enough value
// to avoid potential blocking of write transaction.
//
// IMPORTANT: You must close read-only transactions after you are finished or
// else the database will not reclaim old pages.
func (db *DB) Begin(writable bool) (*Tx, error) {
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() // 获取 mmap 读锁 先 metalock 后 mmaplock,与写事务一致,避免死锁。
// 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.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) beginRWTx() (*Tx, error) {
// If the database was opened with Options.ReadOnly, return an error.
if db.readOnly {
return nil, ErrDatabaseReadOnly
}
// Obtain writer lock. This is released by the transaction when it closes.
// This enforces only one writer transaction at a time.
db.rwlock.Lock(). // 确保全局唯一写事务,阻塞其他写操作
// Once we have the writer lock then we can lock the meta pages so that
// we can set up the transaction.
db.metalock.Lock() // 锁定元数据页
defer db.metalock.Unlock()
// Exit if the database is not open yet.
if !db.opened {
db.rwlock.Unlock()
return nil, ErrDatabaseNotOpen
}
// Create a transaction associated with the database.
t := &Tx{writable: true}
t.init(db)
db.rwtx = t. // 设置当前写事务
// 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. // 找到最小活跃事务ID
}
}
if minid > 0 {
db.freelist.release(minid - 1). // 释放更旧的页
}
return t, nil
}
Update View
DB对象对外只暴露读写接口就是Update() 和 View()
update 函数封装了读写事务的 创建、执行、提交/回滚 全流程,提供原子性操作保障。用户只需关注业务逻辑(通过回调函数 fn),无需手动处理事务状态,大幅降低出错风险。
// Update executes a function within the context of a read-write managed transaction.
// If no error is returned from the function then the transaction is committed.
// If an error is returned then the entire transaction is rolled back.
// Any error that is returned from the function or returned from the commit is
// returned from the Update() method.
//
// Attempting to manually commit or rollback within the function will cause a panic.
func (db *DB) Update(fn func(*Tx) error) error {
t, err := db.Begin(true)
if err != nil {
return err
}
// Make sure the transaction rolls back in the event of a panic. :防止用户函数 fn 发生 panic 或 未处理的错误 导致事务未正常关闭
defer func() {
if t.db != nil {
t.rollback()
}
}()
// Mark as a managed tx so that the inner function cannot manually commit. 禁止手动提交/回滚
t.managed = true
// If an error is returned from the function then rollback and return error.
err = fn(t) // 执行用户逻辑
t.managed = false //// 解除托管标记
if err != nil {
_ = t.Rollback() // 显式回滚
return err
}
return t.Commit() //若 fn 执行成功,提交事务持久化数据。
}
View
// View executes a function within the context of a managed read-only transaction.
// Any error that is returned from the function is returned from the View() method.
//
// Attempting to manually rollback within the function will cause a panic.
func (db *DB) View(fn func(*Tx) error) error {
t, err := db.Begin(false)
if err != nil {
return err
}
// Make sure the transaction rolls back in the event of a panic.
defer func() {
if t.db != nil {
t.rollback()
}
}()
// Mark as a managed tx so that the inner function cannot manually rollback.
t.managed = true
// If an error is returned from the function then pass it through.
err = fn(t)
t.managed = false
if err != nil {
_ = t.Rollback()
return err
}
if err := t.Rollback(); err != nil {
return err
}
return nil
}
回滚操作 (t.Rollback()) 仅释放资源,不涉及磁盘回退。
快照隔离:通过持有 mmaplock 读锁,保证事务期间内存映射不变,读取一致性视图。
Batch
将每次写、每次刷盘的操作转换为多次写 、一次刷盘。 提升性能。
/ Batch calls fn as part of a batch. It behaves similar to Update,
// except:
//
// 1. concurrent Batch calls can be combined into a single Bolt
// transaction.
//
// 2. the function passed to Batch may be called multiple times,
// regardless of whether it returns error or not.
//
// This means that Batch function side effects must be idempotent and
// take permanent effect only after a successful return is seen in
// caller.
//
// The maximum batch size and delay can be adjusted with DB.MaxBatchSize
// and DB.MaxBatchDelay, respectively.
//
// Batch is only useful when there are multiple goroutines calling it.
func (db *DB) Batch(fn func(*Tx) error) error {
errCh := make(chan error, 1) // 初始化错误通道
db.batchMu.Lock()
if (db.batch == nil) || (db.batch != nil && len(db.batch.calls) >= db.MaxBatchSize) {
// There is no existing batch, or the existing batch is full; start a new one.
db.batch = &batch{
db: db,
}
db.batch.timer = time.AfterFunc(db.MaxBatchDelay, db.batch.trigger)
}
db.batch.calls = append(db.batch.calls, call{fn: fn, err: errCh})
if len(db.batch.calls) >= db.MaxBatchSize {
// wake up batch, it's ready to run
go db.batch.trigger()
}
db.batchMu.Unlock()
err := <-errCh
if err == trySolo {
err = db.Update(fn) // 回退到独立事务
}
return err
}
BoltDB总结
逻辑结构
DB
├── Bucket 1 (Root Bucket)
│ ├── B+ Tree
│ │ ├── Root Node (Branch Node)
│ │ │ ├── Child Node 1 (Leaf Node, Key-Value Pairs)
│ │ │ └── Child Node 2 (Leaf Node, Key-Value Pairs)
│ │ └── ...
│ └── Sub-Buckets (Nested)
│ └── Bucket 1.1
├── Bucket 2
├── Freelist (Track Free Pages)
└── Metadata (Active/Backup Meta Pages)
物理结构
File: bolt.db
├── Page 0 (Meta Page 1) --> 存储数据库元信息(root bucket、freelist位置等)
├── Page 1 (Meta Page 2) --> 备份元页(用于崩溃恢复)
├── Page 2 (Freelist Page) --> 空闲页列表(记录可重用页的 ID)
├── Page 3 (Leaf Page) --> 根 Bucket 的数据页(初始为空)
├── Page 4 (Branch Page) --> B+ 树的分支页(路由子页)
├── Page 5 (Leaf Page) --> 存储实际键值对
└── ...
页类型与结构
页类型 物理结构 (二进制格式)
Meta Page magic(4B) | version(4B) | pageSize(4B) | rootBucket(16B) | freelistPgid(8B) | ...
Freelist Page count(8B) | pgid1(8B) | pgid2(8B) | ... (记录空闲页 ID 列表)
Branch Page flags(2B) | count(2B) | key1(16B) | pgid1(8B) | key2(16B) | pgid2(8B) | ...
Leaf Page flags(2B) | count(2B) | key1_len(4B) | key1_data | value1_len(4B) | value1_data | ...
物理结构和逻辑结构映射关系
Logical Object Physical Representation
────────────────────────────────────────────────────────────
DB ↦ Meta Pages (Pg0/Pg1) + File Memory Mapping
Bucket ↦ Root Page ID (Stored in Meta Page)
Node (Branch/Leaf) ↦ Branch/Leaf Page (Serialized to Page Buffer)
Freelist ↦ Freelist Page (Pg2) + In-Memory Cache
LSM Tree
数据库处理俩种 读多写少 或者 写多 读少。
MemTable ImmuMemtable -SSTable. LogAhead
数据压缩合并
KV分离技术
跳表
完结

浙公网安备 33010602011771号