爲了使用內存作爲數據庫的主要存儲方式,開發內存數據庫,我們需要對內存中的數據進行保證。即可以備份與還原,那麼爲了將內存中的數據備份到外存中,我們可以採取以下策略:
選取一個外存文件,將其映射到某個內存地址;
當更新內存時,適時地更新外存文件;
系統重啓時,從外存中重新讀取內存內容。
那麼這裏就有幾個問題,首先是映射問題,起初我嘗試了win32api:
createFileMapping, _ = syscall.GetProcAddress(kernel32, "CreateFileMappingW")
mapViewOfFile, _ = syscall.GetProcAddress(kernel32, "MapViewOfFile")
createFile, _ = syscall.GetProcAddress(kernel32, "CreateFileW")
closeHandle, _ = syscall.GetProcAddress(kernel32, "CloseHandle")
flushViewOfFile, _ = syscall.GetProcAddress(kernel32, "FlushViewOfFile")
unmapViewOfFile, _ = syscall.GetProcAddress(kernel32, "UnmapViewOfFile")
但是實際使用中,發現win32系統爲了性能等方面的考慮,映射文件後,不一定就真正給你開闢了內存空間來訪問,這時訪問會出現異常,windows捕獲異常後纔會再次加載這些文件,導致測試時時好時壞。由此,我決定自己寫一個文件映射的庫。
當創建一個文件映射時,我們使用malloc申請一塊內存,然後創建一個對應大小的文件,並將地址與文件路徑的對應關係存入map中:
var ImageTable = make(map[uintptr]string)
var commonBuffer = make([]byte, 1024 * 1024) // To clear the file quickly
var count = 0 // Windows only support 100ns level
var DataBlockList list.List
// CreateImage creates a image file and returns the address
func CreateImage(size int) (ip *DataBlock, err error) {
defer signalBackup()
filename := common.COMMON_DIR + "\\image\\" + strconv.Itoa(count)
count++
ip = &DataBlock {
RawPtr: uintptr(C.malloc(C.size_t(size))),
Size: size,
}
file, err := os.Create(filename)
defer file.Close()
for i := size;i > 0;i -= 1024 * 1024 {
if i < 1024 * 1024 {
file.Write(commonBuffer[:i])
} else {
file.Write(commonBuffer)
}
}
ImageTable[ip.RawPtr] = filename
DataBlockList.PushBack(ip)
return
}
commonBuffer數組是一個比較大的0數組,爲了快速刷到文件中,而不用每次創建文件都創建一個buffer。count變量的使用,是由於windows系統最多支持到100ns級的時間記錄,爲了讓文件名序列化不受干擾,設置count變量,每次創建鏡像都會自增。同時,該變量和映射表都會被備份在文件中,便於以後的恢復(後文會提及)。DataBlockList是一個DataBlock的鏈表,爲了備份時方便遍歷而設置,DataBlock是爲了封裝Read,Write函數而實現的數據類型。Read,Write函數用於讀寫內存,爲什麼不直接讓使用者讀寫呢,因爲數據庫經常是多個會話同時操作,並行訪問需要對資源加鎖。以下便是DataBlock的實現:
type DataBlock struct {
RawPtr uintptr
Size int
RWMutex sync.RWMutex
}
func (b *DataBlock) read(offset, size int) ([]byte, error) {
if offset + size > b.Size {
return nil, OUT_OF_SIZE
}
var header reflect.SliceHeader
header.Data = uintptr(b.RawPtr + uintptr(offset))
header.Len = size
header.Cap = size
return *(*[]byte)(unsafe.Pointer(&header)), nil
}
func (b *DataBlock) Read(offset, size int) ([]byte, error) {
b.RWMutex.RLock()
defer b.RWMutex.RUnlock()
return b.read(offset, size)
}
func (b *DataBlock) write(offset int, data []byte) (int, error) {
var header reflect.SliceHeader
size := len(data)
header.Data = uintptr(b.RawPtr + uintptr(offset))
header.Len = size
header.Cap = size
d := *(*[]byte)(unsafe.Pointer(&header))
var n int
if offset + size > b.Size {
n = b.Size - offset
} else {
n = size
}
copy(d, data[:n])
return n, nil
}
func (b *DataBlock) Write(offset int, data []byte) (int, error) {
b.RWMutex.Lock()
defer b.RWMutex.Unlock()
var copies *DataBlock
copies, ok := CopyTable[b]
if !ok {
return b.write(offset, data)
}
copies.Write(offset, data)
return b.Write(offset, data)
}
DataBlock結構有一個讀寫鎖做併發控制,允許多個線程同時讀,但不允許寫和任何其他寫或者讀操作同時進行,保證線程安全。同時這段代碼用到了CopyTable,CopyTable是一個記錄正在複製的表,因爲我們申請的空間是固定的,一旦需要擴容,就需要複製操作,而複製是一個很耗時的操作,在此過程中,其他線程可能操作/改變正在複製的數據,所以在write函數中加入複製表的判斷,如果該塊正在被複制,那麼對該塊的操作要同時寫入兩個副本。對CopyTable表的操作見copy函數:
func Copy(dst, src *DataBlock) (int, error) {
CopyTable[src] = dst
data, err := src.Read(0, src.Size)
if err != nil {
return 0, err
}
delete(CopyTable, src)
return dst.Write(0, data)
}
有了Copy函數,重新分配內存的ReallocImage函數也是水到渠成,重新創建一個文件,大小爲新的大小,申請一塊對應的空間,然後建立映射表,刪除原來的映射表:
// ReallocImage creates a new bigger image file and returns the new address with copying data
func ReallocImage(ip *DataBlock, size int) (*DataBlock, error) {
defer signalBackup()
filename := common.COMMON_DIR + "\\image\\" + strconv.Itoa(count)
count++
os.Remove(ImageTable[ip.RawPtr])
ipNew := &DataBlock {
RawPtr: uintptr(C.malloc(C.size_t(size))),
Size: size,
}
file, err := os.Create(filename)
defer file.Close()
if err != nil {
return nil, err
}
for i := size;i > 0;i -= 1024 * 1024 {
if i < 1024 * 1024 {
file.Write(commonBuffer[:i])
} else {
file.Write(commonBuffer)
}
}
Copy(ipNew, ip)
delete(ImageTable, ip.RawPtr)
C.free(unsafe.Pointer(ip.RawPtr))
ImageTable[ipNew.RawPtr] = filename
RemoveBlock(ip)
DataBlockList.PushBack(ipNew)
return ipNew, nil
}
至於ReleaseImage函數,只需要釋放資源,對應修改映射關係即可,接下來說明備份與恢復系統,備份系統由一個單獨的線程控制,該線程在收到備份命令前阻塞,收到信號後開始備份,將映射表和count寫入文件,然後分別寫入每個鏡像:
func signalBackup() {
startBackup <- true
}
func BackupRoutine() {
for {
<- startBackup
SaveImageTable()
SyncAllImageToFile()
close(startBackup)
startBackup = make(chan bool, MAX_BAK_CHAN_SIZE)
}
}
signalBackup函數負責發送一個備份信號,該函數會在用戶進行文件鏡像相關操作時觸發,詳情見上文內存鏡像相關函數。func (b *DataBlock) SyncToFile() error {
data, err := b.Read(0, b.Size)
if err != nil {
return err
}
filename, ok := ImageTable[b.RawPtr]
if !ok {
return NOT_FOUND_ADDRESS
}
log.WriteLog("sys", "Sync " + strconv.Itoa(int(b.RawPtr)) + " to file.")
return ioutil.WriteFile(filename, data, 0666)
}
func SaveImageTable() {
tempTable := make(map[string]string)
for k, v := range ImageTable {
tempTable[strconv.Itoa(int(k))] = v
}
data, _ := json.Marshal(tempTable)
ioutil.WriteFile(common.COMMON_DIR + "\\image\\imageTable.json", data, 0666)
ioutil.WriteFile(common.COMMON_DIR + "\\image\\count", []byte(strconv.Itoa(count)), 0666)
log.WriteLog("sys", "Save image table to file.")
}