本文是使用 golang 实现 redis 系列的第四篇文章,将介绍如何使用 golang 实现 Append Only File 持久化及 AOF 文件重写。

本文完整源代码在作者GithubHDT3213/godis

AOF 文件

AOF 持久化是典型的异步任务,主协程(goroutine) 可以使用 channel 将数据发送到异步协程由异步协程执行持久化操作。

在 DB 中定义相关字段:

type DB struct {
// 主线程使用此channel将要持久化的命令发送到异步协程
aofChan chan *reply.MultiBulkReply
// append file 文件描述符
aofFile *os.File
// append file 路径
aofFilename string // aof 重写需要的缓冲区,将在AOF重写一节详细介绍
aofRewriteChan chan *reply.MultiBulkReply
// 在必要的时候使用此字段暂停持久化操作
pausingAof sync.RWMutex
}

在进行持久化时需要注意两个细节:

  1. get 之类的读命令并不需要进行持久化
  2. expire 命令要用等效的 expireat 命令替换。举例说明,10:00 执行 expire a 3600 表示键 a 在 11:00 过期,在 10:30 载入AOF文件时执行 expire a 3600 就成了 11:30 过期与原数据不符。

我们在命令处理方法中返回 AOF 需要的额外信息:

type extra struct {
// 表示该命令是否需要持久化
toPersist bool
// 如上文所述 expire 之类的命令不能直接持久化
// 若 specialAof == nil 则将命令原样持久化,否则持久化 specialAof 中的指令
specialAof []*reply.MultiBulkReply
} type CmdFunc func(db *DB, args [][]byte) (redis.Reply, *extra)

以 SET 命令为例:

func Set(db *DB, args [][]byte) (redis.Reply, *extra) {
//....
var result int
switch policy {
case upsertPolicy:
result = db.Put(key, entity)
case insertPolicy:
result = db.PutIfAbsent(key, entity)
case updatePolicy:
result = db.PutIfExists(key, entity)
}
extra := &extra{toPersist: result > 0} // 若实际写入了数据则toPresist=true, 若因为XX或NX选项没有实际写入数据则toPresist=false
if result > 0 {
if ttl != unlimitedTTL { // 使用了 EX 或 NX 选项
expireTime := time.Now().Add(time.Duration(ttl) * time.Millisecond)
db.Expire(key, expireTime)
// 持久化时使用 set key value 和 pexpireat 命令代替 set key value EX ttl 命令
extra.specialAof = []*reply.MultiBulkReply{
reply.MakeMultiBulkReply([][]byte{
[]byte("SET"),
args[0],
args[1],
}),
makeExpireCmd(key, expireTime),
}
} else {
db.Persist(key) // override ttl
}
}
return &reply.OkReply{}, extra
} var pExpireAtCmd = []byte("PEXPIREAT") func makeExpireCmd(key string, expireAt time.Time) *reply.MultiBulkReply {
args := make([][]byte, 3)
args[0] = pExpireAtCmd
args[1] = []byte(key)
args[2] = []byte(strconv.FormatInt(expireAt.UnixNano()/1e6, 10))
return reply.MakeMultiBulkReply(args)
}

在处理命令的调度方法中将 aof 命令发送到 channel:

func (db *DB) Exec(c redis.Client, args [][]byte) (result redis.Reply) {
// ....
// normal commands
var extra *extra
cmdFunc, ok := router[cmd] // 找到命令对应的处理函数
if !ok {
return reply.MakeErrReply("ERR unknown command '" + cmd + "'")
}
// 使用处理函数执行命令
if len(args) > 1 {
result, extra = cmdFunc(db, args[1:])
} else {
result, extra = cmdFunc(db, [][]byte{})
} // AOF 持久化
if config.Properties.AppendOnly {
if extra != nil && extra.toPersist {
// 写入 specialAof
if extra.specialAof != nil && len(extra.specialAof) > 0 {
for _, r := range extra.specialAof {
db.addAof(r)
}
} else {
// 写入原始命令
r := reply.MakeMultiBulkReply(args)
db.addAof(r)
}
}
}
return
}

在异步协程中写入命令:

func (db *DB) handleAof() {
for cmd := range db.aofChan {
// 异步协程在持久化之前会尝试获取锁,若其他协程持有锁则会暂停持久化操作
// 锁也保证了每次写入完整的一条指令不会格式错误
db.pausingAof.RLock()
if db.aofRewriteChan != nil {
db.aofRewriteChan <- cmd
}
_, err := db.aofFile.Write(cmd.ToBytes())
if err != nil {
logger.Warn(err)
}
db.pausingAof.RUnlock()
}
}

读取过程与协议解析器一节基本相同,不在正文中赘述:loadAof

AOF 重写

若我们对键a赋值100次会在AOF文件中产生100条指令但只有最后一条指令是有效的,为了减少持久化文件的大小需要进行AOF重写以删除无用的指令。

