Redis设计与实现(七)RDB和AOF持久化及其原理、RDB文件结构


redis有两种持久化的方式,一个是RDB一个是AOF。

一、RDB

RDB方式会生成一个RDB文件,该文件时一个经过压缩的二进制文件,可以通过该文件还原数据库状态。

生成RDB文件的命令:

  • SAVE

    阻塞命令,直到RDB文件创建完毕,阻塞期间不接收任何命令

  • BGSAVE

    1. 非阻塞命令,会fork一个子进程进行创建RDB文件,父进程继续处理命令请求
    2. 执行BGSAVE期间,客户端发送的SAVEBGSAVE命令会直接拒绝执行,避免产生竞争条件
    3. 执行BGSAVE期间,客户端发送的BGREWRITEAOF命令会延迟到BGSAVE执行完毕后调用
    4. 执行BGREWRITEAOF时,客户端发送的BGSAVE会直接被拒绝

    3和4都是基于性能考虑的,避免同时执行大量的磁盘写入。

设置rdb文件保存路径:

config set dir /usr/local

相关配置:

stop-writes-on-bgsave-error yes #bssave出错时是否继续执行写命令
dbfilename dump.rdb # 快照名
dir ./ # 快照文件路径

BGSAVE流程:
在这里插入图片描述

RBD文件会在服务器启动时自动载入,但如果开启了AOF持久化功能,会优先使用AOF文件还原数据库,因为AOF保存的数据更加完整。

所以RDB文件只有在AOF功能关闭时才会载入。

1.1自动保存原理

可在redis配置文件中配置save执行时间:

save 900 1 
save 300 10
save 60 10000

如果没有配置,则上面是默认条件。

900s内对数据库至少修改1次;

300s内对数据库至少修改10次;

60s内对数据库至少修改10000次;

以上配置的条件,满足任意一个,就会执行一次BGSAVE命令。

这些信息都存储在redsiServer结构中:

struct redisServer{
    //...
    //保存条件的数组
    struct saveparam *saveparams;
    
    //修改计数器
    long long dirty;
    //上次执行保存的时间
    time_t lastsave;
}

saveparam:

struct saveparam{
    //秒数
    time_t seconeds;
    //修改次数
    int changes;
    
}

这样就可以将我们的配置保存起来。

dirty用于记录距离上次成功执行SAVE或BGSAVE命令后服务器对数据库进行修改的次数;

如,set msg wml就会让dirty+1,sadd num 1 2 3就会对dirty+3.

redis会每隔100ms执行一次serverCron函数,在该函数中会检查上述条件,判断是否进行保存:

  1. 遍历saveparams数组
  2. 计算距离上次成功执行保存操作有多少s
  3. 如果修改次数超过配置次数且时间超过配置的时间,就执行BGSAVE命令

1.2 RDB文件结构

主要有以下5部分:

  • REDIS

    一个5字节的常量,值为“REDIS”,仅用于判断是否为RDB文件

  • db_version

    4字节的字符串表示的整数,表示RDB文件的版本

  • database

    保存任意多个数据库,如果数据库为空,则该字段为空

  • EOF

    1字节常量,表名RDB文件正文结束,载入只读到此

  • check_sum

    8字节无符号整数,校验和。用于检查文件是否出错或损坏

1.2.1 database
  • SELECTDB

    1字节常量,标识后面要读的是数据库编号

  • db_number

    数据库号,读入后,会调用SELECT命令切换到该数据库

  • key_value_pair

    该库中的所有键值对数据

1.2.1.1 key_value_pair
  • TYPE

    记录值的类型

  • key

    即键。一个字符串对象

  • value

    即值。根据TYPE不同,值的内容和长度也不同。

TYPE取值如下:

REDIS_RDB_TYPE_STRING
REDIS_RDB_TYPE_LIST
REDIS_RDB_TYPE_SET
REDIS_RDB_TYPE_ZSET
REDIS_RDB_TYPE_HASH
REDIS_RDB_TYPE_ZIPLIST
REDIS_RDB_TYPE_SET_INTSET
REDIS_RDB_TYPE_ZSET_ZIPLIST
REDIS_RDB_TYPE_HASH_ZIPLIST

