redis是个内存数据库,所有的操作都是在内存中进行,但是内存有个特点是,程序出问题或者系统出问题、重启,关机都会造成内存数据丢失。
所以需要把内存中的数据dump到硬盘中备份起来。
RDB持久化,是内存数据库dump到硬盘的过程,其中RDB是个文件格式,待会介绍。
本文从两个方向剖析,
1)加载dump.rdb文件到内存中。
2)内存数据库dump到硬盘中dump.rdb文件。
加载dump.rdb文件到内存
main函数入口:
int main(int argc, char **argv) { //... // 从 AOF 文件或者 RDB 文件中载入数据 loadDataFromDisk(); //... }loadDataFromDisk函数就是加载硬盘数据到内存(AOF或者RDB),具体的实现来看代码:
/* Function called at startup to load RDB or AOF file in memory. */ void loadDataFromDisk(void) { // 记录开始时间 long long start = ustime(); // AOF 持久化已打开? if (server.aof_state == REDIS_AOF_ON) { // 尝试载入 AOF 文件 if (loadAppendOnlyFile(server.aof_filename) == REDIS_OK) // 打印载入信息,并计算载入耗时长度 redisLog(REDIS_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000); // AOF 持久化未打开 } else { // 尝试载入 RDB 文件 if (rdbLoad(server.rdb_filename) == REDIS_OK) { // 打印载入信息,并计算载入耗时长度 redisLog(REDIS_NOTICE,"DB loaded from disk: %.3f seconds", (float)(ustime()-start)/1000000); } else if (errno != ENOENT) { redisLog(REDIS_WARNING,"Fatal error loading the DB: %s. Exiting.",strerror(errno)); exit(1); } } }AOF下一篇文件再剖析他的结构,现在就看RDB文件的加载:if (rdbLoad(server.rdb_filename) == REDIS_OK) {// RDB文件的加载,server.rdb_filename默认值位dump.rdb
/* * 将给定 rdb 中保存的数据载入到数据库中。 */ int rdbLoad(char *filename) { uint32_t dbid; int type, rdbver; redisDb *db = server.db+0; char buf[1024]; long long expiretime, now = mstime(); FILE *fp; rio rdb; // 打开 rdb 文件 if ((fp = fopen(filename,"r")) == NULL) return REDIS_ERR; // 初始化写入流 rioInitWithFile(&rdb,fp); rdb.update_cksum = rdbLoadProgressCallback; rdb.max_processing_chunk = server.loading_process_events_interval_bytes; if (rioRead(&rdb,buf,9) == 0) goto eoferr; buf[9] = '\0'; // 检查版本号 if (memcmp(buf,"REDIS",5) != 0) { fclose(fp); redisLog(REDIS_WARNING,"Wrong signature trying to load DB from file"); errno = EINVAL; return REDIS_ERR; } rdbver = atoi(buf+5); if (rdbver < 1 || rdbver > REDIS_RDB_VERSION) { fclose(fp); redisLog(REDIS_WARNING,"Can't handle RDB format version %d",rdbver); errno = EINVAL; return REDIS_ERR; } // 将服务器状态调整到开始载入状态 startLoading(fp); while(1) { robj *key, *val; expiretime = -1; /* Read type. * * 读入类型指示,决定该如何读入之后跟着的数据。 * * 这个指示可以是 rdb.h 中定义的所有以 * REDIS_RDB_TYPE_* 为前缀的常量的其中一个 * 或者所有以 REDIS_RDB_OPCODE_* 为前缀的常量的其中一个 */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; // 读入过期时间值 if (type == REDIS_RDB_OPCODE_EXPIRETIME) { // 以秒计算的过期时间 if ((expiretime = rdbLoadTime(&rdb)) == -1) goto eoferr; /* We read the time so we need to read the object type again. * * 在过期时间之后会跟着一个键值对,我们要读入这个键值对的类型 */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; /* the EXPIRETIME opcode specifies time in seconds, so convert * into milliseconds. * * 将格式转换为毫秒*/ expiretime *= 1000; } else if (type == REDIS_RDB_OPCODE_EXPIRETIME_MS) { // 以毫秒计算的过期时间 /* Milliseconds precision expire times introduced with RDB * version 3. */ if ((expiretime = rdbLoadMillisecondTime(&rdb)) == -1) goto eoferr; /* We read the time so we need to read the object type again. * * 在过期时间之后会跟着一个键值对,我们要读入这个键值对的类型 */ if ((type = rdbLoadType(&rdb)) == -1) goto eoferr; } // 读入数据 EOF (不是 rdb 文件的 EOF) if (type == REDIS_RDB_OPCODE_EOF) break; /* Handle SELECT DB opcode as a special case * * 读入切换数据库指示 */ if (type == REDIS_RDB_OPCODE_SELECTDB) { // 读入数据库号码 if ((dbid = rdbLoadLen(&rdb,NULL)) == REDIS_RDB_LENERR) goto eoferr; // 检查数据库号码的正确性 if (dbid >= (unsigned)server.dbnum) { redisLog(REDIS_WARNING,"FATAL: Data file was created with a Redis server configured to handle more than %d databases. Exiting\n", server.dbnum); exit(1); } // 在程序内容切换数据库 db = server.db+dbid; // 跳过 continue; } /* Read key * * 读入键 */ if ((key = rdbLoadStringObject(&rdb)) == NULL) goto eoferr; /* Read value * * 读入值 */ if ((val = rdbLoadObject(type,&rdb)) == NULL) goto eoferr; /* Check if the key already expired. This function is used when loading * an RDB file from disk, either at startup, or when an RDB was * received from the master. In the latter case, the master is * responsible for key expiry. If we would expire keys here, the * snapshot taken by the master may not be reflected on the slave. * * 如果服务器为主节点的话, * 那么在键已经过期的时候,不再将它们关联到数据库中去 */ if (server.masterhost == NULL && expiretime != -1 && expiretime < now) { decrRefCount(key); decrRefCount(val); // 跳过 continue; } /* Add the new object in the hash table * * 将键值对关联到数据库中 */ dbAdd(db,key,val); /* Set the expire time if needed * * 设置过期时间 */ if (expiretime != -1) setExpire(db,key,expiretime); decrRefCount(key); } /* Verify the checksum if RDB version is >= 5 * * 如果 RDB 版本 >= 5 ,那么比对校验和 */ if (rdbver >= 5 && server.rdb_checksum) { uint64_t cksum, expected = rdb.cksum; // 读入文件的校验和 if (rioRead(&rdb,&cksum,8) == 0) goto eoferr; memrev64ifbe(&cksum); // 比对校验和 if (cksum == 0) { redisLog(REDIS_WARNING,"RDB file was saved with checksum disabled: no check performed."); } else if (cksum != expected) { redisLog(REDIS_WARNING,"Wrong RDB checksum. Aborting now."); exit(1); } } // 关闭 RDB fclose(fp); // 服务器从载入状态中退出 stopLoading(); return REDIS_OK; eoferr: /* unexpected end of file is handled here with a fatal exit */ redisLog(REDIS_WARNING,"Short read or OOM loading DB. Unrecoverable error, aborting now."); exit(1); return REDIS_ERR; /* Just to avoid warning */ }
函数int rdbLoad(char *filename) 中,很明显看出rdb的文件结构,如下:
内存数据库dump到硬盘中dump.rdb文件
使用函数rdbSave:
/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success * * 将数据库保存到磁盘上。 * * 保存成功返回 REDIS_OK ,出错/失败返回 REDIS_ERR 。 */ 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; } /* 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 */ // 冲洗缓存,确保数据已写入磁盘 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; }
以上是大致rdb持优化的过程,细节还得继续扣代码。