英文源地址
由于我们的B树时不可变的, 每次对kv存储的更新都会在路径上创建新节点, 而不是更新当前节点, 从而使一些节点无法从最新版本访问到.我们需要从旧版本中重用这些不可访问的节点, 否则, 数据库文件将无限增长.
设计空闲列表
为了重用这些页, 我们将添加一个持久化存储的空闲列表来跟踪未使用的页.更新操作在添加新页之前重用列表中的页, 并且将当前版本中未使用的页添加到列表.
该列表用作栈(先进后出), 每个更新操作都可以从列表顶部删除或者添加.
func (fl *FreeList) Total() int {
}
func (fl *FreeList) Get(topn int) uint64 {
}
func (fl *FreeList) Update(popn int, freed []uint64) {
}
和b树一样, 空闲列表也是不可变的.每个节点包括:
- 指向未使用页面的多个指针
- 到下一个节点的链接
- 列表中项目的总数. 这只适用于头节点.
node的格式
const BNODE_FREE_LIST = 3
const FREE_LIST_HEADER = 4 + 8+ 8
const FREE_LIST_CAP = (BTREE_PAGE_SIZE - FREE_LIST_HEADER) / 8
访问列表节点的函数
func flnSize(node BNode) int {
}
func flnNext(node BNode) uint64 {
}
func flnPtr(node BNode, idx int) {
}
func flnSetPtr(node BNode, size uint16, next uint64){
}
func flnSetHeader(node BNode, size uint16, next uint64) {
}
func flnSetTotal(node BNode, total uint64) {
}
空闲列表的数据类型
FreeList类型包含一个指向头节点的指针和用于管理磁盘页的回调函数.
type FreeList struct {
head uint64
get func(uint64) BNode
new func(BNode) uint64
use func(uint64, BNode)
}
这些回调与b树的有所不同, 因为列表使用的页由列表本身管理.
- new回调函数只用于追加新的页, 因为空闲列表必须从自身重用页
- 没有del回调函数, 因为空闲列表将未使用的页添加给自己
- use回调函数将挂起的更新操作注册到重用的页中.
type BTree struct {
// pointer (a nonzero page number)
root uint64
// callbacks for managing on-disk pages
get func(uint64) BNode // dereference a pointer
new func(BNode) uint64 // allocate a new page
del func(uint64) // deallocate a page
}
空闲列表的实现
从列表中获取第n项只是一个简单的列表遍历.
func (fl *FreeList) Get(topn int) uint64 {
// assert(0 <= topn && topn < f1.Total())
node := fl.get(fl.head)
for flnSize(node) <= topn {
topn -= flnSize(node)
next := flnNext(node)
// assert(next != 0)
node = fl.get(next)
}
return flnPtr(node, flnSize(node)-topn-1)
}
更新列表是个棘手的操作, 它首先从列表中删除popn项, 然后将释放的项添加到列表中, 这可以分为3个阶段:
1.如果头节点大于popn, 则删除它. 节点本身稍后将添加到列表中. 重复这个步骤, 直到它不再满足条件.
2.我们可能需要从列表中删除一些项, 并可能向列表中添加一些项, 更新列表头需要新的页, 并且应该从列表本身的项中重用新的页. 一个接一个地从列表中弹出一些项, 直到有足够的页面可供下一阶段重用.
3.通过添加新节点来修改列表.
func (fl *FreeList) Update(popn int, freed []uint64) {
// assert(popn <= fl.Total())
if popn == 0 && len(freed) == 0 {
return
}
total := fl.Total()
reuse := []uint64{}
for fl.head != 0 && len(reuse)*FREE_LIST_CAP < len(freed) {
node := fl.get(fl.head)
freed = append(freed, fl.head)
if popn >= flnSize(node) {
popn -= flnSize(node)
} else {
remain := flnSize(node) - popn
popn = 0
for remain > 0 && len(reuse)*FREE_LIST_CAP < len(freed)+remain {
remain--
reuse = append(reuse, flnPtr(node, remain))
}
for i := 0; i < remain; i++ {
freed = append(freed, flnPtr(node, i))
}
}
total -= flnSize(node)
fl.head = flnNext(node)
}
// assert(len(reuse)*FREE_LIST_CAP >= len(freed) || fl.head == 0)
flPush(fl, freed, reuse)
flnSetTotal(fl.get(fl.head), uint64(total+len(freed)))
}
func flPush(fl *FreeList, freed []uint64, reuse []uint64) {
for len(freed) > 0 {
new := BNode{make([]byte, BTREE_PAGE_SIZE)}
size := len(freed)
if size > FREE_LIST_CAP {
size = FREE_LIST_CAP
}
flnSetHeader(new, uint16(size), fl.head)
for i, ptr := range freed[:size] {
flnSetPtr(new, i, ptr)
}
freed = freed[size:]
if len(reuse) > 0 {
fl.head, reuse = reuse[0], reuse[1:]
fl.use(fl.head, new)
} else {
fl.head = fl.new(new)
}
}
// assert(len(reuse) == 0)
}
管理磁盘页
完成数据结构的修改. 临时页保存在字典中, 按其分配的页码进行映射. 删除的页码也在那里.
type KV struct {
Path string
fp *os.File
tree BTree
mmap struct {
file int
total int
chunks [][]byte
}
page struct {
flushed uint64
temp [][]byte
nfree int
nappend int
updates map[uint64][]byte
}
}
b树的页管理
pageGet函数也被修改为返回临时页, 因为空闲列表代码依赖于此行为.
func (db *KV) pageGet(ptr uint64) BNode {
if page, ok := db.page.updates[ptr]; ok {
// assert(page != nil)
return BNode{page}
}
return pageGetMapped(db, ptr)
}
func pageGetMapped(db *KV, ptr uint64) BNode {
start := uint64(0)
for _, chunk := range db.mmap.chunks {
end := start + uint64(len(chunk))/BTREE_PAGE_SIZE
if ptr < end {
offset := BTREE_PAGE_SIZE * (ptr - start)
return BNode{chunk[offset : offset+BTREE_PAGE_SIZE]}
}
start = end
}
panic("bad ptr")
}
将分配b树页的函数更改为首先重用空闲列表的页
func (db *KV) pageNew(node BNode) uint64 {
// assert(len(node.data) <= BTREE_PAGE_SIZE)
ptr := uint64(0)
if db.page.nfree < db.free.Total() {
ptr = db.free.Get(db.page.nfree)
db.page.nappend++
} else {
ptr = db.page.flushed + uint64(db.page.nappend)
db.page.nappend++
}
db.page.updates[ptr] = node.data
return ptr
}
删除的页被标记, 稍后更新到空闲列表
func (db *KV) pageDel(ptr uint64) {
db.page.updates[ptr] = nil
}
空闲列表的页管理
func (db *KV) pageAppend(node BNode) uint64 {
// assert(len(node.data) <= BTREE_PAGE_SIZE)
ptr := db.page.flushed + uint64(db.page.nappend)
db.page.nappend++
db.page.updates[ptr] = node.data
return ptr
}
func (db *KV) pageUse(ptr uint64, node BNode) {
db.page.updates[ptr] = node.data
}
更新空闲列表
在扩展文件并将页写入磁盘前, 我们必须先更新空闲列表, 因为它会创建挂起的写操作
func writePages(db *KV) error {
freed := []uint64{}
for ptr, page := range db.page.updates {
if page == nil {
freed = append(freed, ptr)
}
}
db.free.Update(db.page.nfree, freed)
for ptr, page := range db.page.updates {
if page != nil {
copy(pageGetMapped(db, ptr).data, page)
}
}
return nil
}
指向列表头的指针被添加到master page中.
完成
kv存储部分完成. 它是持久化的和抗崩溃的, 尽管它只能按顺序访问.
本书的第二部分会有更多的内容(不过也收费).
- KV存储上的关系型数据库
- 并发访问数据库和事务的支持