如果带有过期时间,则在TYPE前,还会存储两个字段:

  • EXPIRETIME_MS

    告知后面要读的是以ms为单位过期时间的

  • ms

    8字节长带符号整数,记录ms为单位的时间戳

1.2.2value

value根据TYPE不同,有不同的值对象。

这里就不详细讲了。

列表对象、集合对象、哈希表对象、有序集合对象等,都会在最前面使用一个字段保存该对象的长度,如集合中有3个元素,则该字段就是3,哈希表中有2个键值对,该字段就保存2,可通过该属性知道要载入多少数据;

后面就会依此存储集合或哈希表中的元素,每个元素前都使用一个字段告知该元素的长度。

如列表:有元素 a,haha,hei

其结构就是:

3,1,”a”,4,“haha”,3,”hei”

共3个元素,1表示元素a的长度,4表示haha的长度,3表示hei长度

哈希的话,每个键和值的前面也会保存该键和值的长度。

有序集合的话,会将分数转为字符串存储,然后前面保存该分数字符串的长度。

字符串对象

TYPE值为REDIS_RDB_TYPE_STRING类型时,value就是一个字符串对象。

字符串对象编码可以是REDIS_ENCODING_INTREDIS_ENCODING_RAW,这两个类型在前文介绍redis对象时详细讲过,可以去看一下。

  • REDIS_ENCODING_INT

    保存的是一个不超过32位的整数。其结构为:

    ————————————————————
    |ENCODING | integer|
    ————————————————————
    

    其中ENCODING可以是REDIS_ENCODING_INT8REDIS_ENCODING_INT16REDIS_ENCODING_INT32的其中一个。

  • REDIS_ENCODING_RAW

    保存的是一个字符串。

    如果长度小于等于20字节,则原样保存

    如果大于20字节,则压缩后保存

可在配置文件中的rdbcompression选项配置压缩。

rdbcompression yes # 是否压缩快照

针对未压缩的字符串,其结构为:

len+string,如一个字符串对象hello,其存储的就是:

5 “hello”,len=5,string=”hello”

针对压缩的字符串,其结构为:

  • REDIS_RDB_ENC_LZF

    标识被LZF算法压缩过

  • compressed_len

    压缩后的长度

  • origin_len

    压缩前长度

  • compressred_string

    压缩后的字符串值

二、AOF

通过配置文件可开启AOF持久化功能:

appendonly yes #开启AOF持久化

AOF持久化的实现分为三个步骤:

  1. 追加
  2. 文件写入
  3. 文件同步

2.1追加

服务器每执行完一个写命令,都会将被执行的命令追加到aof_buf缓冲区末尾,该缓冲区也定义在redisServer结构中:

struct redisServer{
    //....
    //AOF缓冲区
    sds aof_buf;
    //...
};

2.2写入和同步

redis服务器进程就是一个事件循环,其中的时间事件负责执行像serverCron函数这样需要定时运行的函数。

服务器每次结束一个事件循环前,都会调用flushAppendOnlyFile函数,考虑是否要将缓冲区中的内容写入到AOF文件中。

而这个和配置文件中的appendfsync选项有关,其取值为:

  • always

    将缓冲区中所有内容都写到AOF文件。

    这种方式最安全,因为每次都进行写入同步,最多只会丢失一个事件循环产生的数据,也因此效率是最慢的。

  • everysec

    将缓冲区所有内容写到AOF文件,每秒进行一次文件同步,同步操作由一个县城专门负责。

    这种方式效率足够快,就算出现故障,最多只丢失1s的数据

  • no

    将缓冲区内容写入AOF文件,但何时同步由操作系统决定。

    速度最快,但出现故障会丢失上次同步AOF文件后的所有写命令数据。

2.3载入与还原

服务器只需读入并重新执行一遍AOF中的命令,就可以恢复服务器关闭前的数据。

