为什么需要Redis持久化?
redis本身是内存数据库,数据都是存储在操作系统内存中的,那么就会存在当进程退出或者服务断电等情况下内存数据丢失的问题,所以redis就需要提供某种持久化方案用来把内存中的数据保存到磁盘上,用以解决内存数据丢失的问题。
持久化的几种方式?
一提到持久化,不仅仅是Redis,基本上所有的持久化方案都会采用“快照”和“日志”这个两种方式,快照是一种定时生成的方式,而日志则更倾向于实时产生,(redis4.0开始支持了快照和日志的混合模式)
Redis中的RDB
本文先介绍Redis中的第一种持久化方案—RDB,这就是基于“快照”的方式完成持久化的。
在redis中既可以手动执行命令触发rdb,也可以通过服务端的配置选择定时触发rdb,rdb执行结束后生成一个经过压缩的二进制.rdb文件,还原时再通过这个rdb文件进行恢复。
触发rdb的命令
在redis中提供了两种命令可以生成rdb文件,一个是save,一个bgsave。
执行save命令后会阻塞服务端进程,直到RDB文件创建完毕为止,在整个RDB文件生成过程中服务器不能处理任何命令请求,所以一般情况下我们不会使用save命令触发rdb持久化。
而bgsave则是会fork出一个子进程,父进程继续处理客户端发来的命令请求,子进程写入完成时,用新的rdb替换旧的rdb,并删除旧的rdb文件,整个过程只是调用fork函数时会阻塞。
save命令执行后的日志信息
1264:M 02 Nov 2020 14:19:37.547 * DB saved on disk
bgsave命令执行后的日志信息,可以明显看出多出了有一个1289的进程。
1264:M 02 Nov 2020 14:19:49.731 * Background saving started by pid 1289
1289:C 02 Nov 2020 14:19:49.739 * DB saved on disk
1289:C 02 Nov 2020 14:19:49.739 * RDB: 4 MB of memory used by copy-on-write
1264:M 02 Nov 2020 14:19:49.743 * Background saving terminated with success
rdb的自动触发配置
在conf配置文件中,找到如下的配置项,文档已经描述的非常详细,以下这段配置表示的含义如下:
save 900 1:900秒之内,对数据库至少修改了1次。
save 300 10:300秒之内,对数据库至少修改了10次。
save 60 10000:60秒之内,对数据库至少修改了10000次。
只要满足三个条件中任意一个,就会触发bgsave。
bgsave运行流程
源码入口
save
int rdbSave(char *filename, rdbSaveInfo *rsi) {
char tmpfile[256];
char cwd[MAXPATHLEN]; /* Current working dir path for error messages. */
FILE *fp;
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) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
/* Use RENAME to make sure the DB file is changed atomically only
* if the generate DB file is ok. */
if (rename(tmpfile,filename) == -1) {
char *cwdp = getcwd(cwd,MAXPATHLEN);
serverLog(LL_WARNING,
"Error moving temp DB file %s on the final "
"destination %s (in server root dir %s): %s",
tmpfile,
filename,
cwdp ? cwdp : "unknown",
strerror(errno));
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
serverLog(LL_NOTICE,"DB saved on disk");
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
stopSaving(1);
return C_OK;
werr:
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
stopSaving(0);
return C_ERR;
}
bgsave
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
if (hasActiveChildProcess()) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
openChildInfoPipe();
if ((childpid = redisFork()) == 0) {
int retval;
/* Child */
redisSetProcTitle("redis-rdb-bgsave");
redisSetCpuAffinity(server.bgsave_cpulist);
retval = rdbSave(filename,rsi);
if (retval == C_OK) {
sendChildCOWInfo(CHILD_INFO_TYPE_RDB, "RDB");
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
/* Parent */
if (childpid == -1) {
closeChildInfoPipe();
server.lastbgsave_status = C_ERR;
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
return C_OK;
}
return C_OK; /* unreached */
}
dirty计数器和lastsave
-
dirty计数器记录距离上一次成功执行save命令或者bgsave命令之后,服务器对数据库状态(服务器中的所有数据库)进行了多少次修改(包括写入、删除、更新等操作)。
-
lastsave属性是一个UNIX时间戳,记录了服务器上一次成功执行save命令或者bgsave命令的时间。
当服务器成功执行一个数据库修改命名之后,程序就会对dirty计数器进行更新,命令修改了多少次数据库,dirty计数器的值就增加多少。
定时检查是否满足触发RDB条件
Redis服务器通过serverCron函数,默认情况下每隔100毫秒会执行一次,该函数用于对正在运行的服务器进行维护,其中一项工作就是检查save选择所设置的保持条件是否已经满足,如果满足的话,就执行bgsave命令。
RDB文件的载入
当redis服务器在启动时如果检测到rdb文件的存在,就会自动载入,如果开启了aof,那么服务器会优先使用aof文件进行恢复。
RDB其他参数的介绍
stop-writes-on-bgsave-error
默认情况下,如果bgsave失败,Redis将停止接受写操作。以此让用户意识到bgsave出了问题,否则可能没有人会注意到,并将发生一些灾难。
如果bgsave再次开始工作,Redis将自动再次允许写入。
然而,如果你已经设置了适当的监测Redis服务器和持久性,你可以禁用这个功能,这样即使生成rdb文件时出现磁盘、权限等出现问题,redis也会照常工作。
rdbcompression
默认情况下使用LZF压缩,如果你不想消耗CPU,可以设置为false,但是文件可能会更大。
rdbchecksum
使用CRC64算法对生成的rdb文件进行检验,但会有大约10%的性能消耗。
dbfilename
生成rdb文件的名称
dir
生成rdb文件的目录
写时拷贝(copy-on-write)
上面bgsave日志的截图中,出现了一个关键词 copy-on-write,这是提高bgsave性能的关键,unix系统中,提供的fork函数,就是copy-on-write的运用。
fork系统调用会产生一个子进程,它与父进程可以共享内存地址,所以当产生fork调动那一时刻,父子进程就有着完全相同的内容。
1、刚调用fork函数时,父子进程都映射了同一块物理地址
2、此时如果在子进程bgsave过程中,数据A发生写操作。
看出变化了没,对数据A的写操作并不会影响子进程,而父进程也没有直接对数据A所在的物理地址进行写入,而是重新复制了一份,然后在新复制的数据操作,这就是copy-on-write。
通过copy-on-write机制,在创建子进程时就不会立刻产生全量的内存数据拷贝消耗,从而避免了性能问题。
RDB的优缺点
优点
- 非常适用于有需要定期对数据进行备份的场景。
- 由于是以二进制文件进行保存,所以恢复速度快。
- bgsave命令下,通过fork子进程完成rdb文件的生成,对父进程影响小。
缺点
- 由于rdb一般都是采用周期性生成快照的方式,所以丢失数据严重。
- 二进制文件保存,虽然恢复快,但可读性很差。
- fork函数执行过程中,如果父进程有大量的写入,对于内存的消耗会较高。