Redis RDB持久化机制
1. RDB的介绍
因为Redis是内存数据库,因此将数据存储在内存中,如果一旦服务器进程退出,服务器中的数据库状态就会消失不见,为了解决这个问题,Redis提供了两种持久化的机制:RDB和AOF。本篇主要剖析RDB持久化的过程。
RDB持久化是把当前进程数据生成时间点快照(point-in-time snapshot)保存到硬盘的过程,避免数据意外丢失。
1.1 RDB触发机制
RDB触发机制分为手动触发和自动触发。
手动触发的两条命令:
SAVE:阻塞当前Redis服务器,知道RDB过程完成为止。
BGSAVE:Redis 进程执行fork()操作创建出一个子进程,在后台完成RDB持久化的过程。(主流)
自动触发的配置:
c
save 900 1 //服务器在900秒之内,对数据库执行了至少1次修改
save 300 10 //服务器在300秒之内,对数据库执行了至少10修改
save 60 1000 //服务器在60秒之内,对数据库执行了至少1000修改
// 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作
// 或者使用命令CONFIG SET 命令配置
1.2 RDB持久化的流程
我们用图来表示 BGSAVE命令 的触发流程,如下图所示:
RDB命令源码如下:
void bgsaveCommand(client *c) {
int schedule = 0;
if (c->argc > 1) {
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"schedule")) {
schedule = 1;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (server.aof_child_pid != -1) {
if (schedule) {
server.rdb_bgsave_scheduled = 1;
addReplyStatus(c,"Background saving scheduled");
} else {
addReplyError(c,
"An AOF log rewriting in progress: can't BGSAVE right now. "
"Use BGSAVE SCHEDULE in order to schedule a BGSAVE whenver "
"possible.");
}
} else if (rdbSaveBackground(server.rdb_filename) == C_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
我们后面会重点讲解rdbSaveBackground()函数的工作过程。
1.3 RDB的优缺点
RDB的优点:
RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据快照。非常适用于备份,全景复制等场景。
- Redis 加载
RDB恢复数据远远快于AOF的方式。
RDB的缺点:
RDB没有办法做到实时持久化或秒级持久化。因为BGSAVE每次运行的又要进行fork()的调用创建子进程,这属于重量级操作,频繁执行成本过高,因为虽然Linux支持读时共享,写时拷贝(copy-on-write)的技术,但是仍然会有大量的父进程的空间内存页表,信号控制表,寄存器资源等等的复制。
RDB文件使用特定的二进制格式保存,Redis版本演进的过程中,有多个RDB版本,这导致版本兼容的问题。
2. RDB 的源码剖析
之前我们给出了 BGSAVE命令 的源码,因此我们就重点剖析 rdbSaveBackground()的工作过程,一层一层的剥开封装。
在RDB持久化之前需要设置一些标识,用来标识服务器当前的状态,定义在server.h/struct redisServer 结构体中,我们列出会用到的一部分,如果需要可以在这里查看。
struct redisServer {
redisDb *db;
list *slaves, *qiank;
int loading;
off_t loading_total_bytes;
off_t loading_loaded_bytes;
time_t loading_start_time;
off_t loading_process_events_interval_bytes;
size_t stat_peak_memory;
long long stat_fork_time;
double stat_fork_rate;
long long dirty;
long long dirty_before_bgsave;
pid_t rdb_child_pid;
struct saveparam *saveparams;
int saveparamslen;
char *rdb_filename;
int rdb_compression;
int rdb_checksum;
time_t lastsave;
time_t lastbgsave_try;
time_t rdb_save_time_last;
time_t rdb_save_time_start;
int rdb_bgsave_scheduled;
int rdb_child_type;
int lastbgsave_status;
int stop_writes_on_bgsave_err;
int rdb_pipe_write_result_to_parent;
int rdb_pipe_read_result_from_child;
time_t unixtime;
long long mstime;
long long latency_monitor_threshold;
dict *latency_events;
};
然后我们直接给rdbSaveBackground()函数出源码:
在这里,就可以看见fork()函数的执行,在子进程中执行了rdbSave()函数,父进程则执行了一些设置状态的操作。
int rdbSaveBackground(char *filename) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
start = ustime();
if ((childpid = fork()) == 0) {
int retval;
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty();
if (private_dirty) {
serverLog(LL_NOTICE,
&#