步骤:

  1. 创建一个不带网络连接的伪客户端
  2. 从AOF文件中分析并读取一条写命令
  3. 使用伪客户端执行读出的命令
  4. 循环2和3直到将AOF的命令全部执行完

2.4 AOF重写

相关命令:

no-appendfsync-on-rewrite yes #导出 rdb 快照的过程中,要不要停止同步 aof
auto-aof-rewrite-percentage 100 #aof 文件大小比起上次重写时的大小,增长率到100%时,重写
auto-aof-rewrite-min-size 64mb # aof 文件,至少超过 64M 时,重写

因为每执行一个写命令,就会被写入到AOF,所以AOF文件会不断的膨胀,时间久了会占用大量的内存。

如对一个集合执行5次添加,则就会产生5条命令写在AOF文件中,但我们完全可以使用一条命令保存在AOF文件中。

而AOF重写(使用BGREWRITEAOF命令)就是为了完成这样的一个功能,可将多条写命令合并为一个命令,如:

127.0.0.1:6379> sadd list 1
(integer) 1
127.0.0.1:6379> sadd list 2
(integer) 1
127.0.0.1:6379> sadd list 3
(integer) 1

如果不重写,上面会有三条命令写入AOF,如果使用重写,则只保存如下一条命令:

sadd list 1 2 3

在重写时会创建一个新的AOF文件,将重写后的新文件覆盖掉旧文件,以减少内存占用。

2.5 重写的实现

AOF重写不会读取AOF文件,而是直接读取数据库进行重写。

步骤如下:

  1. 创建新的AOF文件
  2. 遍历非空数据库
  3. 写入SELECT命令指定数据库号
  4. 遍历数据库的所有未过期的键
  5. 根据键的类型进行重写(如是集合的话,就通过LRANGE命令获取列表键的所有元素,再使用RPUSH重写所有键)
  6. 如果键带过期时间,则重写过期时间
  7. 关闭文件
注意:

为了避免执行命令时客户端输入缓冲区的溢出,重写处理列表、哈希表、集合和有序集合时,会检查其元素的数量。

以集合为例,如果集合元素超过64个,这64个使用一条命令保存,后面的再用一个处理64个元素的命令保存,如果还有剩余,就继续,每个命令都只处理64个元素。

2.6后台重写

为避免大量的写入操作造成长时间的阻塞,redis让AOF重写放到子进程进行,以达到如下目的:

  1. 子进行AOF重写时,父进程可继续处理请求
  2. 子进程带有服务器进程的数据副本,使用子进程而不是线程,可在避免使用锁情况下,保证数据安全。
子进程重写的问题:

因为子进程重写时父进程可继续执行问题,所以重写完毕后,可能会导致新的命令没有写入,造成数据的不一致性。

解决:

设置AOF重写缓冲区。

即,执行写命令后,会追加到AOF缓冲区和AOF重写缓冲区两个地方。

这样除了可以被正常处理的AOF缓冲区内容外,新加的全部写命令都被放到了AOF重写缓冲区中。

当子进程完成AOF重写后,会给父进程发送一个信号,父进程收到信号后会调用一个信号处理函数执行以下操作:

  1. 将AOF重写缓冲区所有内容写入到新AOF文件,此时新AOF文件的数据就和服务端的数据一致;
  2. 将新AOF文件改名,原子的覆盖旧的AOF文件

处理完毕后,父进程继续正常接收新命令。

因此整个AOF后台重写,只有在父进程调用信号处理函数时才会造成短暂阻塞,将性能影响降到最低。

三、RBD和AOF启动顺序

  1. 如果开启AOF,判断是否存在AOF文件
  2. 如果存在AOF文件,加载AOF文件,加载成功,则启动成功
  3. 如果不存在AOF文件判断是否存在RDB
  4. 如果存在RDB,则加载RDB,加载RDB成功,则启动成功,否则启动失败
  5. 如果不存在RDB,则什么都不做直接启动成功
  6. 如果未开启AOF,进入4

参考:《Redis设计与实现》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值