RDB触发机制
命令触发
- SAVE:SAVE命令会阻塞Redis服务进程,知道RDB文件创建完毕为止。
- BGSAVE:BGSAVE会创建子进程,子进程负责创建RDB文件,父进程继续处理命令请求
自动间隔性保存
当配置文件中save选项的条件满足时,服务器自动执行BGSAVE命令。
// 满足以上三个条件中的任意一个,则自动触发 BGSAVE 操作 save 900 1 // 服务器在900秒之内,对数据库执行了至少1次修改 save 300 10 // 服务器在300秒之内,对数据库执行了至少10修改 save 60 1000 // 服务器在60秒之内,对数据库执行了至少1000修改
自动执行 BGSAVE 命令的条件保存在 redisServer 结构中的 savaparams 属性。当服务器成功执行一个修改命令后,dirty 计数会加一,而 lastsave属性记录了最后一次完成 SAVE 的 UNIX 时间戳。Redis的周期性操作函数 serverCron 会定时检查 save 选项的条件是否满足,如果满足,就会执行BGSAVE命令。
struct redisServer{ // 记录了BGSAVE自动执行的条件 struct saveparam *saveparams; // 自从上次 SAVE 执行以来,数据库被修改的次数 long long dirty; // 最后一次完成 SAVE 的时间 time_t lastsave; // ....... } // 服务器的保存条件(BGSAVE 自动执行的条件) struct saveparam { // 多少秒之内 time_t seconds; // 发生多少次修改 int changes; };
RDB持久化源码
BGSAVE 命令底层实现函数 dbSaveBackground 会先 fork 出子进程,由子进程执行 rdbSave 函数。整个持久化函数 rdbSave 的核心在于通过 rdbSaveKeyValuePair 函数保存数据库的键值对,rdbSaveKeyValuePair 函数底层的 rdbSaveObject 函数会针对不用的对象类型采用不用的编码格式来保存数据。
int rdbSave(char *filename) { dictIterator *di = NULL; dictEntry *de; char tmpfile[256]; char magic[10]; int j; long long now = mstime(); FILE *fp; rio rdb; uint64_t cksum; // 创建临时文件 snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid()); fp = fopen(tmpfile,"w"); if (!fp) { redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s", strerror(errno)); return REDIS_ERR; } // 初始化 I/O rioInitWithFile(&rdb,fp); // 设置校验和函数 if (server.rdb_checksum) rdb.update_cksum = rioGenericUpdateChecksum; // 写入 RDB 版本号 snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION); if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr; // 遍历所有数据库 for (j = 0; j < server.dbnum; j++) { // 指向数据库 redisDb *db = server.db+j; // 指向数据库键空间 dict *d = db->dict; // 跳过空数据库 if (dictSize(d) == 0) continue; // 创建键空间迭代器 di = dictGetSafeIterator(d); if (!di) { fclose(fp); return REDIS_ERR; } /* * 写入 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; // 写入 EOF 代码 if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_EOF) == -1) goto werr; /* * CRC64 校验和。 * 如果校验和功能已关闭,那么 rdb.cksum 将为 0 , * 在这种情况下, RDB 载入时会跳过校验和检查。 */ cksum = rdb.cksum; memrev64ifbe(&cksum); rioWrite(&rdb,&cksum,8); // 冲洗缓存,确保数据已写入磁盘 if (fflush(fp) == EOF) goto werr; if (fsync(fileno(fp)) == -1) goto werr; if (fclose(fp) == EOF) goto werr; /* * 使用 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; }
RDB文件结构
- REDIS:5字节,保存着 "REDIS" 五个字符
- db_version:4字节,RDB文件的版本号
- database 0:数据库中的键值对
- SELECTDB:1字节常量
- db_number:数据库号码
- key_value_pairs:键值对
- 含过期时间的键值对会带有 EXPIRETIME_MS 和过期时间
- EOF:RDB文件的结束标志
- check_sum:校验和(CRC64),用来检查RDB文件是否出错
key_value_pairs键值对中的 TYPE 属性:记录类对象的编码类型,程序会根据 TYPE 属性来决定如何读入和解释value数据。