本章需要解决数据持久化的问题。
前两章的数据都是储存在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(较为复杂,需要较长时间)。