我正在参加「掘金·启航计划」。主要内容是对于校招练手项目的解析,促进自己更好地理解项目,理解redis。
AOF持久化
Append Only File 通过保存Redis服务器所执行的写命令来记录数据库状态。服务器启动时,可以通过载入和执行AOF文件中保存的命令来还原服务器关闭之前的数据库状态。
开启AOF需设置配置:appendonly yes。默认保存的AOF文件名是appendonly.aof。
- 所有写命令会追加到aof_buf
- aof缓冲区根据对应的策略向硬盘同步
- AOF文件越来越大,需要对文件进行重写压缩
- Redis服务器重启后,加载AOF文件数据恢复
实现
命令追加 Append
当AOF持久化功能处于打开状态时,服务器在执行完一个写命令后,会以协议的格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。
AOF命令写入的格式是文本协议格式,具有很好的兼容性。如:客户端向服务器发送SET KEY VALUE,服务器会在执行完SET命令后,将协议内容追加到aof_buf缓冲区的末尾。“3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n”
写入和同步
Redis服务器进程是一个事件循环,循环中的文件事件负责接收客户端的命令请求,并向客户端发送命令回复。
因为服务器在处理文件事件时可能会执行写命令,使一些内容被追加到aof_buf缓冲区里。所以在服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile,考虑是否需要将aof_buf缓冲区的内容写入和保存到AOF文件。
| appendfsync | 默认为everysec |
|---|---|
| always | 将aof_buf缓冲区中所有内容写入,并使用fysnc操作同步到AOF文件 |
| everysec | 将aof_buf缓冲区中所有内容使用write写入AOF文件,如果上次同步AOF文件的时间距离现在超过1s,再次对AOF文件使用fysnc操作进行同步。这个同步由一个线程专门进行 |
| no | aof_buf缓冲区中所有内容使用write写入AOF文件,但不对AOF文件进行同步,何时同步由操作系统决定 |
用户调用write函数,将一些数据写入到文件时,操作系统通常会把写入数据暂时保存在一个页缓冲区里,当缓冲区空间被填满或超过指定时限后,才真正将缓冲区数据写入磁盘。
这样虽然提高效率,但是为写入数据带来安全问题。如计算机的停机会导致保存在内存缓冲区的写入数据丢失。
因此,系统提供了fsync函数,强制让操作系统立即将缓冲区数据写入硬盘,确保数据的安全性。
重写
随着命令不断写入,AOF文件越来越大。引入AOF重写机制,把Redis进程内的数据转换为写命令同步到新AOF文件。而AOF文件重写后可以变小,是因为
- 进程内已经超时的数据不再写入文件
- 重写使用进程内数据直接生成,新的AOF文件只保留最终数据的写入命令
- 多条写命令可以合并为一个
- 执行AOF重写请求。
如果当前进程正在执行AOF重写,请求不执行,并报错。如果正在执行bgsave操作,重写命令阻塞,直到bgsave完成后执行。
- 父进程使用fork操作创建子进程
- 主进程fork操作完成后,继续响应其他命令。所有写入命令写入aof_buf,并根据appendfsync策略同步磁盘。保证原有AOF机制正确性
-
- fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依旧响应命令,因此redis使用AOF重写缓冲区保存这部分新数据,防止新AOF文件生成期间丢失这部分数据。
- 新aof文件写入完成后,子进程发送信号给父进程,父进程把aof重写缓冲区的数据写入新的aof文件
- 新的aof文件替换旧的aof文件,完成aof重写
写时复制技术: copy-on-write 当使用fork操作复制父进程的数据时,先不复制进程的整个地址空间,而是共享数据,父子进程的地址空间指向共同的物理内存页。读取时不影响,当写入时,在对数据进行一次副本拷贝。
实现方式:fork操作后,将父进程所有内存页标记为只读。一旦子进程尝试对父进程的内存页进行修改,会触发异常,陷入内核,内核为尝试修改的内存页进行一次副本拷贝,恢复该页面的可修改权。
载入和数据还原
优先加载AOF持久化文件
- 创建一个没有网络连接的伪客户端,执行AOF文件保存的写命令。
- 从AOF文件中分析并读取一条写命令
- 使用伪客户端执行被读出的写命令
- 循环执行,直到所有写命令被处理完毕。
AOF文件
AOF持久化是典型的异步任务,主协程可以使用channel将数据发送到异步协程,由异步协程进行持久化操作。
type Handler struct {
aofChan chan *payload //该channel将要持久化的命令发送到异步协程
aofFilename string //append file 路径
aofFile *os.File //append file 文件描述符
aofFinished chan struct{} //aof重写时需要的缓冲区
pausingAof sync.RWMutex //必要时使用该字段暂停持久化操作
currentDB int
}
在异步协程中写入命令
func (handler *Handler) handleAof() {
handler.currentDB = 0
for p := range handler.aofChan {
//使用锁 保证每次都会写入一条完整指令
handler.pausingAof.RLock()
//每个客户端都可以选择自己的数据库 payload中保存客户端选择的数据库
//选择的数据库与aof文件中最新的数据库不一致时,写入一条select命令
if p.dbIndex != handler.currentDB {
data := protocol.MakeMultiBulkReply(utils.ToCmdLine("SELECT", strconv.Itoa(p.dbIndex))).ToBytes()
_, err := handler.aofFile.Write(data)
if err != nil {
logger.Warn(err)
continue
}
handler.currentDB = p.dbIndex
}
//写入命令内容
data := protocol.MakeMultiBulkReply(p.cmdLine).ToBytes()
_, err := handler.aofFile.Write(data)
if err != nil {
logger.Warn(err)
}
handler.pausingAof.RUnlock()
}
//关闭过程中主协程会先关闭handler.aofChan,然后使用<-handler.aofFinished等待缓冲区中的命令
//通过handler.aofFinished通过主协程aof缓冲区完毕
handler.aofFinished <- struct{}{}
}
读取时服用协议解析器中的解析器
func (handler *Handler) loadAof(maxBytes int) {
aofChan := handler.aofChan
handler.aofChan = nil
defer func(aofChan chan *payload) {
handler.aofChan = aofChan
}(aofChan)
file, err := os.Open(handler.aofFilename)
if err != nil {
if _, ok := err.(*os.PathError); ok {
return
}
logger.Warn(err)
return
}
defer file.Close()
var reader io.Reader
if maxBytes > 0 {
reader = io.LimitReader(file, int64(maxBytes))
} else {
reader = file
}
ch := parser.ParseStream(reader)
fakeConn := &connection.FakeChan{}
for p := range ch {
if p.Err != nil {
if p.Err == io.EOF {
break
}
logger.Error("parse error:" + p.Err.Error())
continue
}
if p.Data == nil {
logger.Error("empty payload")
continue
}
r, ok := p.Data.(*protocol.MultiBulkReply)
if !ok {
logger.Error("require multi bulk protocal")
continue
}
ret := handler.db.Exec(fakeConn, r.Args)
if protocol.IsErrReply(ret) {
logger.Error("exec err", ret.ToBytes())
}
}
}
AOF重写
由于golang不支持fork操作,因此采用读取aof文件生成副本的方式来代替fork
在进行aof重写操作时需满足
- 若aof重写失败或被中断,aof文件需保持重写之前的状态,不能丢失数据
- 进行aof重写期间执行的命令必须保存在新的aof文件中,不能丢失
大致流程如下
- 暂停AOF写入-》更改状态为重写中-〉准备重写-》恢复aof写入
- 重写写成读取aof文件前一部分,并重写到临时文件
- 暂停aof文件写入-〉将重写过程中产生的新数据写入临时文件,使用临时文件覆盖旧的aof文件,使用文件系统的mv命令确保安全-》恢复aof写入
准备开始重写
- 获得重写文件大小
- fsync进行文件缓冲区数据的同步
- 对于重写操作使用同步锁
- 新建临时文件.aof,打开文件进行读写,并返回os.File
- 返回重写所需的上下文信息
//StartRewrite 准备开始重写
func (handler *Handler) StartRewrite() (*RewriteCtx, error) {
//使用锁 保证每次会写入一条完整的指令
handler.pausingAof.Lock()
defer handler.pausingAof.Unlock()
//调用fsync将缓冲区的数据进行同步,防止aof文件不完整造成错误
err := handler.aofFile.Sync()
if err!=nil{
logger.Warn("fsync failed")
return nil, err
}
//返回文件描述符
fileInfo,_:=os.Stat(handler.aofFilename)
//获得当前aof大小,用来判断哪些数据是aof重写过程中产生
filesize:=fileInfo.Size()
file,err:=ioutil.TempFile("",".aof")
if err!=nil{
logger.Warn("tmp file create failed")
return nil,err
}
return &RewriteCtx{
tmpFile: file,
fileSize: filesize,
dbIdx: handler.currentDB,
},nil
}
开始重写
- 加载需要重写的临时文件,重写的数据库引擎,重写的文件路径,重写的文件大小
-
- loadAof()
-
-
- 根据文件路径打开文件,根据传递的文件大小限制文件尺寸,通过channel读取客户端发来的命令,使用exec执行命令并加载(处理aof重写时的指令
-
- 选择一个db
-
- utils.ToCmdLine()
-
-
- 以Bulk String的格式写入 “SELECT i”以及写入字节(.Write(data))
-
- 遍历aof文件中的命令
-
- EntityToCmd 根据不同的类型选择不同的处理。如dict.Dict->HMSET key xxx
i := 0
hash.ForEach(func(field string, val interface{}) bool {
bytes, _ := val.([]byte)
args[2+i*2] = []byte(field)//0->2 1->4
args[3+i*2] = bytes//0->3 1->5
i++
return true
})
- 处理已过期的写命令
// DoRewrite 重写aof文件
func (handler *Handler) DoRewrite(ctx *RewriteCtx) error {
tmpFile := ctx.tmpFile
// 将重写开始前的数据加载到内存
tmpAof := handler.newRewriteHandler()
tmpAof.loadAof(int(ctx.fileSize))
//将内存中的数据写入临时文件
for i := 0; i < config.Properties.Databases; i++ {
// 选择db
data := protocol.MakeMultiBulkReply(utils.ToCmdLine("SELECT", strconv.Itoa(i))).ToBytes()
_, err := tmpFile.Write(data)
if err != nil {
return err
}
tmpAof.db.ForEach(i, func(key string, entity *database.DataEntity, expiration *time.Time) bool {
cmd := EntityToCmd(key, entity)
if cmd != nil {
tmpFile.Write(cmd.ToBytes())
}
if expiration != nil {
cmd = makeExpireCmd(key, *expiration)
if cmd != nil {
tmpFile.Write(cmd.ToBytes())
}
}
return true
})
}
return nil
}
//aof/rewrite.go
func (handler *Handler) newRewriteHandler() *Handler {
h := &Handler{}
h.aofFilename = handler.aofFilename
h.db = handler.tmpDBMaker()
return h
}
完成重写
- 开启同步锁,此时暂停aof文件的写入
- 打开线上的aof文件,通过seek定位上次重写的位置
- 写入select命令,选中重写时线上aof选择的数据库
- 选中数据库后,可以将重写过程中产生的数据复制到新的aof文件中
- 使用新的aof文件替代旧的aof文件
- 重新打开线上aof文件,写入一条select命令,使aof文件与数据库文件保持一致
func (handler *Handler) FinishRewrite(ctx *RewriteCtx) {
//暂停handlerAof的写入
handler.pausingAof.Lock()
defer handler.pausingAof.Unlock()
tmpFile := ctx.tmpFile
src, err := os.Open(handler.aofFilename)
if err != nil {
logger.Error("open aofFilename failed:" + err.Error())
return
}
defer func() {
src.Close()
}()
_, err = src.Seek(ctx.fileSize, 0)
if err != nil {
logger.Error("seek failed:" + err.Error())
return
}
data := protocol.MakeMultiBulkReply(utils.ToCmdLine("SELECT", strconv.Itoa(ctx.dbIdx))).ToBytes()
_, err = tmpFile.Write(data)
if err != nil {
logger.Error("tmp file rewrite failed:" + err.Error())
return
}
_, err = io.Copy(tmpFile, src)
if err != nil {
logger.Error("copy aof file failed:" + err.Error())
return
}
handler.aofFile.Close()
os.Rename(tmpFile.Name(), handler.aofFilename)
var aofFile *os.File
aofFile, err = os.OpenFile(handler.aofFilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
panic(err)
}
handler.aofFile = aofFile
data = protocol.MakeMultiBulkReply(utils.ToCmdLine("SELECT", strconv.Itoa(handler.currentDB))).ToBytes()
_, err = handler.aofFile.Write(data)
if err != nil {
panic(err)
}
}