本章需要解决数据持久化的问题。

前两章的数据都是储存在map(内存)里面,重启后数据会丢失,一个完整的数据库应该包括数据的持久化。

传统高级数据库使用b+tree储存数据,硬盘的每个block对应b+tree的Node,有效利用磁盘I/O,通常只需要2-3次I/O即可获取所需的数据,但是b+tree映射在硬盘的技术过于复杂,本章不使用该数据结构,有兴趣的可以了解一下mmap(内存映射磁盘的方法)。

我们主要使用简单的csv来存储key-value的值:格式为key,value。简单方便易于实现,不过效率十分低下,每次更新数据和删除都要遍历整个文件。不过我们志在理解原理,高效的方法以后再另起一章说明。

操作csv的方法很简单,go自带encoding/csv库,但同时会引来问题,并发操作导致多线程操作同一个文件,这里我们使用go的channel来解决:

//main.go
//声明几个channel
func main() {
	postChan := make(chan map[string]string)
	putChan := make(chan []string)
	delChan := make(chan string)
        //to do something  
        cache := router.Cache{Data: make(map[string]string), Mutex: &sync.RWMutex{}}
        storage.Read(&cache)
}

使用select轮询每个channel,解决文件竞争的问题:

//main.go
//get message from channel
go func() {
	for {
		select {
		case addData := <-postChan:
			storage.Write(addData)
		case updateData := <-putChan:
			storage.Update(updateData)
		case key := <-delChan:
			storage.Delete(key)
		}
	}
}()

每次启动程序时重csv加载map:

//storage.go
func Read(cache *router.Cache) error {
	var f *os.File
	var err error
	_, err = os.Stat(file)
	if err == nil {
		f, err = os.OpenFile(file, os.O_APPEND, 0666)
		if err != nil {
			return err
		}
		reader := csv.NewReader(f)
		records, err := reader.ReadAll()
		if err == nil {
			for _, slice := range records {
				if len(slice) == 2 {
					cache.Data[slice[0]] = slice[1]
				}
			}
		}
	} else {
		f, err = os.Create(file)//创建文件
		if err != nil {
			return err
		}
	}
	defer f.Close()
	return nil
}

新增数据,修改之前的post方法:

//router.go
func (cache Cache) Post(r *http.Request, postChan chan<- map[string]string) string {
    //before
    //把数据放入channel
    postChan <- response
}

配合select轮询写入数据:

//storage.go
func Write(data map[string]string) {
	f, err:= os.OpenFile(file, os.O_APPEND, 0666)
	if err != nil {
		return
	}
	w := csv.NewWriter(f)
	for k, v := range data {
		w.Write([]string{k, v})
	}
	w.Flush()
}

详细代码:参见github。

下一章,b+tree和mmap(较为复杂,需要较长时间)。