写在前面

本文实现的Godis代码版本为:v0.1

Redis持久化方式

RDB持久化

BGSAVE和SAVE命令生成RDB文件,存储数据库信息。当服务器启动,RDB文件也会作为原始数据,加载近服务内存。这里存在一个优先级问题——当AOF持久化是打开状态,优先从AOF文件加载数据、还原数据库状态。

SAVE命令会阻塞服务,而BGSAVE派生独立进程,不会阻塞。同时可以通过选项配置自动执行RDB持久化的周期。

Redis服务端通过记录几个参数(如第一篇提到的server.dirty字段记录了上一次SAVE后经历了多少次数据库修改)维护数据库的修改情况。当周期性的后台操作serverCon执行时,会检查数据库的更新状态是否满足RDB持久化条件,依此保存数据库状态。
注意:RDB的文件对数据库数据的存储,采用的方式是存储键值对。

AOF持久化

appendonly.aof

如果AOF持久化对每次修改命令都计入文件,会多记录一些无效命令。如:

set alpha 123
set alpha 1
set alpha 321
set alpha 123

四条命令是过程,数据库记录的最终值123才是过程的最终结果。
为了避免对同一个key的操作的“无效命令”的记录,Redis有AOF重写机制——读取当前数据状态作为AOF文件要追加的命令记录。

Godis实现AOF持久化

Godis只实现AOF持久化,并且不对命令进行重写归并操作,所有修改操作都会记录进AOF文件。这也意味着,在数据保存阶段,会有很多无效I/O操作;加载阶段,会有很多无效的命令被执行。

数据持久化到磁盘

在Godis的编码中没有使用Redis类似的事件循环,我们在此依赖server.dirty字段作为标识。dirty变化即为持久化的时机。

首先,在命令调用处添加AOF持久化判断,如果dirty变化,则进行持久化:

func call(c *Client, s *Server) {
    dirty := s.Dirty
    c.Cmd.Proc(c, s)
    dirty = s.Dirty - dirty
    if dirty > 0 {//dirty变化 进行持久化
        AppendToFile(s.AofFilename, c.QueryBuf)
    }

}

执行持久化操作的函数AppendToFile也很简单,对文件追加写,并且即刻关闭:

func AppendToFile(fileName string, content string) error {
    // 以只写的模式,打开文件
    f, err := os.OpenFile(fileName, os.O_WRONLY|syscall.O_CREAT, 0644)
    if err != nil {
        log.Println("log file open failed" + err.Error())
    } else {
        n, _ := f.Seek(0, os.SEEK_END)
        _, err = f.WriteAt([]byte(content), n)
    }
    defer f.Close()
    return err
}
set
func SetCommand(c *Client, s *Server) {
    ···
    s.Dirty++
    ···
}
set alpha 123

clipboard.png

已经成功在文件中写入了命令协议。

服务启动加载数据

持久化数据从文件加载进内存的方式是模拟客户端执行命令,逐条将AOF文件命令发送给服务端。

func LoadData() {
    c := godis.CreateClient()
    pros := core.ReadAof(godis.AofFilename)
    for _, v := range pros {
        c.QueryBuf = string(v)
        err := c.ProcessInputBuffer()
        if err != nil {
            log.Println("ProcessInputBuffer err", err)
        }
        godis.ProcessCommand(c)
    }
}

core.ReadAof将AOF文件读入内存,分条存储。而后的ProcessCommand在set/get命令实现处有介绍,不再说明。

集成测试

get alpha

clipboard.png

查验AOF文件,没有读命令get相关的记录。

本篇问题

伪客户端执行时不要执行持久化操作,对Client结构增加伪终端标志位,用于持久化判断:

func LoadData() {
    c := godis.CreateClient()
    c.FakeFlag = true
    ···
}

小结

没有下集预告了。
这五篇短文的初衷是记录在学习GO时写的小demo,结果花在其他语言之前的精力超出了预算,所以在那以后也没有再继续开发Godis的新feature。

明天在公司的工作迎来忙碌的项目改造期,趁着今天端午小长假最后一天,了解了V0.1版本。
不过,在不久的将来,也许下个小长假,会将前文提到过的key过期、网络优化、API开发、Stream等新的feature和优化公之于众。
欢迎讨论。