从零实现一个数据库(DataBase) Go语言实现版 7.空闲列表: 重用页

英文源地址
由于我们的B树时不可变的, 每次对kv存储的更新都会在路径上创建新节点, 而不是更新当前节点, 从而使一些节点无法从最新版本访问到.我们需要从旧版本中重用这些不可访问的节点, 否则, 数据库文件将无限增长.

设计空闲列表

为了重用这些页, 我们将添加一个持久化存储的空闲列表来跟踪未使用的页.更新操作在添加新页之前重用列表中的页, 并且将当前版本中未使用的页添加到列表.
该列表用作栈(先进后出), 每个更新操作都可以从列表顶部删除或者添加.

func (fl *FreeList) Total() int {
	
}

func (fl *FreeList) Get(topn int) uint64 {
	
}

func (fl *FreeList) Update(popn int, freed []uint64)  {
	
}

和b树一样, 空闲列表也是不可变的.每个节点包括:

  1. 指向未使用页面的多个指针
  2. 到下一个节点的链接
  3. 列表中项目的总数. 这只适用于头节点.
    在这里插入图片描述
    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树的有所不同, 因为列表使用的页由列表本身管理.

  1. new回调函数只用于追加新的页, 因为空闲列表必须从自身重用页
  2. 没有del回调函数, 因为空闲列表将未使用的页添加给自己
  3. 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存储上的关系型数据库
  • 并发访问数据库和事务的支持
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值