写在前面
本文实现的Godis代码版本为:v0.1
Redis持久化方式
RDB持久化
BGSAVE和SAVE命令生成RDB文件,存储数据库信息。当服务器启动,RDB文件也会作为原始数据,加载近服务内存。这里存在一个优先级问题——当AOF持久化是打开状态,优先从AOF文件加载数据、还原数据库状态。
SAVE命令会阻塞服务,而BGSAVE派生独立进程,不会阻塞。同时可以通过选项配置自动执行RDB持久化的周期。
Redis服务端通过记录几个参数(如第一篇提到的server.dirty字段记录了上一次SAVE后经历了多少次数据库修改)维护数据库的修改情况。当周期性的后台操作serverCon执行时,会检查数据库的更新状态是否满足RDB持久化条件,依此保存数据库状态。
注意:RDB的文件对数据库数据的存储,采用的方式是存储键值对。
AOF持久化
前文提到RDB文件保存的是数据本身,而AOF文件存储的是执行的命令转成的协议。可以通过开启Redis的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
命令)的实现处,添加对server.dirty的更新。
func SetCommand(c *Client, s *Server) {
···
s.Dirty++
···
}
我们来测试下效果,重新编译godis-server.go,并执行set alpha 123
:
已经成功在文件中写入了命令协议。
服务启动加载数据
持久化数据从文件加载进内存的方式是模拟客户端执行命令,逐条将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
,查看是否能获取之前set的值:
查验AOF文件,没有读命令get相关的记录。
本篇问题
伪客户端执行时不要执行持久化操作,对Client结构增加伪终端标志位,用于持久化判断:
func LoadData() {
c := godis.CreateClient()
c.FakeFlag = true
···
}
小结
没有下集预告了。
这五篇短文的初衷是记录在学习GO时写的小demo,结果花在其他语言之前的精力超出了预算,所以在那以后也没有再继续开发Godis的新feature。
明天在公司的工作迎来忙碌的项目改造期,趁着今天端午小长假最后一天,了解了V0.1版本。
不过,在不久的将来,也许下个小长假,会将前文提到过的key过期、网络优化、API开发、Stream等新的feature和优化公之于众。
欢迎讨论。
有疑问加站长微信联系(非本文作者)