1、創建事務
事務分爲讀寫事務和讀事務,讀寫事務不能併發,讀事務可以併發。
func (db *DB) Begin(writable bool) (*Tx, error) {
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, ErrDatabaseReadOnly
}
db.rwlock.Lock()
// 鎖定meat就可以保證只有本tx可以修改meta
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
// 找到目前被使用過的最小txid
var minid txid = 0xFFFFFFFFFFFFFFFF
for _, t := range db.txs {
if t.meta.txid < minid {
minid = t.meta.txid
}
}
//暫存在pending中的page小於最小txid的都釋放到free中
if minid > 0 {
db.freelist.release(minid - 1)
}
return t, nil
}
func (tx *Tx) init(db *DB) {
tx.db = db
tx.pages = nil
// Copy the meta page since it can be changed by the writer.
tx.meta = &meta{}
db.meta().copy(tx.meta)//對db中的mate做個快照
// Copy over the root bucket.
tx.root = newBucket(tx) //創建root bucket
tx.root.bucket = &bucket{}
*tx.root.bucket = tx.meta.root//db-mete做快照
// Increment the transaction id and add a page cache for writable transactions.
if tx.writable {
//讀寫事務需要創建pages記錄變化
tx.pages = make(map[pgid]*page)
tx.meta.txid += txid(1) //txid遞增
}
}
創建事務就是把db中的元數據賦值一份到tx中,這些元數據寫的過程中會變化,爲了保護之前的數據一致性不被破壞,這裏需要拷貝一份新數據,事務提交之後使用tx中的數據在把db中數據覆蓋一遍。
2、事務提交
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
}
var startTime = time.Now()
//對於前面介紹刪除的節點,會導致有點節點不滿足b+tree的條件,對於這樣的節點就需要進行節點合併等操作。後面詳細介紹
tx.root.rebalance()
if tx.stats.Rebalance > 0 {
tx.stats.RebalanceTime += time.Since(startTime)
}
// spill data onto dirty pages.
startTime = time.Now()
//當事務內有插入操作時,會導致節點變大,爲了讓節點滿足b+tree的規則需要對節點進行拆分,後面詳細介紹
if err := tx.root.spill(); err != nil {
tx.rollback()
return err
}
tx.stats.SpillTime += time.Since(startTime)
// 更新meta
tx.meta.root.root = tx.root.root
//記錄當前分配的page個數
opgid := tx.meta.pgid
// 釋放數據被拷貝到新page上的老page,只是掛到pending中並未真正釋放
tx.db.freelist.free(tx.meta.txid, tx.db.page(tx.meta.freelist))
//計算記錄空閒page所需空間並申請
p, err := tx.allocate((tx.db.freelist.size() / tx.db.pageSize) + 1)
if err != nil {
tx.rollback()
return err
}
//將空閒page記錄到p對應的空間
if err := tx.db.freelist.write(p); err != nil {
tx.rollback()
return err
}
//更新freelist指向新的page位置
tx.meta.freelist = p.id
// 如果超過當前空間,擴大db文件
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()
//將Tx.pages中記錄的髒頁寫入db文件
if err := tx.write(); err != nil {
tx.rollback()
return err
}
// 校驗忽略
if tx.db.StrictMode {
}
// 把meta 寫入磁盤,meta記錄的是根節點
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
}
2.1節點拆分
spill是將節點分割,分割後的節點會使節點數變多,爲了符合b+tree的規則就要對tree做拆分。按照從leaf向root節點的順序依次拆分。
func (b *Bucket) spill() error {
// Spill all child buckets first.
for name, child := range b.buckets {//遍歷所有bucket
var value []byte
if child.inlineable() {//inline類型處理
child.free()
value = child.write()
} else {
//處理該子bucket
if err := child.spill(); err != nil {
return err
}
//該子bucket經過spill會導致該子bucket從roo到lead整條路徑的變化,記錄變化後的root(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
}
// 子bucket也是有父節點的,找到父節點並更新
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
}
//bucket對應的node節點拆分
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))
}
//既然更新了rootNode也必須更新root page對應的id
b.root = b.rootNode.pgid
return nil
}
具體的spill執行還是在node中:
func (n *node) spill() error {
var tx = n.bucket.tx
if n.spilled {
return nil
}
// 從葉子節點開始向上拆分,此處遞歸調用spill
sort.Sort(n.children)
for i := 0; i < len(n.children); i++ {
if err := n.children[i].spill(); err != nil {
return err
}
}
n.children = nil
// 拆分node成一組nodes
var nodes = n.split(tx.db.pageSize)
for _, node := range nodes {//遍歷每一個拆分出來的node
// Add node's page to the freelist if it's not new.
if node.pgid > 0 {//如果有對應老的page,釋放
tx.db.freelist.free(tx.meta.txid, tx.page(node.pgid))
node.pgid = 0
}
// 爲該node分配新的page
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對應page id
node.write(p) //將node寫入page
node.spilled = true//標記該node已處理
// 更新父節點
if node.parent != nil {
var key = node.key
if key == nil {
key = node.inodes[0].key
}
//將拆分出來的每一個node讀拆入其父節點中,葉子節點是插入kv,非葉子節點插入k-pgid
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++
}
// 如果所有node都spill完,並且有新的root被拆分出來,處理root節點的拆分
if n.parent != nil && n.parent.pgid == 0 {
n.children = nil
return n.parent.spill()
}
return nil
}
繼續向下看具體的node節點是如何拆分的:
func (n *node) splitTwo(pageSize int) (*node, *node) {
// 如果不用拆分就返回
if len(n.inodes) <= (minKeysPerPage*2) || n.sizeLessThan(pageSize) {
return n, nil
}
// 確定拆分節點的閥值
var fillPercent = n.bucket.FillPercent
if fillPercent < minFillPercent {
fillPercent = minFillPercent
} else if fillPercent > maxFillPercent {
fillPercent = maxFillPercent
}
threshold := int(float64(pageSize) * fillPercent)
// 確定拆分的index
splitIndex, _ := n.splitIndex(threshold)
// 已經拆分沒有父節點怎麼可以
if n.parent == nil {
n.parent = &node{bucket: n.bucket, children: []*node{n}}
}
// 生成新節點
next := &node{bucket: n.bucket, isLeaf: n.isLeaf, parent: n.parent}
n.parent.children = append(n.parent.children, next)
// inodes 根據定好的index分到兩個節點內
next.inodes = n.inodes[splitIndex:]
n.inodes = n.inodes[:splitIndex]
// Update the statistics.
n.bucket.tx.stats.Split++
return n, next
}
總結下spill,一個原則就是自下而上,層層拆分,Bucket拆分完後,在更新bucket所在的node。其實這是兩棵樹,一顆bucket樹,一顆node樹。看下圖即可明白:
2.2 freelist 管理
free
給定 txid, 釋放 page 及 overflow 所有的頁,將所有 pid 追加到 f.pending[txid] 數組中,並添加到 cache map
// page retrieves a page reference from the mmap based on the current page size.
func (db *DB) page(id pgid) *page {
pos := id * pgid(db.pageSize)
return (*page)(unsafe.Pointer(&db.data[pos]))
}
func (f *freelist) free(txid txid, p *page) {
if p.id <= 1 {
panic(fmt.Sprintf("cannot free page 0 or 1: %d", p.id))
}
// Free page and all its overflow pages.
var ids = f.pending[txid]
for id := p.id; id <= p.id+pgid(p.overflow); id++ {
// Verify that page is not already free.
if f.cache[id] {
panic(fmt.Sprintf("page %d already freed", id))
}
// Add to the freelist and cache.
ids = append(ids, id)
f.cache[id] = true
}
f.pending[txid] = ids
}
release
指定最大 txid 釋放全部無效事務的引用頁,遍歷所有 pending 事務 map,所有事務 id 小於給定 txid 的全部釋放,並追加到 f.ids 數組中,然後排序
func (f *freelist) release(txid txid) {
m := make(pgids, 0)
for tid, ids := range f.pending {
if tid <= txid {
// Move transaction's pending pages to the available freelist.
// Don't remove from the cache since the page is still free.
m = append(m, ids...)
delete(f.pending, tid)
}
}
sort.Sort(m)
f.ids = pgids(f.ids).merge(m)
}
分配 freelist,分配頁時,返回 n 個連續可用頁 的首個 id. 這麼做是有好處的,對這片磁盤的寫都是順序寫,減少隨機寫。但是這裏有個問題,如果一直找不到,那就會大量的從物理空間申請,會有碎片化問題
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 {
panic(fmt.Sprintf("invalid page allocation: %d", id))
}
// Reset initial page if this is not contiguous.
if previd == 0 || id-previd != 1 {
initial = id
}
if (id-initial)+1 == pgid(n) {
if (i + 1) == n {
f.ids = f.ids[i+1:]
} else {
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
}
2.3 髒page刷盤
func (tx *Tx) write() error {
// Sort pages by 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)
// 遍歷所有的髒page
for _, p := range pages {
size := (int(p.overflow) + 1) * tx.db.pageSize
offset := int64(p.id) * int64(tx.db.pageSize)//寫入offset
ptr := (*[maxAllocSize]byte)(unsafe.Pointer(p))
for {
//將p寫入文件偏移offset
sz := size
if sz > maxAllocSize-1 {
sz = maxAllocSize - 1
}
buf := ptr[:sz]
if _, err := tx.db.ops.writeAt(buf, offset); err != nil {
return err
}
tx.stats.Write++
size -= sz
if size == 0 {
break
}
offset += int64(sz)
ptr = (*[maxAllocSize]byte)(unsafe.Pointer(&ptr[sz]))
}
}
// Ignore file sync if flag is set on DB.
if !tx.db.NoSync || IgnoreNoSync {
if err := fdatasync(tx.db); err != nil {
return err
}
}
// 將釋放的page歸還到內存池中
for _, p := range pages {
if int(p.overflow) != 0 {
continue
}
buf := (*[maxAllocSize]byte)(unsafe.Pointer(p))[:tx.db.pageSize] for i := range buf {
buf[i] = 0
}
tx.db.pagePool.Put(buf)
}
return nil
}
2.4 meta刷盤
func (tx *Tx) writeMeta() error {
// Create a temporary buffer for the meta page.
buf := make([]byte, tx.db.pageSize)
p := tx.db.pageInBuffer(buf, 0)
tx.meta.write(p)
// Write the meta page to file.
if _, err := tx.db.ops.writeAt(buf, int64(p.id)*int64(tx.db.pageSize)); err != nil {
return err
}
if !tx.db.NoSync || IgnoreNoSync {
if err := fdatasync(tx.db); err != nil {
return err
}
}
// Update statistics.
tx.stats.Write++
return nil
}
Meta寫入文件比較簡單,不在詳細介紹。
Rebalance 沒有詳細介紹,理解了spill流程Rebalance應該很容易理解。
3、回滾
回滾操作做兩件事,1:加入pending 、cache中的page再刪除掉。2:從ids刪除的空閒page在加回去。數據不需要回滾,因爲有cow可以保證數據一致性
func (tx *Tx) rollback() {
if tx.db == nil {
return
}
if tx.writable {
//從pending 、cache刪除掛起的page
tx.db.freelist.rollback(tx.meta.txid)
//回滾空閒page
tx.db.freelist.reload(tx.db.page(tx.db.meta().freelist))
}
tx.close()
}
func (f *freelist) rollback(txid txid) {
// 把加入cache/pending中的page再刪除掉
for _, id := range f.pending[txid] {
delete(f.cache, id)
}
// Remove pages from pending list.
delete(f.pending, txid)
}
func (f *freelist) reload(p *page) {
f.read(p)
// Build a cache of only pending pages.
pcache := make(map[pgid]bool)
for _, pendingIDs := range f.pending {
for _, pendingID := range pendingIDs {
pcache[pendingID] = true
}
}
// 重新構建ids
var a []pgid
for _, id := range f.ids {
if !pcache[id] {
a = append(a, id)
}
}
f.ids = a
f.reindex()
}