1 RDB持久化简介
Redis是一个键值对数据库服务器,服务器中通常包含任意个非空的数据库,每个非空的数据库又包含任意个键值对。为方便起见,我们将非空数据库及它们的键值对统称为数据库状态。RDB是Redis持久化方式的一种,可以将Redis在内存中的数据库状态保存到磁盘中,避免意外丢失。
RDB持久化既可以手动执行,也可以根据服务器的配置选项自动执行,该功能可以将某个时间点上的数据库状态保存到一个RDB文件中----一个经过压缩的二进制文件,通过该文件可以将数据库还原到生成该文件时的状态。
1.1 RDB文件的结构
RDB文件的基本结构如下:
- REDIS 文件最开头保存着REDIS五个字符,标志rdb文件的开始
- db_version 一个四字节的以字符表示的整数,记录了文件使用的RDB版本号
- databases 存放了服务器上所有非空数据库的所有数据
- EOF 标志着数据库内容的结尾(不是文件的结尾)
-
CHECK_SUM RDB 文件所有内容的校验和, 一个
uint_64t
类型值。REDIS 在写入 RDB 文件时将校验和保存在 RDB 文件的末尾,当读取时, 根据它的值对内容进行校验
RDB文件中的数据库(database)的结构如下:
- SELECTDB 一字节的常量,标示接下来要读的是一个数据库号码。
- db_number 保存了一个数据库号码,使后面读入的键值对可以载入到正确的数据库中。
- key_value_pairs 保存了键值对,如果键值对带有过期时间也保存在内。
不带过期时间的键值对结构:
带过期时间的键值对结构:
2 RDB持久化的实现
2.1 RDB文件的创建
有两个命令可以生成RDB文件:
- SAVE: 阻塞Redis服务器进程,直到RDB文件创建完毕为止,在服务器阻塞期间,不能处理任何命令请求;
- BGSAVE: 该命令会派生出一个子进程,由子进程负责RDB文件的创建;服务器进程(父进程)继续处理命令请求;
RDB的创建工作由rdb.c/rdbSave()完成,SAVE和BGSAVE通过不同的方式调用该函数,如下面的伪代码:
def SAVE(): #创建RDB文件 rdbSave() def BGSAVE(): #创建子进程 pid = fork() if pid == 0: #子进程创建RDB文件 rdbSave() #完成之后向父进程发送信号 signal_parent() elif pid > 0: #父进程处理命令请求并通过轮询等待子进程的信号 handle_request_and_wait_signal() else: #处理出错情况 handle_fork_error()
2.1.1 SAVE/BGSAVE执行时数据库的状态
(1)SAVE命令执行时,服务器被阻塞,拒绝所有客户端命令请求;
(2)BGSAVE命令执行时,RDB文件的创建工作在子进程中执行,父进程仍可以接受客户端命令请求。但是服务器对SAVE, BGSAVE, BGREWRITEAOF的处理方式和平时有所不同。
- BGSAVE执行期间,客户端发送的SAVE, BGSAVE命令会被拒绝,因为两个进程同时调用rdbSave(),可能产生竞争条件;
- BGSAVE和BGREWRITEAOF同样不能同时执行。如果BGSAVE正在执行,则BGREWRITEAOF将会延迟到BGSAVE执行完之后再执行;如果BGREWRITEAOF正在执行,则客户端发送的BGSAVE命令将会被拒绝。(这两个命令不同时执行,主要是性能方面的考虑,俩子进程同时执行大量磁盘写入操作不会是个好主意)
2.1.2 rdbSave()函数的实现
int rdbSave(char *filename) { dictIterator *di = NULL; dictEntry *de; FILE *fp; rio rdb; uint64_t cksum; ... // 创建临时文件 snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); // 初始化 I/O rioInitWithFile(&rdb,fp); // 写入 RDB 版本号 snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION); if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr; // 遍历所有数据库,将有效数据库状态保存到rdb文件中 for (j = 0; j < server.dbnum; j++) { // 指向数据库 redisDb *db = server.db+j; // 指向数据库键空间 dict *d = db->dict; // 跳过空数据库 if (dictSize(d) == 0) continue; // 创建键空间迭代器 di = dictGetSafeIterator(d); /* Write the SELECT DB opcode, 写入 DB 选择器 */ if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr; if (rdbSaveLen(&rdb,j) == -1) goto werr; /* Iterate this DB writing every entry * 遍历数据库,并写入每个键值对的数据 */ while((de = dictNext(di)) != NULL) { sds keystr = dictGetKey(de); robj key, *o = dictGetVal(de); long long expire; // 根据 keystr ,在栈中创建一个 key 对象 initStaticStringObject(key,keystr); // 获取键的过期时间 expire = getExpire(db,&key); // 保存键值对数据 if (rdbSaveKeyValuePair(&rdb,&key,o,expire,now) == -1) goto werr; } dictReleaseIterator(di); } di = NULL; /* So that we don't release it again on error. */ /* EOF opcode * * 写入 EOF 代码 */ if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr; /* CRC64 checksum. It will be zero if checksum computation is disabled, the * loading code skips the check in this case. * * CRC64 校验和。 * * 如果校验和功能已关闭,那么 rdb.cksum 将为 0 , * 在这种情况下, RDB 载入时会跳过校验和检查。 */ cksum = rdb.cksum; memrev64ifbe(&cksum); rioWrite(&rdb,&cksum,8); /* Make sure data will not remain on the OS's output buffers */ // 冲洗缓存,确保数据已写入磁盘 // c库缓冲—–fflush———〉内核缓冲——–fsync—–〉磁盘 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. * * 使用 RENAME ,原子性地对临时文件进行改名,覆盖原来的 RDB 文件。 */ if (rename(tmpfile,filename) == -1) { redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno)); unlink(tmpfile); return REDIS_ERR; } // 写入完成,打印日志 redisLog(REDIS_NOTICE,"DB saved on disk"); // 清零数据库脏状态 server.dirty = 0; // 记录最后一次完成 SAVE 的时间 server.lastsave = time(NULL); // 记录最后一次执行 SAVE 的状态 server.lastbgsave_status = REDIS_OK; return REDIS_OK; werr: // 关闭文件 fclose(fp); // 删除文件 unlink(tmpfile); redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno)); if (di) dictReleaseIterator(di); return REDIS_ERR; }
2.1.3 RDB持久化--自动间隔保存
redis允许用户设置服务器配置的save选项,让服务器每隔一段时间执行一次bgsave。用户可以设置多个保存条件,只要一个条件满足,服务器就会执行bgsave。
(1)设置保存条件
save 900 1 服务器在900秒之内,对数据库进行至少1次修改struct redisServer { ... struct saveparam *saveparams; /* Save points array for RDB */ } struct saveparam { time_t seconds; int changes; };
(2)dirty计数器和lastsave属性
struct redisServer { ... time_t lastsave; /* Unix time of last successful save */ time_t lastbgsave_try; /* Unix time of last attempted bgsave */ }
(3)检查保存条件
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 || ldbPendingChildren()) {……} else {//遍历设置的保存条件 for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && server.unixtime-server.lastsave > sp->seconds && (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY || server.lastbgsave_status == C_OK)) {//满足保存条件,进行rdb rdbSaveBackground(server.rdb_filename,NULL); break; } } } }
2.2 RDB文件的加载
Redis的有两种持久化方式:RDB, AOF,而且AOF文件的更新频率通常比RDB文件高,所以,如果开启了AOF持久化功能,则会就先采用AOF文件还原数据库状态:
rdb文件重载入内存还原数据库时,采用的是rdbLoad函数:
int rdbLoad(char *filename, rdbSaveInfo *rsi) { …… if ((fp = fopen(filename,"r")) == NULL) return C_ERR; startLoading(fp); rioInitWithFile(&rdb,fp); retval = rdbLoadRio(&rdb,rsi);//载入rdb文件 …… return retval; } int rdbLoadRio(rio *rdb, rdbSaveInfo *rsi) { …… rdb->update_cksum = rdbLoadProgressCallback; rdb->max_processing_chunk = server.loading_process_events_interval_bytes; if (rioRead(rdb,buf,9) == 0) goto eoferr; buf[9] = '