資料讀寫流程
在bitcast論文中,想要獲取記憶體中儲存的資料,我們首先得獲取索引資料,在索引資料中獲取到檔案id以及資料儲存所在位置,然後根據這些資訊去讀取檔案內容。
所有我們在進行寫資料時也得有兩步,第一步將key value資訊持久化到檔案中,第二部是將索引資訊儲存到記憶體中。
流程如下圖所示
資料結構
根據bitcast論文,我們定義一下所需的資料結構資訊,option中寫入了一些系統的配置資訊,例如資料庫檔案儲存位置以及單文資料庫檔案儲存大小等等。
// Db bitcask例項 面向使用者的介面
type Db struct {
// 系統配置
option option
// 鎖
lock *sync.RWMutex
// 活動檔案
activeFile *data.FileData
// 老檔案列表 只允許讀 不允許寫
oldFile map[uint32]*data.FileData
// 記憶體中儲存的索引資訊
index index.Indexer
}
type option struct {
// 檔案儲存目錄
DirPath string
// 單資料檔案大小閾值
FileDataSize int64
}
Put
put的業務邏輯和上面的流程圖一樣先構建logRecord,然後序列化並追加到硬碟中,最後將索引資料
// Put 新增kv
func (db *Db) Put(key []byte, value []byte) error {
// 判斷key是否合法
if len(key) == 0 {
return errors.New("key為空")
}
// 構建logRecord
logRecord := &data.LogRecord{
Key: key,
Value: value,
Type: data.Normal,
}
// 向檔案追加資料
logRecordPos, err := db.appendLogRecord(logRecord)
if err != nil {
return errors.New("檔案追加失敗")
}
// 將追加的索引新增記憶體中
db.index.Put(key, logRecordPos)
return nil
}
在我們的資料檔案大小有個閾值,如果閾值超過指定大小閾值則將檔案轉換成舊的檔案並新建一個活動檔案進行讀寫,舊的檔案只執行讀不允許寫。之後將資料序列化並追加到活動檔案中,追加完成後將當前資料索引資訊進行返回以便於
// 將KV資料追加到檔案中
func (db *Db) appendLogRecord(logRecord *data.LogRecord) (*data.LogRecordPos, error) {
db.lock.Lock()
defer db.lock.Unlock()
// 如果當前活躍檔案為空 則建立當前活躍檔案
if db.activeFile == nil {
if err := db.setActiveFile(); err != nil {
return nil, err
}
}
// 判斷檔案是否到達閾值 如果到達閾值則將舊的資料檔案歸檔,建立新的資料檔案
if db.activeFile.WriteOffset >= db.option.FileDataSize {
db.oldFile[db.activeFile.FileId] = db.activeFile
if err := db.setActiveFile(); err != nil {
return nil, err
}
}
// 將記錄物件序列化為二進位制位元組陣列
encodingData, _ := data.EncodingLogRecord(logRecord)
offset := db.activeFile.WriteOffset
// 寫入到檔案中
err := db.activeFile.Write(encodingData)
if err != nil {
return nil, err
}
return &data.LogRecordPos{
FileId: db.activeFile.FileId,
Pos: offset,
}, nil
}
整個資料分為資料頭和資料體,如下圖所示:
首先先構建資料頭,資料頭分為四部分
- crc: 用於校驗資料完整性,這個需要將除crc外的資料物件都構建出來才能進行設定。
- type: 表示資料是否被刪除,如果是刪除狀態的話則會在合併流程將該值移除調。
- keySize: key大小
- valueSize: value大小
根據key value構建資料頭之後,根據位元組數構建資料體,最後計算crc校驗和後進行返回。
// EncodingLogRecord 將record物件例項化為位元組陣列並返回長度以及序列化後的物件結果
func EncodingLogRecord(logRecord *LogRecord) ([]byte, int64) {
header := make([]byte, 15)
// 前3個位元組為crc冗餘校驗位,該位等整個LogRecord讀取出來才能進行計算,所以需要先跳過前三個位元組,從第四個位元組開始設定
var index = 4
header[index] = logRecord.Type
index++
keySize := len(logRecord.Key)
valueSize := len(logRecord.Value)
// 寫入位元組數值到header中 PutVarint會返回每次寫入位元組數 因為keySize和valueSize不是定長的,所以需要這樣設定一些
index += binary.PutVarint(header[index:], int64(keySize))
index += binary.PutVarint(header[index:], int64(valueSize))
// 計算logRecord長度 header長度 + key長度 + value長度
var size = int64(index + keySize + valueSize)
logRecordByteArray := make([]byte, size)
// 將header資料複製到logRecordByteArray中
copy(logRecordByteArray[:index], header[:index])
// 將key value設定到位元組陣列中 因為key value儲存的就是位元組陣列所以不需要編解碼 直接設定即可
copy(logRecordByteArray[index:], logRecord.Key)
copy(logRecordByteArray[index+keySize:], logRecord.Value)
// crc校驗和
crcResult := crc32.ChecksumIEEE(logRecordByteArray[4:])
binary.LittleEndian.PutUint32(logRecordByteArray[:4], crcResult)
return logRecordByteArray, size
}