本文是使用 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. 为什么要使用webpack?

    在网页中会引用到哪些常见的静态资源? js (.js  .jsx  .coffee  .ts) css (.css  .less  .sass  .scss scss是sass的plus版) imag ...

  2. seo搜索优化教程13-SEO搜索引擎站点收录

    为了使大家更方便的了解及学习网络营销推广.seo搜索优化,星辉科技强势推出seo搜索优化教程.此为seo教程第13课 想要用户能够在搜索引擎中通过关键词搜索到您的页面信息,首先要做的是让搜索引擎收录您 ...

  3. 波兰政府在继韩国之后也增加了对 Linux 的使用

    导读 前段时间, 韩国政府起草了一项战略,准备采用基于 Linux 的开源操作系统全面取代 Windows 7,以摆脱对其的依赖. 目前,波兰的社会保险公司 ZUS( Zakład Ubezpiecz ...

  4. Vue 学习记录(一)

    Vue 研究了一段时间,要学的东西挺多的. 一.基本指令 1.v-text:2.v-html:3.v-pre;4.v-cloak:5.v-once:6.v-if:7.v-else:8.v-else-i ...

  5. 我在使用DriverManager时发现的问题

    小白今天在连接数据库的时候,心里面突然想起之前有大佬说没有必要添加一条"com.mysql.jdbc.Driver",当时也实验了确实可行,但是这个可是驱动地址,难道是不用添加是根 ...

  6. abp(net core)+easyui+efcore实现仓储管理系统——入库管理之五(四十一)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...

  7. SQLServer——MASTER..spt_values

    常常见到这个表,人家用得天花乱坠的. 自己select一看却莫名其妙的. 如上, 这个表主要用来保存一些枚举值, 据说是从sybase继承过来,许多函数和存储过程可以看到它的身影.也可以叫系统常量表吧 ...

  8. tomcat源码分析01-启动过程概览

    导读:tomcat是一个开源的web服务器,它实现了我们常用的Servlet,JSP,EL等相关规范,因为其性能稳定,开源等因素得到越来越多开发者的青睐,出于学习的目的,我决定研读其源码,并将阶段性成 ...

  9. python基础知识6——函数

    函数:自定义函数:函数的参数:不带参数,普通参数,默认参数,动态参数:返回值return:函数作用域:内置函数高阶函数:map,reduce,filter,sorted:lambda表达式:文件操作: ...

  10. 01背包模板题 hdu2602 Bonecollector

    由于数组的滚动过程中当前值(i,j)的更新需要用到上一层的(i-1,j-wi)的值,所以在更新当前的j之前不能更新上一层的j之前的值,故01背包是从后向前更新的(重量取值是从大到小的). 代码如下: ...