Bolt源碼解析(四):事務與數據一致性

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()
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章