文章目录
redis有两种持久化的方式,一个是RDB一个是AOF。
一、RDB
RDB方式会生成一个RDB文件,该文件时一个经过压缩的二进制文件,可以通过该文件还原数据库状态。
生成RDB文件的命令:
-
SAVE
阻塞命令,直到RDB文件创建完毕,阻塞期间不接收任何命令
-
BGSAVE
- 非阻塞命令,会fork一个子进程进行创建RDB文件,父进程继续处理命令请求
- 执行
BGSAVE
期间,客户端发送的SAVE
和BGSAVE
命令会直接拒绝执行,避免产生竞争条件 - 执行
BGSAVE
期间,客户端发送的BGREWRITEAOF
命令会延迟到BGSAVE
执行完毕后调用 - 执行
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
函数,在该函数中会检查上述条件,判断是否进行保存:
- 遍历
saveparams
数组 - 计算距离上次成功执行保存操作有多少s
- 如果修改次数超过配置次数且时间超过配置的时间,就执行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_INT
或REDIS_ENCODING_RAW
,这两个类型在前文介绍redis对象时详细讲过,可以去看一下。
-
REDIS_ENCODING_INT
保存的是一个不超过32位的整数。其结构为:
———————————————————— |ENCODING | integer| ————————————————————
其中ENCODING可以是
REDIS_ENCODING_INT8
、REDIS_ENCODING_INT16
、REDIS_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持久化的实现分为三个步骤:
- 追加
- 文件写入
- 文件同步
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中的命令,就可以恢复服务器关闭前的数据。
步骤:
- 创建一个不带网络连接的伪客户端
- 从AOF文件中分析并读取一条写命令
- 使用伪客户端执行读出的命令
- 循环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文件,而是直接读取数据库进行重写。
步骤如下:
- 创建新的AOF文件
- 遍历非空数据库
- 写入SELECT命令指定数据库号
- 遍历数据库的所有未过期的键
- 根据键的类型进行重写(如是集合的话,就通过LRANGE命令获取列表键的所有元素,再使用RPUSH重写所有键)
- 如果键带过期时间,则重写过期时间
- 关闭文件
注意:
为了避免执行命令时客户端输入缓冲区的溢出,重写处理列表、哈希表、集合和有序集合时,会检查其元素的数量。
以集合为例,如果集合元素超过64个,这64个使用一条命令保存,后面的再用一个处理64个元素的命令保存,如果还有剩余,就继续,每个命令都只处理64个元素。
2.6后台重写
为避免大量的写入操作造成长时间的阻塞,redis让AOF重写放到子进程进行,以达到如下目的:
- 子进行AOF重写时,父进程可继续处理请求
- 子进程带有服务器进程的数据副本,使用子进程而不是线程,可在避免使用锁情况下,保证数据安全。
子进程重写的问题:
因为子进程重写时父进程可继续执行问题,所以重写完毕后,可能会导致新的命令没有写入,造成数据的不一致性。
解决:
设置AOF重写缓冲区。
即,执行写命令后,会追加到AOF缓冲区和AOF重写缓冲区两个地方。
这样除了可以被正常处理的AOF缓冲区内容外,新加的全部写命令都被放到了AOF重写缓冲区中。
当子进程完成AOF重写后,会给父进程发送一个信号,父进程收到信号后会调用一个信号处理函数执行以下操作:
- 将AOF重写缓冲区所有内容写入到新AOF文件,此时新AOF文件的数据就和服务端的数据一致;
- 将新AOF文件改名,原子的覆盖旧的AOF文件
处理完毕后,父进程继续正常接收新命令。
因此整个AOF后台重写,只有在父进程调用信号处理函数时才会造成短暂阻塞,将性能影响降到最低。
’
三、RBD和AOF启动顺序
- 如果开启AOF,判断是否存在AOF文件
- 如果存在AOF文件,加载AOF文件,加载成功,则启动成功
- 如果不存在AOF文件判断是否存在RDB
- 如果存在RDB,则加载RDB,加载RDB成功,则启动成功,否则启动失败
- 如果不存在RDB,则什么都不做直接启动成功
- 如果未开启AOF,进入4
参考:《Redis设计与实现》