Redis是一个内存数据库,如果不将数据持久化到磁盘里,在服务器掉电或进程退出后,服务内的数据将会全部丢失。所以,持久化是Redis必不可少的一项功能。
- RDB持久化
RDB全称为Redis Database,使用快照形式将某一时刻内存中数据状态记录下来。这样一来即便是服务宕机,也能够通过RDB文件恢复到快照时的数据状态。
那么如何使用RDB这个功能呢?
通过客户端访问redis服务,使用sava或bgsave命令可以生成快照文件。从命令字面意思就大概能明白,save是同步处理,bgsave是后台处理
接下来我们从源码角度看一下save命令和bgsave的实现
- save
int rdbSave(char *filename){
//创建RDB文件
//写入魔数+RDB版本号
//遍历redis数据库
for(){
while(){
//获取当前数据库所有键值对及对应过期时间
//过期键不写入文件
//保存对象类型、键、值
}
}
// 写入EOF常量,表示rdb内容结束
// 写入check_sum校验和,用来判断文件是否损坏
}
由此可以了解到rdb文件内容的结构
魔数 | rdb版本号 | database | EOF | check_sum |
---|---|---|---|---|
REDIS | 4字节 | 1字节 | 8字节 |
- bgsave
fork()方法执行后会变成两个进程执行,
- 父进程返回fork出的子进程ID
- 子进程返回0
- fork失败返回-1
int rdbSaveBackground(char *filename) {
// 创建子进程
pid = fork();
if (pid == 0) {
//关闭网络通信
rdbSave(filename);
//向父进程发送完成信号
} else {
if(pid == -1) {
//fork失败 报告失败
}
//关闭自动rehash
}
}
生成快照时写命令采用写时复制技术
redis提供自动生成RDB文件。在redis.conf文件中找到这样一段配置,默认是注释掉的。注释也为我们举了例子来说明配置的用法,在second秒内达到changes次修改则会触发快照,只要任意一个save配置条件满足就会触发bgsave命令。
# Save the DB to disk.
#
# save <seconds> <changes>
...
# Unless specified otherwise, by default Redis will save the DB:
# * After 3600 seconds (an hour) if at least 1 key changed
# * After 300 seconds (5 minutes) if at least 100 keys changed
# * After 60 seconds if at least 10000 keys changed
#
# You can set these explicitly by uncommenting the three following lines.
#
# save 3600 1
# save 300 100
# save 60 10000
设置的触发条件被保存在saveparams里,同时,redis通过dirty计数器和lastsave时间戳来记录自上次生成RDB文件之后数据库有多少次修改
struct redisServer{
...
long long dirty;
struct saveparam *saveparams;
time_t lastsave;
...
};
struct saveparam {
// 多少秒之内
time_t seconds;
// 发生多少次修改
int changes;
};
通过redisCron来检查条件是否需要调用bgsave
int serverCron{
...
//判断当前没有在处理的bgsave或bgrewriteaof
for(int i = 0; i < server.saveparamslen; i++) {
if(server.dirty >= saveparam.changes && server.unixtime - server.lastsave > saveparam.seconds ) {
bgsave();
break;
}
}
}
...
return 1000/server.hz; //返回下次执行时间间隔 默认100ms
- AOF持久化
与RDB快照文件不同,AOF通过记录服务器所有的写命令来实现宕机后恢复的。
aof文件内容采用文本形式记录,与通信协议使用的RESP2规则一致。以set key value
为例,在aof文件中会表示成*3\r\n$3\r\nset\r\n$3\r\nkey\r\n$5\r\nvalue
- *3表示当前命令有三个部分
- $n表示后面跟的命令、键、值的占用n个字节
同样的,我们需要调整配置来开启AOF,在redis.conf文件中将appendonly no 改为yes。配置文件中默认开启的是everysec模式
- always 每次写命令执行完都会将命令回写到磁盘
- everysec 将写命令存入aof文件缓存,通过serverCron每秒回写
- no 将写命令存入缓存,通过操作系统控制何时回写
# appendfsync always
appendfsync everysec
# appendfsync no
AOF重写
因为aof文件记录所有key的变更记录,随着提供服务的时长,文件也会逐渐变大。通过aof文件进行恢复也会非常耗时,需要将历史命令再执行一遍。redis提供了aof rewrite,可以把多个操作同一key的命令重写为一条能够表示当前状态的命令。
aof重写需要重新将文件写回磁盘,需要耗费大量时间。所以,aof重写也采用fork子进程的方式避免阻塞主进程,在进行bgrewrite期间,写命令会记录到aof和aof重写缓冲区,以避免宕机导致数据丢失。
源码部分
struct redisServer{
...
sds aof_buf;//aof缓存字段 sds类型,可以向后追加
...
};
在redis.c中的main方法最后会调用aeMain开启事件Loop,处理时间事件和文件事件。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
beforesleep方法中调用了flushAppendOnlyFile,将aof缓存刷入磁盘
void beforeSleep(struct aeEventLoop *eventLoop) {
// ...
flushAppendOnlyFile(0);
// ...
}
void flushAppendOnlyFile(int force) {
// ...
if (server.aof_fsync == AOF_FSYNC_ALWAYS) {
// ...
aof_fsync(server.aof_fd); // 同步策略为always时,尝试将缓存写到磁盘
// ...
} else if ((server.aof_fsync == AOF_FSYNC_EVERYSEC &&
server.unixtime > server.aof_last_fsync)) {
// 提交任务
if (!sync_in_progress) aof_background_fsync(server.aof_fd);
// ...
}
}
aof的rewrite同样也是通过serverCron
int serverCron{
...
// 没有后台执行的rdb或aof,且有aof重写任务需要执行
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
server.aof_rewrite_scheduled)
{
rewriteAppendOnlyFileBackground();
}
...
}
int rewriteAppendOnlyFileBackground(void) {
...
if ((childpid = fork()) == 0) {
// 创建临时文件
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == REDIS_OK) {
// 重写成功的一些操作
}
} else {
// 父进程操作
}
}
int rewriteAppendOnlyFile(char *filename) {
...
// 遍历所有数据库
for(j = 0; j < server.dbnum; j++) {
...
// 迭代所有的key
while((de = dictNext(di)) != NULL) {
//根据不同的对象类型进行相应的重写操作
}
}
}