这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
产生背景
Redis 是一个键值对数据库服务器,服务器中通常包含着任意个非空数据库,而每个非空数据库中又可以包含任意哥键值对。我们将服务器中非空数据库以及它们的键值对统称为数据库状态。
Redis 是内存数据库,它将自己的数据库状态存储在内存中。如果不讲存储在内存中的数据库状态保存到磁盘里,那么一旦服务器进程退出,服务器中的数据库状态也会消失不见
RDB 持久化方式及磁盘交互
Redis 提供了 RDB 的持久化方式,这个功能可以将 Redis 在内存中的数据库状态保存到磁盘里面,避免数据丢失。
RDB 持久化既可以手动执行,也可以根据服务器配置选项定期执行,该功能可以将某个时间点上的数据库状态保存在一个 RDB 文件中。
RDB 文件是一个压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态。
RDB 文件的创建与载入
有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE
,另一个是 BGSAVE
.
SAVE
命令会阻塞 Redis 服务器进程,直到 RDB 文件创建完毕为止,在服务器进程阻塞期间,服务器不能处理任何命令请求BGSAVE
命令会派生一个子进程,然后由子进程负责创建 RDB 文件,服务器进程(父进程)继续处理命令请求。
创建 RDB 文件的实际工作是由 rdb.c/rdbSave 函数完成。
void saveCommand(client *c) {
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
return;
}
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
// 创建 RDB 文件
if (rdbSave(server.rdb_filename,rsiptr) == C_OK) {
addReply(c,shared.ok);
} else {
addReply(c,shared.err);
}
}
void bgsaveCommand(client *c) {
// ...
if (server.rdb_child_pid != -1) {
addReplyError(c,"Background save already in progress");
} else if (hasActiveChildProcess()) {
// ...
} else if (rdbSaveBackground(server.rdb_filename,rsiptr) == C_OK) {
addReplyStatus(c,"Background saving started");
} else {
addReply(c,shared.err);
}
}
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
// ...
// 创建子进程
if ((childpid = redisFork(CHILD_TYPE_RDB)) == 0) {
// ...
// 保存
retval = rdbSave(filename,rsi);
// ...
} else {
// ...
}
return C_OK; /* unreached */
}
/* Save the DB on disk. Return C_ERR on error, C_OK on success. */
int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
FILE *fp = NULL;
rio rdb;
int error = 0;
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Failed opening the RDB file %s (in server root dir %s) "
"for saving: %s",
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
return C_ERR;
}
rioInitWithFile(&rdb,fp);
startSaving(RDBFLAGS_NONE);
if (server.rdb_save_incremental_fsync)
rioSetAutoSync(&rdb,REDIS_AUTOSYNC_BYTES);
if (rdbSaveRio(&rdb,&error,RDBFLAGS_NONE,rsi) == C_ERR) {
errno = error;
goto werr;
}
/* Make sure data will not remain on the OS's output buffers */
if (fflush(fp)) goto werr;
if (fsync(fileno(fp))) goto werr;
if (fclose(fp)) { fp = NULL; goto werr; }
fp = NULL;
// ...
}
自动间隔性保存
用户可以通过 save 选项设置多个保存条件,但只要其中任意一个条件被满足,服务器就会执行 BGSAVE
命令
举个例子,如果我们向服务器提供一下配置:
save 900 1 # 服务器 900 秒内,对数据库进行至少 1 次修改
save 300 10 # 服务器 300 秒内,对数据库进行至少 10 次修改
save 60 10000 # 服务器 60 秒内,对数据库进行至少 10000 次修改
那么只要满足以上三个条件中的任意一个,BGSAVE
命令就会被执行
设置保存条件
服务器程序会根据 save 选项所设置的保存条件,设置服务器状态 redisServer 结构的 saveparams 属性
struct redisServer {
// ...
// 记录了保存条件的数组
struct saveparam *saveparams;
// ...
}
struct saveparam {
// 秒数
time_t seconds;
// 修改数
int change;
}
比如说上述save选项的值在服务器中存储的 saveparam 数组,如下图
dirty 计数器和 lastsave 属性
除了 saveparams 数组之外,服务器状态还维持着一个 dirty 计数器,以及一个 lastsave 属性
- dirty 计数器记录距离上一次成功执行
SAVE
或者BGSAVE
命令后,服务器对数据库状态进行了多少次修改 - lastsave 是一个时间戳,记录了服务器上一次成功执行
SAVE
或者BGSAVE
命令的时间
struct redisServer {
// ...
// 修改计数器
long long dirty;
// 上一次执行保存的时间
time_t lastsave;
// ...
}
当服务器成功执行一个数据库修改命令后,程序就会对 dirty 计数器进行更新: 命令修改了多少次数据库,dirty 计数器就增加多少
检查保存条件是否满足
Redis 服务器周期性操作函数 serverCron 默认每隔 100ms 就会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是通过遍历 saveparams 数组中的所有保存条件,检查是否存在满足的条件,如果有,则执行 BGSAVE
。