Golang 实现 Redis(4): AOF 持久化与AOF重写
本文是使用 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
}
在进行持久化时需要注意两个细节:
- get 之类的读命令并不需要进行持久化
- 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重写操作时需要满足两个要求:
- 若 AOF 重写失败或被中断,AOF 文件需保持重写之前的状态不能丢失数据
- 进行 AOF 重写期间执行的命令必须保存到新的AOF文件中, 不能丢失
因此我们设计了一套比较复杂的流程:
- 暂停AOF写入 -> 更改状态为重写中 -> 准备重写 -> 恢复AOF写入
- 在重写过程中,持久化协程在将命令写入文件的同时也将其写入内存中的重写缓存区
- 重写协程读取 AOF 文件中的前一部分(重写开始前的数据,不包括读写过程中写入的数据)并重写到临时文件(tmp.aof)中
- 暂停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重写的更多相关文章
- redis之RDB持久化与AOF持久化
		Redis是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,而每个非空数据库中又可以包含任意个键值对,为了方便起见,我们将服务器中的非空数据库以及它们的键值对统称为数据库状态. 因为Red ... 
- <Redis> 入门五 持久化RBD/AOF
		RDB RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘(默认是 dump.rdb). 默认持久化机制,就是将内存中的数据以快照的方式写入二进制文件dump.rbd中. 触发快照的条件 ... 
- Redis使用RDB持久化和AOF持久化的区别 - 小白之所见
- Redis之持久化(RDB AOF)
		Redis 提供了 RDB 和 AOF 两种持久化方案: RDB:生成指定时间间隔内的 Redis 内存中数据快照,是一个二进制文件 dumpr.rdb AOF:记录 Redis 除了查询以外的所有写 ... 
- redis 系列17 持久化 AOF
		一.概述 除了上篇介绍的RDB持久化功能之外,Redis还提供了AOF(Append Only File)持久化功能.与RDB保存数据库中的键值对来记录数据库状态不同,AOF是通过保存redis服务器 ... 
- redis 笔记03    RDB 持久化、AOF持久化、事件、客户端
		RDB 持久化 1. RDB文件用于保存和还原Redis服务器所有数据库中的所有键值对数据. 2. SAVE命令由服务器进程直接执行保存操作,所以该命令会阻塞服务器. 3. BGSAVE由子进程执行保 ... 
- 详解Redis RDB持久化、AOF持久化
		1.持久化 1.1 持久化简介 持久化(Persistence),持久化是将程序数据在持久状态和瞬时状态间转换的机制,即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘). 1.2 red ... 
- 《面试官之你说我听》:简明的图解Redis RDB持久化、AOF持久化
		欢迎关注文章这一系列,一起学习 <提升能力,涨薪可待篇> <面试知识,工作可待篇> <实战演练,拒绝996篇> 如果此文对你有帮助.喜欢的话,那就点个赞呗,点个关注 ... 
- redis基础:redis下载安装与配置,redis数据类型使用,redis常用指令,jedis使用,RDB和AOF持久化
		知识点梳理 课堂讲义 课程计划 1. REDIS 入 门 (了解) (操作) 2. 数据类型 (重点) (操作) (理解) 3. 常用指令 (操作) 4. Jedis (重点) (操作) ... 
随机推荐
- JZOJ 5258. 友好数对 (Standard IO)
			5258. 友好数对 (Standard IO) Time Limits: 1000 ms Memory Limits: 524288 KB Detailed Limits Description I ... 
- Java堆内存是线程共享的!面试官:你确定吗?
			Java作为一种面向对象的,跨平台语言,其对象.内存等一直是比较难的知识点,所以,即使是一个Java的初学者,也一定或多或少的对JVM有一些了解.可以说,关于JVM的相关知识,基本是每个Java开发者 ... 
- 通过mockjs来制作假数据
			需用用到的模块为express和mockjs //导入模块开启服务器模块 const express=require('express') //导入假数据模块 const mockjs=require ... 
- JetBrains 第二轮:再为免费全家桶续命三个月
			昨天分享了如何通过参与JetBrains的解密任务来获取正版全家桶的兑换码.今天 JetBrains 一早继续在Twitter推出第二波任务: 下面,我们就继续来一起参与一下,为我们的正版JetBra ... 
- 【Oracle】RAC的多实例数据迁移至单机的多实例。
			思路:一般的思路可以通过RMAN进行数据的恢复.由于数据库可以停机,因此,这次试用数据泵(expdp,impdp)进行数据 的导入导出. 1.源数据库导出 通过编写导出shell脚本导出数据,如下: ... 
- Spring框架——IOC 容器的创建与使用
			企业级开发框架 Spring Framework 是整个 Spring 生态的基础,各个模块都是基于 Spring Framework 衍生出来的. Spring 的两大核心机制 IOC 控制翻转.A ... 
- 超实用的Flask入门基础教程,新手必备!
			Flask入门基础教程 Flask简介 Flask是一个轻量级的可定制框架,使用Python语言编写,较其他同类型框架更为灵活.轻便.安全且容易上手.它可以很好地结合MVC模式进行开发,开发人员分工合 ... 
- .NET Core技术研究-HttpContext访问的正确姿势
			将ASP.NET升级到ASP.NET Core之后,相信大家都会遇到HttpContext.Current无法使用的问题.这也是我们迁移ASP.NET Core必须解决的问题. 本文我们详细讨论一下, ... 
- Linux下安装Python3.4
			PS:如果本机安装了python2,尽量不要管他,使用python3运行python脚本就好,因为可能有程序依赖目前的python2环境, 比如yum!!!!! 不要动现有的python2环境! 1. ... 
- 物联网 软硬件系统  树莓派  单片机  esp32  小程序  网页 开发  欢迎相互交流学习~
			物联网软硬件开发 知识分享 多年学生项目开发经验 物联网 软硬件系统 树莓派 单片机 esp32 小程序 网页 开发 欢迎相互交流学习~ http://39.105.218.125:9000/ 
