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 (重点) (操作) ...
随机推荐
- 续python学习(一)
接上面没写完的知识点继写. 当然,这些知识点都很简单,可能没必要花费太多时间去记忆,多写写代码就会了. 5.字符串的使用.索引和切片是字符串主要的两个应用.索引:顾名思义就是找出某个字符在一个字符串中 ...
- python3编写程序,根据输入的行列数值,生成相应的矩阵(其中元素为随机数)。
代码如下: import random n = int(input("请输入行:")) m = int(input("请输入列:")) x = y = 0 wh ...
- 回想笔记 瞎比比 域名注册 解析绑定ip 下载证书 设置证书 重定向http到https请求
2019.7.27 回想笔记 拥有腾讯云服务器一台 阿里云注册5元域名,进行备案 完成之后 使用解析 绑定服务器ip地址 ,使用域名可以访问到web服务器而不是通过直接暴露ip地址进行访问 证书购买 ...
- 【Spring Data 系列学习】Spring Data JPA @Query 注解查询
[Spring Data 系列学习]Spring Data JPA @Query 注解查询 前面的章节讲述了 Spring Data Jpa 通过声明式对数据库进行操作,上手速度快简单易操作.但同时 ...
- Simulink仿真入门到精通(一) Simulink界面介绍
Simulink提供了一个动态系统建模.仿真和综合分析的集成环境,是MATLAB最重要的组件之一. 以模块为功能单位,通过信号线进行连接 通过GUI调配每个模块的参数 仿真结果以数值和图像等形象化方式 ...
- Linux启动nginx时报错nginx: [emerg] getpwnam("nginx") failed
编译时指定了用户而没有创建用户导致报错 解决: 查看你添加的用户是什么, [root@localhost nginx]# sbin/nginx -Vnginx version: nginx/1.10. ...
- Unity 随机数与随机种子
随机数几乎应用于游戏开发的方方面面,例如,随机生成的地图,迷宫,怪物属性等,在Unity中,使用随机数非常方便: // // 摘要: // Return a random integer number ...
- Hadoop集群搭建(四)~centos6.8关闭防火墙
一.centos关闭防火墙 1,关闭防火墙.service iptables stop 2,关闭防火墙开机自启.chkconfig iptables off 3,查看防火墙状态.service ipt ...
- Spring注解 - 生命周期、属性赋值、自动装配
一.Bean的生命周期 流程 Bean创建 -- 初始化 -- 销毁 创建: 单实例:在容器启动时创建对象 多实例:每次调用时创建对象 初始化: 都是在对象创建完成后,调用初始化方法 销毁: 单实例: ...
- 我用STM32MP1做了个疫情监控平台3—疫情监控平台实现
目录 1.前言 2.数据接口的获取 3.Qt界面的实现 4.在开发板上运行Qt程序 5.使用无线模块联网 6.代码下载 @ 1.前言 之前我使用桌面版本Qt实现了肺炎疫情监控平台:基于Qt的新冠肺炎疫 ...