重写必须在固定不变的数据集上进行,不能直接使用内存中的数据。Redis 重写的实现方式是进行 fork 并在子进程中遍历数据库内的数据重新生成AOF文件。由于 golang 不支持 fork 操作,我们只能采用读取AOF文件生成副本的方式来代替fork。

在进行AOF重写操作时需要满足两个要求:

  1. 若 AOF 重写失败或被中断,AOF 文件需保持重写之前的状态不能丢失数据
  2. 进行 AOF 重写期间执行的命令必须保存到新的AOF文件中, 不能丢失

因此我们设计了一套比较复杂的流程:

  1. 暂停AOF写入 -> 更改状态为重写中 -> 准备重写 -> 恢复AOF写入
  2. 在重写过程中,持久化协程在将命令写入文件的同时也将其写入内存中的重写缓存区
  3. 重写协程读取 AOF 文件中的前一部分(重写开始前的数据,不包括读写过程中写入的数据)并重写到临时文件(tmp.aof)中
  4. 暂停AOF写入 -> 将重写缓冲区中的命令写入tmp.aof -> 使用临时文件tmp.aof覆盖AOF文件(使用文件系统的mv命令保证安全)-> 清空重写缓冲区 -> 恢复AOF写入

在不阻塞在线服务的同时进行其它操作是一项必需的能力,AOF重写的思路在解决这类问题时具有重要的参考价值。比如Mysql Online DDL: gh-ost采用了类似的策略保证数据一致。

首先准备开始重写操作:

func (db *DB) startRewrite() (*os.File, int64, error) {
// 暂停AOF写入, 数据会在 db.aofChan 中暂时堆积
db.pausingAof.Lock()
defer db.pausingAof.Unlock() // 创建重写缓冲区
db.aofRewriteChan = make(chan *reply.MultiBulkReply, aofQueueSize) // 读取当前 aof 文件大小, 不读取重写过程中新写入的内容
fileInfo, _ := os.Stat(db.aofFilename)
filesize := fileInfo.Size() // 创建临时文件
file, err := ioutil.TempFile("", "aof")
if err != nil {
logger.Warn("tmp file create failed")
return nil, 0, err
}
return file, filesize, nil
}

在重写过程中,持久化协程进行双写:

func (db *DB) handleAof() {
for cmd := range db.aofChan {
db.pausingAof.RLock()
if db.aofRewriteChan != nil {
// 数据写入重写缓冲区
db.aofRewriteChan <- cmd
}
_, err := db.aofFile.Write(cmd.ToBytes())
if err != nil {
logger.Warn(err)
}
db.pausingAof.RUnlock()
}
}

执行重写:

func (db *DB) aofRewrite() {
file, fileSize, err := db.startRewrite()
if err != nil {
logger.Warn(err)
return
} // load aof file
tmpDB := &DB{
Data: dict.MakeSimple(),
TTLMap: dict.MakeSimple(),
Locker: lock.Make(lockerSize),
interval: 5 * time.Second, aofFilename: db.aofFilename,
}
// 只读取开始重写前 aof 文件的内容
tmpDB.loadAof(int(fileSize)) // rewrite aof file
tmpDB.Data.ForEach(func(key string, raw interface{}) bool {
var cmd *reply.MultiBulkReply
entity, _ := raw.(*DataEntity)
switch val := entity.Data.(type) {
case []byte:
cmd = persistString(key, val)
case *List.LinkedList:
cmd = persistList(key, val)
case *set.Set:
cmd = persistSet(key, val)
case dict.Dict:
cmd = persistHash(key, val)
case *SortedSet.SortedSet:
cmd = persistZSet(key, val) }
if cmd != nil {
_, _ = file.Write(cmd.ToBytes())
}
return true
})
tmpDB.TTLMap.ForEach(func(key string, raw interface{}) bool {
expireTime, _ := raw.(time.Time)
cmd := makeExpireCmd(key, expireTime)
if cmd != nil {
_, _ = file.Write(cmd.ToBytes())
}
return true
}) db.finishRewrite(file)
}

重写完毕后写入缓冲区中的数据并替换正式文件:

func (db *DB) finishRewrite(tmpFile *os.File) {
// 暂停AOF写入
db.pausingAof.Lock()
defer db.pausingAof.Unlock() // 将重写缓冲区内的数据写入临时文件
// 因为handleAof已被暂停,在遍历期间aofRewriteChan中不会有新数据
loop:
for {
select {
case cmd := <-db.aofRewriteChan:
_, err := tmpFile.Write(cmd.ToBytes())
if err != nil {
logger.Warn(err)
}
default:
// 只有 channel 为空时才会进入此分支
break loop
}
}
// 释放重写缓冲区
close(db.aofRewriteChan)
db.aofRewriteChan = nil // 使用临时文件代替aof文件
_ = db.aofFile.Close()
_ = os.Rename(tmpFile.Name(), db.aofFilename) // 重新打开文件描述符以保证正常写入
aofFile, err := os.OpenFile(db.aofFilename, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0600)
if err != nil {
panic(err)
}
db.aofFile = aofFile
}

