簡介
介紹及簡單使用:https://www.cnblogs.com/daemon365/p/17690167.html
原始碼地址:https://github.com/etcd-io/bbolt
page
因為 boltdb 是要落盤的,所以就要操作檔案。為了提高效率,boltdb 會和其他資料庫一樣,會按 頁(page)來操作檔案。而且 boltdb 使用了 linux 的 mmap 來記憶體對映操作檔案,這樣可以提高效率。
在 linux 中,每個 page 的大小是 4KB。
getconf PAGESIZE
4096
對應的每頁在我們的代理裡也應該有一個資料結構,來儲存資料。這個資料結構就是 page
。
type Pgid uint64
type Page struct {
id Pgid
flags uint16 // page 型別
count uint16 // page 中的元素數量
overflow uint32 // 是否有後序頁,如果有,overflow 表示後續頁的數量
}
const (
BranchPageFlag = 0x01
LeafPageFlag = 0x02
MetaPageFlag = 0x04
FreelistPageFlag = 0x10
)
Page
裡面有一個 flags
欄位,用來標識這個 page 是什麼型別的。boltdb 裡面有四種型別的 page, 分別是 分支頁(BranchPageFlag)、葉子頁(LeafPageFlag)、後設資料頁(MetaPageFlag)、空閒列表頁(FreelistPageFlag)。
- 分支頁:由於 boltdb 使用的是 B+ 樹,所以分支頁用來儲存 key 和子節點的指標。
- 葉子頁:葉子頁用來儲存 key 和 value。
- 後設資料頁:後設資料頁用來儲存 boltdb 的後設資料,比如 boltdb 的版本號、boltdb 的根節點等。
- 空閒列表頁:由於 boltdb 使用 copy on write,所以當一個 page 被刪除的時候,boltdb 並不會立即釋放這個 page,而是把這個 page 加入到空閒列表頁中,等到需要新的 page 的時候,再從空閒列表頁中取出一個 page。
在 page 之後會儲存對用的結構,比如 meta 或者 freelist。先讀取 page 判斷自己的結構(定長的:8 + 2 + 2 +4),然後再根據不同的資料型別讀取其他的結構(比如BranchPage)。
BranchPage && LeafPage
這兩個分別儲存 B+ tree 的分支頁和葉子頁。對應結構為:
// branchPageElement represents a node on a branch page.
type branchPageElement struct {
pos uint32 // 真實資料對應的偏移量
ksize uint32 // key 的大小
pgid Pgid // 指向 page 的 id
}
// leafPageElement represents a node on a leaf page.
type leafPageElement struct {
flags uint32 // 是否是一個 bucket
pos uint32 // 真實資料對應的偏移量
ksize uint32 // key 的大小
vsize uint32 // value 的大小
}
對應的儲存方式為:
從 page 中拿取資料:
func (p *Page) LeafPageElements() []leafPageElement {
if p.count == 0 {
return nil
}
// 從 page 的指標 加上 page 的大小,就是第一個元素的地址
data := UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
// 轉換為 slice
elems := unsafe.Slice((*leafPageElement)(data), int(p.count))
return elems
}
func (p *Page) BranchPageElements() []branchPageElement {
if p.count == 0 {
return nil
}
data := UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
elems := unsafe.Slice((*branchPageElement)(data), int(p.count))
return elems
}
MetaPage
type Meta struct {
magic uint32 // boltdb 的魔數
version uint32 // boltdb 的版本
pageSize uint32 // boltdb 的 page 大小 ,該值和作業系統預設的頁大小保持一致
flags uint32
root InBucket // boltdb 的根節點
freelist Pgid // 空閒頁的 id
pgid Pgid // 當前 page 的 id
txid Txid // 當前事務的 id
checksum uint64 // 用作校驗的校驗和
}
它是如何寫到 page 中的和從 page 中讀取的呢?
// 把 meta 寫到 page 中
func (m *Meta) Write(p *Page) {
// 檢查 root bucket 的 pgid 是否有效。
// 如果 root.root 的 pgid 大於或等於 m.pgid,這是不合理的,因為這意味著它引用了一個尚未分配的 pgid。
if m.root.root >= m.pgid {
panic(fmt.Sprintf("root bucket pgid (%d) above high water mark (%d)", m.root.root, m.pgid))
// 檢查 freelist 的 pgid 是否有效。
// 如果 freelist 的 pgid 大於或等於 m.pgid 且 freelist 不是 PgidNoFreelist,
// 這同樣表示它引用了一個尚未分配的 pgid,這是不合理的。
} else if m.freelist >= m.pgid && m.freelist != PgidNoFreelist {
panic(fmt.Sprintf("freelist pgid (%d) above high water mark (%d)", m.freelist, m.pgid))
}
// 指定 pageId 和 page 型別
p.id = Pgid(m.txid % 2)
p.SetFlags(MetaPageFlag)
// 計算校驗和
m.checksum = m.Sum64()
m.Copy(p.Meta())
}
// 從 page 中讀取 meta
func (p *Page) Meta() *Meta {
return (*Meta)(UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))
}
// 把 meta 的資料複製到 page 中
func (m *Meta) Copy(dest *Meta) {
*dest = *m
}
// 計算校驗和
func (m *Meta) Sum64() uint64 {
var h = fnv.New64a()
_, _ = h.Write((*[unsafe.Offsetof(Meta{}.checksum)]byte)(unsafe.Pointer(m))[:])
return h.Sum64()
}
FreelistPage
type freelist struct {
// 表示 freelist 的型別,可能會有不同的策略或實現。
freelistType FreelistType
// 儲存所有已釋放且可供分配的頁面ID。
ids []common.Pgid
// 記錄哪個事務ID分配了特定的頁面ID。
allocs map[common.Pgid]common.Txid
// 記錄即將被釋放的頁面ID及其所屬的事務ID。
pending map[common.Txid]*txPending
// 快速查詢所有空閒和待處理頁面ID的快取。
cache map[common.Pgid]struct{}
// 按連續頁面大小分類的空閒頁面,鍵是連續頁面的大小,值是具有相同大小的起始頁面ID的集合。
freemaps map[uint64]pidSet
// 正向對映,鍵是起始頁面ID,值是其span大小。
forwardMap map[common.Pgid]uint64
// 反向對映,鍵是結束頁面ID,值是其span大小。
backwardMap map[common.Pgid]uint64
// 空閒頁面的計數(基於雜湊圖的版本)。
freePagesCount uint64
// 分配函式,根據提供的事務ID和需求的頁面數量分配頁面。
allocate func(txid common.Txid, n int) common.Pgid
// 返回當前空閒頁面數量的函式。
free_count func() int
// 合併連續空閒頁面的函式。
mergeSpans func(ids common.Pgids)
// 獲取所有空閒頁面ID的函式。
getFreePageIDs func() []common.Pgid
// 讀取一系列頁面ID並初始化 freelist 的函式。
readIDs func(pgids []common.Pgid)
}
// FreelistType 定義了 freelist 後端的型別,用字串表示不同的實現策略。
type FreelistType string
// 未來的開發計劃:
// 1. 預設改為使用 `FreelistMapType`;
// 2. 移除 `FreelistArrayType`,不再公開 `FreelistMapType`,
// 並從 `DB` 和 `Options` 結構體中移除 `FreelistType` 欄位。
const (
// FreelistArrayType 表示 freelist 的後端型別為陣列。
// 這種型別可能適用於需要按順序訪問空閒頁的場景。
FreelistArrayType = FreelistType("array")
// FreelistMapType 表示 freelist 的後端型別為雜湊對映。
// 這種型別提供了更快的查詢速度,適合於頻繁、隨機地訪問空閒頁的情況。
FreelistMapType = FreelistType("hashmap")
)
把 freelist 寫到 page 中:
// write 將空閒和待處理的頁面ID寫入到 freelist 頁面。
// 在程式崩潰的事件中,所有待處理的ID都將變成空閒的,因此這些ID都需要被儲存到磁碟上。
func (f *freelist) write(p *common.Page) error {
// 設定頁頭中的頁型別標識
p.SetFlags(common.FreelistPageFlag)
// 獲取需要儲存的 PageID 數量。
l := f.count()
if l == 0 {
// 沒有 id 需要儲存,直接返回。
p.SetCount(uint16(l))
} else if l < 0xFFFF {
// 如果數量小於 0xFFFF
// 將 id 數量寫入到頁頭中
p.SetCount(uint16(l))
// 計算指標頭
data := common.UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
// 開闢一個 slice 用來儲存 id
ids := unsafe.Slice((*common.Pgid)(data), l)
// 將 id 複製到 page 中
f.copyall(ids)
} else {
// 如果數量大於 0xFFFF ,則需要分多個 page 來儲存
// 先設定本頁的數量為 0xFFFF
p.SetCount(0xFFFF)
// 計算指標頭
data := common.UnsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))
ids := unsafe.Slice((*common.Pgid)(data), l+1)
// 將ID數量儲存在第一個元素中
ids[0] = common.Pgid(l)
// 將剩餘的 id 複製到 page 中
f.copyall(ids[1:])
}
return nil
}
// copyall 將所有空閒的ID和所有待處理的ID複製到一個排序後的列表中。
// f.count 返回目標陣列 dst 需要的最小長度。
func (f *freelist) copyall(dst []common.Pgid) {
// 建立一個切片用於存放待處理的ID,容量預設為待處理ID的數量。
m := make(common.Pgids, 0, f.pending_count())
// 遍歷所有待處理事務,並將它們的ID加入到切片 m 中。
for _, txp := range f.pending {
m = append(m, txp.ids...)
}
// 對切片 m 進行排序。
sort.Sort(m)
// 將已經空閒的ID和剛排序的待處理ID合併到目標切片 dst 中。
common.Mergepgids(dst, f.getFreePageIDs(), m)
}
// Mergepgids 將兩個已排序列表 a 和 b 的並集複製到 dst 中。
// 如果 dst 的長度不足以容納結果,會觸發 panic。
func Mergepgids(dst, a, b Pgids) {
// 檢查目標切片 dst 是否足夠大以容納 a 和 b 的所有元素。
if len(dst) < len(a)+len(b) {
panic(fmt.Errorf("mergepgids bad len %d < %d + %d", len(dst), len(a), len(b)))
}
// 如果其中一個列表為空,則直接將另一個列表複製到 dst 中。
if len(a) == 0 {
copy(dst, b)
return
}
if len(b) == 0 {
copy(dst, a)
return
}
// 初始化一個切片 merged 來儲存最終合併的結果。
merged := dst[:0]
// 確定哪個列表的起始值更小,並將其設為 lead,另一個設為 follow。
lead, follow := a, b
if b[0] < a[0] {
lead, follow = b, a
}
// 迴圈合併,直到 lead 為空。
for len(lead) > 0 {
// 合併 lead 中所有小於 follow[0] 的元素。
n := sort.Search(len(lead), func(i int) bool { return lead[i] > follow[0] })
merged = append(merged, lead[:n]...)
if n >= len(lead) {
break
}
// 交換 lead 和 follow,繼續合併過程。
lead, follow = follow, lead[n:]
}
// 將剩餘的 follow 元素加入到 merged 中。
_ = append(merged, follow...)
}
從 page 中讀取 freelist:
// read 從 freelist 頁面讀取頁面ID。
func (f *freelist) read(p *common.Page) {
// 首先檢查是否為 freelist 頁面,如果不是則丟擲錯誤。
if !p.IsFreelistPage() {
panic(fmt.Sprintf("invalid freelist page: %d, page type is %s", p.Id(), p.Typ()))
}
// 從頁面獲取 freelist 頁面的ID列表。
ids := p.FreelistPageIds()
// 如果獲取的ID列表為空,將 f.ids 設定為 nil,表示沒有空閒頁面。
if len(ids) == 0 {
f.ids = nil
} else {
// 如果ID列表不為空,則建立一個新切片並複製這些ID,以避免直接修改原始頁面資料。
idsCopy := make([]common.Pgid, len(ids))
copy(idsCopy, ids)
// 確保複製的ID列表是排序的。
sort.Sort(common.Pgids(idsCopy))
// 將排序後的ID列表讀入 freelist 結構。
f.readIDs(idsCopy)
}
}
// hashmapReadIDs 讀取輸入的 pgids 並初始化 freelist(基於雜湊對映的版本)。
func (f *freelist) hashmapReadIDs(pgids []common.Pgid) {
// 初始化 freelist。
f.init(pgids)
// 重建頁面快取。
f.reindex()
}
// reindex 基於可用和待處理的空閒列表重建自由快取。
func (f *freelist) reindex() {
// 獲取所有空閒頁面ID。
ids := f.getFreePageIDs()
// 建立一個新的快取對映。
f.cache = make(map[common.Pgid]struct{}, len(ids))
// 將所有空閒ID新增到快取中。
for _, id := range ids {
f.cache[id] = struct{}{}
}
// 將所有待處理的空閒ID也新增到快取中。
for _, txp := range f.pending {
for _, pendingID := range txp.ids {
f.cache[pendingID] = struct{}{}
}
}
}
分配頁:
// hashmapAllocate 根據傳入的事務ID和請求的頁面數量,分配頁面。
func (f *freelist) hashmapAllocate(txid common.Txid, n int) common.Pgid {
if n == 0 {
// 如果請求的頁面數量為0,則直接返回0,表示沒有分配任何頁面。
return 0
}
// 檢查是否存在完全匹配的空閒span。
if bm, ok := f.freemaps[uint64(n)]; ok {
for pid := range bm {
// 移除這個span。
f.delSpan(pid, uint64(n))
// 記錄這個頁面ID被哪個事務分配。
f.allocs[pid] = txid
// 從快取中移除已分配的頁面。
for i := common.Pgid(0); i < common.Pgid(n); i++ {
delete(f.cache, pid+i)
}
return pid
}
}
// 在對映中查詢大於請求大小的更大span。
for size, bm := range f.freemaps {
if size < uint64(n) {
continue
}
for pid := range bm {
// 移除找到的大span。
f.delSpan(pid, size)
// 記錄頁面分配。
f.allocs[pid] = txid
// 計算剩餘的span大小,並新增回 freelist。
remain := size - uint64(n)
f.addSpan(pid+common.Pgid(n), remain)
// 從快取中移除已分配的頁面。
for i := common.Pgid(0); i < common.Pgid(n); i++ {
delete(f.cache, pid+i)
}
return pid
}
}
return 0
}
// delSpan 從 freelist 中刪除一個span。
func (f *freelist) delSpan(start common.Pgid, size uint64) {
// 更新前向和後向對映,移除對應的條目。
delete(f.forwardMap, start)
delete(f.backwardMap, start+common.Pgid(size-1))
// 從 freemaps 中移除span。
delete(f.freemaps[size], start)
if len(f.freemaps[size]) == 0 {
// 如果某個大小的span已經沒有其他項,從 freemaps 中完全移除這個大小。
delete(f.freemaps, size)
}
// 更新空閒頁面計數。
f.freePagesCount -= size
}
// addSpan 向 freelist 中新增一個新的span。
func (f *freelist) addSpan(start common.Pgid, size uint64) {
// 更新前向和後向對映。
f.backwardMap[start-1+common.Pgid(size)] = size
f.forwardMap[start] = size
// 確保 freemaps 中存在對應大小的對映。
if _, ok := f.freemaps[size]; !ok {
f.freemaps[size] = make(map[common.Pgid]struct{})
}
// 新增新的span到 freemaps。
f.freemaps[size][start] = struct{}{}
// 更新空閒頁面計數。
f.freePagesCount += size
}
Node
page 的操作跟多都是基於磁碟設計的,在記憶體中使用這些資料結構並不是很方便。所以 boltdb 會把 page 的資料結構轉換為 node 的資料結構,這樣在記憶體中操作就會方便很多。
type node struct {
bucket *Bucket // bucket 的指標
isLeaf bool // 是否是葉子節點
unbalanced bool // 是否平衡
spilled bool // 是否溢位
key []byte // 該 node 的起始 key
pgid common.Pgid // 該 node 對應的 page id
parent *node // 父節點
children nodes // 子節點
inodes common.Inodes // 儲存鍵值對的結構體陣列
}
type Inode struct {
flags uint32 // 用於 leaf node 是否是一個 bucket (subbucket)
pgid Pgid // 用於 branch node, 子節點的 page id
key []byte // key
value []byte // value
}
type Inodes []Inode
page to node
func (n *node) read(p *common.Page) {
n.pgid = p.Id()
n.isLeaf = p.IsLeafPage()
// 讀取 inodes
n.inodes = common.ReadInodeFromPage(p)
// 儲存第一個鍵,以便在將節點寫入到父節點時能夠找到這個節點。
if len(n.inodes) > 0 {
n.key = n.inodes[0].Key()
common.Assert(len(n.key) > 0, "read: zero-length node key")
} else {
n.key = nil
}
}
func ReadInodeFromPage(p *Page) Inodes {
inodes := make(Inodes, int(p.Count()))
isLeaf := p.IsLeafPage()
for i := 0; i < int(p.Count()); i++ {
inode := &inodes[i]
if isLeaf {
// 轉換為 leafPageElement 結構
elem := p.LeafPageElement(uint16(i))
inode.SetFlags(elem.Flags())
inode.SetKey(elem.Key())
inode.SetValue(elem.Value())
} else {
// 轉換為 branchPageElement 結構
elem := p.BranchPageElement(uint16(i))
inode.SetPgid(elem.Pgid())
inode.SetKey(elem.Key())
}
Assert(len(inode.Key()) > 0, "read: zero-length inode key")
}
return inodes
}
node to page
// write writes the items onto one or more pages.
// The page should have p.id (might be 0 for meta or bucket-inline page) and p.overflow set
// and the rest should be zeroed.
func (n *node) write(p *common.Page) {
common.Assert(p.Count() == 0 && p.Flags() == 0, "node cannot be written into a not empty page")
// Initialize page.
if n.isLeaf {
p.SetFlags(common.LeafPageFlag)
} else {
p.SetFlags(common.BranchPageFlag)
}
if len(n.inodes) >= 0xFFFF {
panic(fmt.Sprintf("inode overflow: %d (pgid=%d)", len(n.inodes), p.Id()))
}
p.SetCount(uint16(len(n.inodes)))
// Stop here if there are no items to write.
if p.Count() == 0 {
return
}
// 將node的inodes寫入page
common.WriteInodeToPage(n.inodes, p)
// DEBUG ONLY: n.dump()
}
func WriteInodeToPage(inodes Inodes, p *Page) uint32 {
// 計算寫入的初始偏移量。
off := unsafe.Sizeof(*p) + p.PageElementSize()*uintptr(len(inodes))
isLeaf := p.IsLeafPage()
for i, item := range inodes {
Assert(len(item.Key()) > 0, "write: zero-length inode key")
// 建立一個足夠大小的切片來存放鍵和值。
sz := len(item.Key()) + len(item.Value())
b := UnsafeByteSlice(unsafe.Pointer(p), off, 0, sz)
off += uintptr(sz)
// Write the page element.
if isLeaf {
elem := p.LeafPageElement(uint16(i))
elem.SetPos(uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem))))
elem.SetFlags(item.Flags())
elem.SetKsize(uint32(len(item.Key())))
elem.SetVsize(uint32(len(item.Value())))
} else {
elem := p.BranchPageElement(uint16(i))
elem.SetPos(uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem))))
elem.SetKsize(uint32(len(item.Key())))
elem.SetPgid(item.Pgid())
Assert(elem.Pgid() != p.Id(), "write: circular dependency occurred")
}
// 將鍵和值資料寫入到頁面的末尾。
l := copy(b, item.Key())
copy(b[l:], item.Value())
}
return uint32(off)
}
Bucket
Bucket 是 boltdb 的上層的資料結構,每個 bucket 都有一個完成的 B+ 樹。將多個 page 聯合起來。
type Bucket struct {
*common.InBucket
tx *Tx // 指向關聯事務的指標,將 bucket 與其事務上下文連線。
buckets map[string]*Bucket // 子 bucket 快取;允許透過名字快速訪問子 bucket。
page *common.Page // 內聯頁面的引用,用於直接儲存少量資料或作為資料節點的入口點。
rootNode *node // 根頁面的已例項化節點,如果 bucket 直接儲存在記憶體中,則此節點將被啟用。
nodes map[common.Pgid]*node // 節點快取,用於快速訪問已載入的頁面節點,避免重複讀取磁碟。
// 設定節點分裂時的填充閾值。預設情況下,bucket 將填充至 50%,
// 但如果你知道你的寫入工作負載主要是追加操作,提高這個比例可能會有用。
//
// 這個設定不會跨事務持久化,因此每個事務都必須設定它。
FillPercent float64
}
type InBucket struct {
root Pgid // bucket 根級頁面的頁面ID。如果 bucket 是內聯的,則此值為 0。
sequence uint64 // 單調遞增的序列號,用於 NextSequence() 函式。
}
Bucket 有可能是 node,也可能是 page。查詢某頁面的鍵值對時,首先檢查 Bucket.nodes 快取是否有對應的 node,如果沒有,再從 page 中查詢。
Bucket.FillPercent 記錄 node 的填充百分比。當 node 的已用空間超過其容量的某個百分比後,節點必須分裂,以減少在 B+ Tree 中插入鍵值對時觸發再平衡的機率。預設值是 50%,僅當大量寫入操作在尾部新增時,增大該值才有幫助。
bucket 儲存方式:
遍歷 cursor
type Cursor struct {
bucket *Bucket
stack []elemRef
}
type elemRef struct {
page *common.Page
node *node
index int
}
cursor 分為三類,定位到某一個元素的位置、在當前位置從前往後找、在當前位置從後往前找。方法為:First、Last、Next、Prev 等。
Seek
如果該鍵存在,它會返回該鍵及其對應的值;如果鍵不存在,它則返回最近的後續鍵。
// Seek 方法使用B樹搜尋將游標移動到給定的鍵並返回它。
// 如果鍵不存在,則使用下一個鍵。如果沒有更多的鍵,返回nil。
// 返回的鍵和值只在事務的生命週期內有效。
func (c *Cursor) Seek(seek []byte) (key []byte, value []byte) {
// 確保資料庫事務沒有關閉
common.Assert(c.bucket.tx.db != nil, "tx closed")
// 呼叫內部的seek方法,獲取鍵和值
k, v, flags := c.seek(seek)
// 檢查是否位於頁面的最後一個元素之後,如果是,則移動到下一個元素。
if ref := &c.stack[len(c.stack)-1]; ref.index >= ref.count() {
k, v, flags = c.next()
}
// 如果k為nil,表示未找到鍵,返回nil。
if k == nil {
return nil, nil
} else if (flags & uint32(common.BucketLeafFlag)) != 0 {
// 如果是葉子節點,返回鍵和nil值。
return k, nil
}
// 返回找到的鍵和值。
return k, v
}
// seek 方法將游標移動到給定的鍵,並返回該鍵。
// 如果鍵不存在,則使用下一個鍵。
func (c *Cursor) seek(seek []byte) (key []byte, value []byte, flags uint32) {
// 從根頁面/節點開始,遍歷到正確的頁面。
c.stack = c.stack[:0]
c.search(seek, c.bucket.RootPage())
// 如果是桶,則返回nil值。
return c.keyValue()
}
search
// search 方法遞迴地對給定的頁面/節點進行二分搜尋,直到找到給定的鍵。
func (c *Cursor) search(key []byte, pgId common.Pgid) {
p, n := c.bucket.pageNode(pgId)
if p != nil && !p.IsBranchPage() && !p.IsLeafPage() {
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 e.isLeaf() {
c.nsearch(key)
return
}
// 如果是節點,繼續在節點內部搜尋。
if n != nil {
c.searchNode(key, n)
return
}
// 如果是頁面,繼續在頁面內部搜尋。
c.searchPage(key, p)
}
func (c *Cursor) searchNode(key []byte, n *node) {
var exact bool
// 使用二分搜尋確定鍵的位置。
index := sort.Search(len(n.inodes), func(i int) bool {
ret := bytes.Compare(n.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
// 遞迴搜尋到下一頁。
c.search(key, n.inodes[index].Pgid())
}
func (c *Cursor) searchPage(key []byte, p *common.Page) {
// 對頁面進行二分搜尋以確定正確的範圍。
inodes := p.BranchPageElements()
var exact bool
index := sort.Search(int(p.Count()), func(i int) bool {
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
// 遞迴搜尋到下一頁。
c.search(key, inodes[index].Pgid())
}
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
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
}
keyValue
func (c *Cursor) keyValue() ([]byte, []byte, uint32) {
ref := &c.stack[len(c.stack)-1]
// 如果索引超出範圍,則返回nil。
if ref.count() == 0 || ref.index >= ref.count() {
return nil, nil, 0
}
// 從node中獲取鍵值對。
if ref.node != nil {
inode := &ref.node.inodes[ref.index]
return inode.Key(), inode.Value(), inode.Flags()
}
// 從 page 中獲取鍵值對。
elem := ref.page.LeafPageElement(uint16(ref.index))
return elem.Key(), elem.Value(), elem.Flags()
}
建立 bucket 如果不存在
// CreateBucketIfNotExists 如果指定的儲存桶不存在,則建立它,並返回一個對它的引用。
// 如果儲存桶名為空或太長,則返回錯誤。
// 儲存桶例項僅在事務的生命週期內有效。
func (b *Bucket) CreateBucketIfNotExists(key []byte) (rb *Bucket, err error) {
// 如果日誌不是被丟棄,記錄建立儲存桶的嘗試。
if lg := b.tx.db.Logger(); lg != discardLogger {
lg.Debugf("Creating bucket if not exist %q", key)
defer func() {
if err != nil {
lg.Errorf("Creating bucket if not exist %q failed: %v", key, err)
} else {
lg.Debugf("Creating bucket if not exist %q successfully", key)
}
}()
}
// 檢查資料庫是否關閉,檢查事務是否可寫,檢查鍵名是否為空。
if b.tx.db == nil {
return nil, errors.ErrTxClosed
} else if !b.tx.writable {
return nil, errors.ErrTxNotWritable
} else if len(key) == 0 {
return nil, errors.ErrBucketNameRequired
}
// 使用克隆的鍵而不是原始鍵,以避免記憶體洩漏。
newKey := cloneBytes(key)
// 檢查鍵是否已存在。
if b.buckets != nil {
if child := b.buckets[string(newKey)]; child != nil {
return child, nil
}
}
// 使用游標尋找正確的位置。
c := b.Cursor()
k, v, flags := c.seek(newKey)
// 如果找到的鍵相同,檢查是否已有相同名字的非儲存桶鍵。
if bytes.Equal(newKey, k) {
if (flags & common.BucketLeafFlag) != 0 {
var child = b.openBucket(v)
if b.buckets != nil {
b.buckets[string(newKey)] = child
}
return child, nil
}
return nil, errors.ErrIncompatibleValue
}
// 建立空的內聯儲存桶。
var bucket = Bucket{
InBucket: &common.InBucket{},
rootNode: &node{isLeaf: true},
FillPercent: DefaultFillPercent,
}
var value = bucket.write()
// 在當前節點上插入鍵、值、標誌。
c.node().put(newKey, newKey, value, 0, common.BucketLeafFlag)
// 如果存在內聯頁面,取消引用它,使得儲存桶被視為常規非內聯儲存桶。
b.page = nil
// 返回新建立的儲存桶。
return b.Bucket(newKey), nil
}
// node方法返回游標當前定位的節點。
func (c *Cursor) node() *node {
// 確保游標棧長度大於0,否則丟擲異常。
common.Assert(len(c.stack) > 0, "accessing a node with a zero-length cursor stack")
// 如果棧頂是葉子節點,直接返回該節點。
if ref := &c.stack[len(c.stack)-1]; ref.node != nil && ref.isLeaf() {
return ref.node
}
// 從根開始,向下遍歷層級結構。
var n = c.stack[0].node
if n == nil {
n = c.bucket.node(c.stack[0].page.Id(), nil)
}
for _, ref := range c.stack[:len(c.stack)-1] {
common.Assert(!n.isLeaf, "expected branch node")
n = n.childAt(ref.index)
}
common.Assert(n.isLeaf, "expected leaf node")
return n
}
// put方法在節點中插入鍵值對。
func (n *node) put(oldKey, newKey, value []byte, pgId common.Pgid, flags uint32) {
// 檢查pgId是否超出限制。
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")
}
// 尋找插入的位置。
index := sort.Search(len(n.inodes), func(i int) bool { return bytes.Compare(n.inodes[i].Key(), oldKey) != -1 })
// 如果沒有找到確切匹配,增加容量並移動節點。
exact := len(n.inodes) > 0 && index < len(n.inodes) && bytes.Equal(n.inodes[index].Key(), oldKey)
if !exact {
n.inodes = append(n.inodes, common.Inode{})
copy(n.inodes[index+1:], n.inodes[index:])
}
// 設定inode的屬性。
inode := &n.inodes[index]
inode.SetFlags(flags)
inode.SetKey(newKey)
inode.SetValue(value)
inode.SetPgid(pgId)
common.Assert(len(inode.Key()) > 0, "put: zero-length inode key")
}
// Bucket方法透過名稱檢索巢狀桶。
// 如果桶不存在,返回nil。
// 返回的桶例項只在事務生命週期內有效。
func (b *Bucket) Bucket(name []byte) *Bucket {
// 如果已有桶快取,則直接返回對應桶。
if b.buckets != nil {
if child := b.buckets[string(name)]; child != nil {
return child
}
}
// 移動游標到鍵位置。
c := b.Cursor()
k, v, flags := c.seek(name)
// 如果鍵不存在或者不是桶標誌,則返回nil。
if !bytes.Equal(name, k) || (flags & common.BucketLeafFlag) == 0 {
return nil
}
// 否則建立並快取桶。
var child = b.openBucket(v)
if b.buckets != nil {
b.buckets[string(name)] = child
}
return child
}
插入 key/value
func (b *Bucket) Put(key []byte, value []byte) (err error) {
if lg := b.tx.db.Logger(); lg != discardLogger {
lg.Debugf("Putting key %q", key)
defer func() {
if err != nil {
lg.Errorf("Putting key %q failed: %v", key, err)
} else {
lg.Debugf("Putting key %q successfully", key)
}
}()
}
if b.tx.db == nil {
return errors.ErrTxClosed
} else if !b.Writable() {
return errors.ErrTxNotWritable
} else if len(key) == 0 {
return errors.ErrKeyRequired
} else if len(key) > MaxKeySize {
return errors.ErrKeyTooLarge
} else if int64(len(value)) > MaxValueSize {
return errors.ErrValueTooLarge
}
newKey := cloneBytes(key)
// 移動游標到鍵位置。
c := b.Cursor()
k, _, flags := c.seek(newKey)
// Return an error if there is an existing key with a bucket value.
if bytes.Equal(newKey, k) && (flags&common.BucketLeafFlag) != 0 {
return errors.ErrIncompatibleValue
}
// gofail: var beforeBucketPut struct{}
c.node().put(newKey, newKey, value, 0, 0)
return nil
}
事務
BoltDB 支援 ACID 事務,並採用了使用讀寫鎖機制,支援多個讀操作與一個寫操作併發執行,讓應用程式可以更簡單的處理複雜操作。每個事務都有一個 txid,其中db.meta.txid 儲存了最大的已提交的寫事務 id。BoltDB 對寫事務和讀事務執行不同的 id 分配策略:
- 讀事務:txid == db.meta.txid;
- 寫事務:txid == db.meta.txid + 1;
- 當寫事務成功提交時,會更新了db.meta.txid為當前寫事務 id。
資料庫初始化時會將頁號為 0 和 1 的兩個頁面設定為meta頁,每個事務會獲得一個txid,並選取txid % 2的meta頁做為該事務的讀取物件,每次寫資料後會交替更新meta頁。當其中一個出現資料校驗不一致時會使用另一個meta頁。
BoltDB 的寫操作都是在記憶體中進行,若事務未 commit 時出錯,不會對資料庫造成影響;若是在 commit 的過程中出錯,BoltDB 寫入檔案的順序也保證了不會造成影響:因為資料會寫在新的 page 中不會覆蓋原來的資料,且此時 meta中的資訊不發生變化。
- 開始一份寫事務時,會複製一份 meta資料;
- 從 rootBucket 開始,遍歷 B+ Tree 查詢資料位置並修改;
- 修改操作完成後會進行事務 commit,此時會將資料寫入新的 page;
- 最後更新meta的資訊。
// Tx 代表資料庫上的一個只讀或讀寫事務。
// 只讀事務可用於檢索鍵值和建立游標。
// 讀寫事務可以建立和刪除桶以及建立和刪除鍵。
//
// 重要:必須在使用完事務後提交或回滾事務。
// 只有當沒有事務在使用頁面時,寫入者才能回收這些頁面。
// 長時間執行的讀事務可能會導致資料庫迅速增長。
type Tx struct {
writable bool // 是否為可寫事務
managed bool // 是否為管理事務
db *DB // 關聯的資料庫例項
meta *common.Meta // 後設資料指標
root Bucket // 根桶
pages map[common.Pgid]*common.Page // 頁面對映
stats TxStats // 事務統計
commitHandlers []func() // 提交處理程式列表
// WriteFlag 指定寫相關方法(如 WriteTo())的標誌。
// Tx 使用指定的標誌開啟資料庫檔案以複製資料。
//
// 預設情況下,此標誌未設定,適合主要在記憶體中的工作負載。
// 對於大於可用 RAM 的資料庫,可以設定為 syscall.O_DIRECT 來避免淘汰頁面快取。
WriteFlag int
}
Begin
// Begin 開始一個新事務。
// 多個只讀事務可以併發使用,但一次只能使用一個寫事務。
// 啟動多個寫事務會導致呼叫阻塞,並序列化直到當前寫事務完成。
//
// 事務不應該彼此依賴。在同一個goroutine中開啟一個讀事務和一個寫事務可能會導致寫入者死鎖,
// 因為資料庫需要定期重新對映自身以應對增長,並且在讀事務開啟的時候無法進行。
//
// 如果需要長時間執行的讀事務(例如,快照事務),你可能想要將DB.InitialMmapSize設定為足夠大的值
// 以避免寫事務的潛在阻塞。
//
// 重要:你必須在完成後關閉只讀事務,否則資料庫將無法回收舊頁面。
func (db *DB) Begin(writable bool) (t *Tx, err error) {
if lg := db.Logger(); lg != discardLogger {
lg.Debugf("Starting a new transaction [writable: %t]", writable)
defer func() {
if err != nil {
lg.Errorf("Starting a new transaction [writable: %t] failed: %v", writable, err)
} else {
lg.Debugf("Starting a new transaction [writable: %t] successfully", writable)
}
}()
}
if writable {
return db.beginRWTx()
}
return db.beginTx()
}
func (db *DB) beginRWTx() (*Tx, error) {
// If the database was opened with Options.ReadOnly, return an error.
if db.readOnly {
return nil, berrors.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, berrors.ErrDatabaseNotOpen
}
// Exit if the database is not correctly mapped.
if db.data == nil {
db.rwlock.Unlock()
return nil, berrors.ErrInvalidMapping
}
// Create a transaction associated with the database.
t := &Tx{writable: true}
t.init(db)
db.rwtx = t
db.freePages()
return t, nil
}
// freePages 釋放與已關閉的只讀事務關聯的任何頁面。
func (db *DB) freePages() {
sort.Sort(txsById(db.txs))
minid := common.Txid(0xFFFFFFFFFFFFFFFF)
if len(db.txs) > 0 {
minid = db.txs[0].meta.Txid()
}
if minid > 0 {
db.freelist.release(minid - 1)
}
for _, t := range db.txs {
db.freelist.releaseRange(minid, t.meta.Txid()-1)
minid = t.meta.Txid() + 1
}
db.freelist.releaseRange(minid, common.Txid(0xFFFFFFFFFFFFFFFF))
}
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()
// Exit if the database is not open yet.
if !db.opened {
db.mmaplock.RUnlock()
db.metalock.Unlock()
return nil, berrors.ErrDatabaseNotOpen
}
// Exit if the database is not correctly mapped.
if db.data == nil {
db.mmaplock.RUnlock()
db.metalock.Unlock()
return nil, berrors.ErrInvalidMapping
}
// 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
}
Commit
// Commit 將所有更改寫入磁碟,更新後設資料頁,並關閉事務。
// 如果磁碟寫入發生錯誤,或者在只讀事務上呼叫Commit,將返回錯誤。
func (tx *Tx) Commit() (err error) {
txId := tx.ID() // 獲取事務ID
lg := tx.db.Logger() // 獲取日誌記錄器
if lg != discardLogger {
lg.Debugf("Committing transaction %d", txId)
defer func() {
if err != nil {
lg.Errorf("Committing transaction failed: %v", err)
} else {
lg.Debugf("Committing transaction %d successfully", txId)
}
}()
}
// 檢查是否為管理事務,不允許提交。
common.Assert(!tx.managed, "managed tx commit not allowed")
if tx.db == nil {
return berrors.ErrTxClosed // 事務已關閉錯誤
} else if !tx.writable {
return berrors.ErrTxNotWritable // 非寫事務錯誤
}
// TODO: 使用向量化I/O寫出髒頁
// 重新平衡刪除後的節點
var startTime = time.Now()
tx.root.rebalance()
if tx.stats.GetRebalance() > 0 {
tx.stats.IncRebalanceTime(time.Since(startTime))
}
opgid := tx.meta.Pgid() // 獲取舊的頁面ID
// 將資料溢位到髒頁
startTime = time.Now()
if err = tx.root.spill(); err != nil {
lg.Errorf("spilling data onto dirty pages failed: %v", err)
tx.rollback()
return err
}
tx.stats.IncSpillTime(time.Since(startTime))
// 釋放舊的根桶
tx.meta.RootBucket().SetRootPage(tx.root.RootPage())
// 釋放舊的自由列表,因為提交會寫出一個新的自由列表
if tx.meta.Freelist() != common.PgidNoFreelist {
tx.db.freelist.free(tx.meta.Txid(), tx.db.page(tx.meta.Freelist()))
}
if !tx.db.NoFreelistSync {
err = tx.commitFreelist()
if err != nil {
lg.Errorf("committing freelist failed: %v", err)
return err
}
} else {
tx.meta.SetFreelist(common.PgidNoFreelist)
}
// 如果高水位標記已上移,則嘗試擴大資料庫
if tx.meta.Pgid() > opgid {
if err = tx.db.grow(int(tx.meta.Pgid()+1) * tx.db.pageSize); err != nil {
lg.Errorf("growing db size failed, pgid: %d, pagesize: %d, error: %v", tx.meta.Pgid(), tx.db.pageSize, err)
tx.rollback()
return err
}
}
// 將髒頁寫入磁碟
startTime = time.Now()
if err = tx.write(); err != nil {
lg.Errorf("writing data failed: %v", err)
tx.rollback()
return err
}
// 如果啟用了嚴格模式,則執行一致性檢查
if tx.db.StrictMode {
ch := tx.Check()
var errs []string
for {
chkErr, ok := <-ch
if !ok {
break
}
errs = append(errs, chkErr.Error())
}
if len(errs) > 0 {
panic("check fail: " + strings.Join(errs, "\n"))
}
}
// 將後設資料寫入磁碟
if err = tx.writeMeta(); err != nil {
lg.Errorf("writeMeta failed: %v", err)
tx.rollback()
return err
}
tx.stats.IncWriteTime(time.Since(startTime))
// 結束事務
tx.close()
// 執行提交處理程式,鎖已經移除
for _, fn := range tx.commitHandlers {
fn()
}
return nil
}
Rollback
// Rollback 關閉事務並忽略所有之前的更新。
// 只讀事務必須回滾而不是提交。
func (tx *Tx) Rollback() error {
common.Assert(!tx.managed, "managed tx rollback not allowed") // 斷言此事務不是管理型事務
if tx.db == nil {
return berrors.ErrTxClosed // 如果資料庫已關閉,返回錯誤
}
tx.nonPhysicalRollback() // 執行非物理性回滾
return nil
}
// nonPhysicalRollback 在使用者直接呼叫Rollback時被呼叫,在這種情況下,我們不需要從磁碟重新載入自由頁面。
func (tx *Tx) nonPhysicalRollback() {
if tx.db == nil {
return // 如果資料庫已關閉,直接返回
}
if tx.writable {
tx.db.freelist.rollback(tx.meta.Txid()) // 如果事務是可寫的,回滾自由列表
}
tx.close() // 關閉事務
}
// rollback 從給定的掛起事務中移除頁面。
func (f *freelist) rollback(txid common.Txid) {
// 從快取中移除頁面ID。
txp := f.pending[txid]
if txp == nil {
return // 如果沒有掛起的事務,直接返回
}
var m common.Pgids
for i, pgid := range txp.ids {
delete(f.cache, pgid) // 從快取中刪除頁面ID
tx := txp.alloctx[i]
if tx == 0 {
continue // 如果未分配事務ID,繼續下一個
}
if tx != txid {
// 如果待釋放頁面被中斷,恢復頁面回分配列表。
f.allocs[pgid] = tx
} else {
// 如果釋放的頁面由此事務分配,可以安全地丟棄。
m = append(m, pgid)
}
}
// 從掛起列表中移除頁面,並將其標記為由txid分配的自由頁面。
delete(f.pending, txid)
f.mergeSpans(m)
}
View && Update
// View 在管理的只讀事務上下文中執行一個函式。
// 從函式返回的任何錯誤都會從View()方法返回。
//
// 嘗試在函式內手動回滾會導致panic。
func (db *DB) View(fn func(*Tx) error) error {
t, err := db.Begin(false) // 開始一個只讀事務
if err != nil {
return err // 如果無法開始事務,返回錯誤
}
// 確保在發生panic的情況下事務能夠回滾。
defer func() {
if t.db != nil {
t.rollback() // 執行回滾操作
}
}()
// 標記為管理的事務,以便內部函式不能手動回滾。
t.managed = true
// 如果函式返回錯誤,則傳遞該錯誤。
err = fn(t)
t.managed = false
if err != nil {
_ = t.Rollback() // 執行回滾
return err
}
return t.Rollback() // 完成後回滾事務
}
// Update 在讀寫管理事務的上下文中執行一個函式。
// 如果函式沒有返回錯誤,則提交事務。
// 如果返回了錯誤,則整個事務被回滾。
// 從函式返回的任何錯誤或從提交返回的錯誤都會從Update()方法返回。
//
// 嘗試在函式內手動提交或回滾將導致panic。
func (db *DB) Update(fn func(*Tx) error) error {
t, err := db.Begin(true) // 開始一個讀寫事務
if err != nil {
return err // 如果無法開始事務,返回錯誤
}
// 確保在發生panic的情況下事務能夠回滾。
defer func() {
if t.db != nil {
t.rollback() // 執行回滾操作
}
}()
// 標記為管理的事務,以便內部函式不能手動提交。
t.managed = true
// 如果函式返回錯誤,則回滾並返回錯誤。
err = fn(t)
t.managed = false
if err != nil {
_ = t.Rollback() // 執行回滾
return err
}
return t.Commit() // 無錯誤時提交事務
}
Reference
- https://wingsxdu.com/posts/database/boltdb/
- https://jaydenwen123.github.io/boltdb/
- https://youjiali1995.github.io/storage/boltdb/
- https://www.cnblogs.com/huxiao-tee/p/4660352.html