为了使用内存作为数据库的主要存储方式,开发内存数据库,我们需要对内存中的数据进行保证。即可以备份与还原,那么为了将内存中的数据备份到外存中,我们可以采取以下策略:
选取一个外存文件,将其映射到某个内存地址;
当更新内存时,适时地更新外存文件;
系统重启时,从外存中重新读取内存内容。
那么这里就有几个问题,首先是映射问题,起初我尝试了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.")
}