Golang 实现 Redis(4): AOF 持久化与AOF重写的更多相关文章

  1. redis之RDB持久化与AOF持久化

    Redis是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,而每个非空数据库中又可以包含任意个键值对,为了方便起见,我们将服务器中的非空数据库以及它们的键值对统称为数据库状态. 因为Red ...

  2. <Redis> 入门五 持久化RBD/AOF

    RDB RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘(默认是 dump.rdb). 默认持久化机制,就是将内存中的数据以快照的方式写入二进制文件dump.rbd中. 触发快照的条件 ...

  3. Redis使用RDB持久化和AOF持久化的区别 - 小白之所见

  4. Redis之持久化(RDB AOF)

    Redis 提供了 RDB 和 AOF 两种持久化方案: RDB:生成指定时间间隔内的 Redis 内存中数据快照,是一个二进制文件 dumpr.rdb AOF:记录 Redis 除了查询以外的所有写 ...

  5. redis 系列17 持久化 AOF

    一.概述 除了上篇介绍的RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能.与RDB保存数据库中的键值对来记录数据库状态不同,AOF是通过保存redis服务器 ...

  6. redis 笔记03 RDB 持久化、AOF持久化、事件、客户端

    RDB 持久化 1. RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据. 2. SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器. 3. BGSAVE由子进程执行保 ...

  7. 详解Redis RDB持久化、AOF持久化

    1.持久化 1.1 持久化简介 持久化(Persistence),持久化是将程序数据在持久状态和瞬时状态间转换的机制,即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘). 1.2 red ...

  8. 《面试官之你说我听》:简明的图解Redis RDB持久化、AOF持久化

    欢迎关注文章这一系列,一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇> <实战演练,拒绝996篇> 如果此文对你有帮助.喜欢的话,那就点个赞呗,点个关注 ...

  9. redis基础:redis下载安装与配置,redis数据类型使用,redis常用指令,jedis使用,RDB和AOF持久化

    知识点梳理 课堂讲义 课程计划 1. REDIS 入 门 (了解) (操作)   2. 数据类型 (重点) (操作) (理解) 3. 常用指令   (操作)   4. Jedis (重点) (操作) ...

随机推荐

  1. AI广度优先搜索算法,项目实战北京地图/贪心学院

    广度优先搜索算法详解地铁路线 北京很大,附上地铁图,不要迷路!!! 作为一个程序员,在北京,你很有可能住在回龙观地区,经常从龙泽上地铁,然后畅游北京. 当有一天,你老家的朋友来北京了,希望你能够带她去 ...

  2. Xmind快速入门(基本操作够用了)

    先选择结构--再选择风格 快捷键: 1.tab (产生子主题)2.enter (在下方产生并列主题) shift+enter (在上方产生并列主题)3.Alt+Enter (给某个主题添加标注)4.按 ...

  3. 前端工程师眼中的Docker

    笔者最近在整理 Node.js 操作各数据库的方法,却不料遇到一个很棘手的问题:很多数据库,都需要同时下载 Server 端和 Client 端,并进行相应的配置,着实是麻烦.那有没有方法可以省去这些 ...

  4. 031.核心组件-kubelet

    一 kubelet概述 1.1 kubelet作用 在Kubernetes集群中,在每个Node(又称Minion)上都会启动一个kubelet服务进程.该进程用于处理Master下发到本节点的任务, ...

  5. python pip 更新失败问题

    通过PIP 安装第三方库的时候出现 pip程序版本过旧,用’python -m pip install --upgrade pip‘安装’提示仍然如下 可以用管理员身份运行windows shell  ...

  6. Java第一节课考试

    1 package kaoshi; import java.util.Scanner; public class ScoreInformation { Scanner input=new Scanne ...

  7. Asp.net 的输入框的 Enabled属性 与 ReadOnly属性

    控件不管是设置 Enabled="false" 还是ReadOnly="true",后台都取不到前台的值,值为“空”: 在界面视觉上,Enabled=" ...

  8. 个人项目(Word Count)

    一.Github项目地址 https://github.com/AllForward/GP_Homework/tree/master/个人项目 二.题目叙述 这个项目要求写一个命令行程序,模仿已有wc ...

  9. 快速上手项目远程团队协作--CODING(新手向)

    快速上手项目远程团队协作--CODING(新手向) CODING网址:https://e.coding.net/signin 前言:之前用过github,用过微软的Azure协作(https://az ...

  10. ECharts的使用与总结

    ECharts的使用与总结 一,介绍与需求 1.1,介绍 ECharts商业级数据图表,一个纯Javascript的图表库,可以流畅的运行在PC和移动设备上,兼容当前绝大部分浏览器(IE6/7/8/9 ...