rdb是redis保存内存数据到磁盘数据的其中一种方式(另一种是AOF)。Rdb的主要原理就是在某个时间点把内存中的所有数据的快照保存一份到磁盘上。在条件达到时通过fork一个子进程把内存中的数据写到一个临时文件中来实现保存数据快照。在所有数据写完后再把这个临时文件用原子函数rename(2)重命名为目标rdb文件。这种实现方式充分利用fork的copy on write。
另外一种是通过save命令主动触发保存数据快照,这种是阻塞式的,即不会通过生成子进程来进行数据集快照的保存。
相关配置
save <seconds> <changes> |
经过多少秒且多少个key有改变就进行,可以配置多个,只要有一个满足就进行保存数据快照到磁盘
rdbcompression yes |
保存数据到rdb文件时是否进行压缩,如果不想可以配置成’no’,默认是’yes’,因为压缩可以减少I/O,当然,压缩需要消耗一些cpu资源。
dbfilename dump.rdb |
快照文件名
dir ./ |
快照文件所在的目录,同时也是AOF文件所在的目录
Rdb文件格式
[注:本节所说的类型,值在没有特别标明的情况下都是针对rdb文件来说的]
Rdb文件的整体格式
文件签名 | 版本号 | 类型 | 值 | 类型 | 值 | … | 类型 | 值
[注:竖线和空格是为了便于阅读而加入的,rdb文件中是没有竖线和空格分隔的]
- 文件签名是字符串:REDIS
- 版本号是字符串:0002
- 类型是指值的类型,redis值的类型有很多种,下边一一介绍
- 值是对应的类型下的值,不同类型的值格式不一样。这里的值包含了redis中的key与val。而不是单指redis中val。
REDIS_SELECTDB类型与REDIS_EOF类型
- REDIS_SELECTDB类型:对应的值是redis db的编号,从0开始到比db数小1的数值。redis中可以配置db数,每个key只属于一个db。
- 存储redis db的编号时使用的是存储长度时使用的格式,为了尽量压缩rdb文件,存储长度使用的字节数是不一样的,具体见下边rdb中长度的存储
- REDIS_EOF类型:没有对应的值。rdb文件的结束符。
把这REDIS_SELECTDB类型和REDIS_EOF类型代入到上边的rdb文件的格式中,那么rdb文件的整体格式变成为:
文件签名 | 版本号 | REDIS_SELECTDB类型 | db编号 | 类型 | 值 | … | REDIS_SELECTD 类型 | db编号 | 类型 | 值 | … | REDIS_EOF类型
- 每个db编号后边到下一个REDIS_SELECTDB类型出现之前的数据都是该db下边的key和value的数据
相关代码
Rdb.c:394
int rdbSave(char *filename) { … fp = fopen(tmpfile,”w”); if (!fp) { redisLog(REDIS_WARNING, “Failed saving the DB: %s”, strerror(errno)); return REDIS_ERR; } if (fwrite(“REDIS0002”,9,1,fp) == 0) goto werr; for (j = 0; j < server.dbnum; j++) { … /* Write the SELECT DB opcode */ if (rdbSaveType(fp,REDIS_SELECTDB) == -1) goto werr; if (rdbSaveLen(fp,j) == -1) goto werr; /* Iterate this DB writing every entry */ while((de = dictNext(di)) != NULL) { … initStaticStringObject(key,keystr); expiretime = getExpire(db,&key); /* Save the expire time */ if (expiretime != -1) { /* If this key is already expired skip it */ if (expiretime < now) continue; if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr; if (rdbSaveTime(fp,expiretime) == -1) goto werr; } /* Save the key and associated value. This requires special * handling if the value is swapped out. */ if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY || o->storage == REDIS_VM_SWAPPING) { int otype = getObjectSaveType(o); /* Save type, key, value */ if (rdbSaveType(fp,otype) == -1) goto werr; if (rdbSaveStringObject(fp,&key) == -1) goto werr; if (rdbSaveObject(fp,o) == -1) goto werr; } else { /* REDIS_VM_SWAPPED or REDIS_VM_LOADING */ robj *po; /* Get a preview of the object in memory */ po = vmPreviewObject(o); /* Save type, key, value */ if (rdbSaveType(fp,getObjectSaveType(po)) == -1) goto werr; if (rdbSaveStringObject(fp,&key) == -1) goto werr; if (rdbSaveObject(fp,po) == -1) goto werr; /* Remove the loaded object from memory */ decrRefCount(po); } } dictReleaseIterator(di); } /* EOF opcode */ if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr; … } |
Rdb中长度的存储
Redis为了尽量压缩rdb文件真是费尽心思,先来看看redis为了压缩使用的长度存储。长度主要用在字符串长度,链表长度,hash表的大小存储上。
Redis把长度的存储分为四种,最左边字节的从左到右的前两位用于区分长度的存储类型。
相关代码
Rdb.c:31
int rdbSaveLen(FILE *fp, uint32_t len) { unsigned char buf[2]; int nwritten; if (len < (1<<6)) { /* Save a 6 bit len */ buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6); if (rdbWriteRaw(fp,buf,1) == -1) return -1; nwritten = 1; } else if (len < (1<<14)) { /* Save a 14 bit len */ buf[0] = ((len>>8)&0xFF)|(REDIS_RDB_14BITLEN<<6); buf[1] = len&0xFF; if (rdbWriteRaw(fp,buf,2) == -1) return -1; nwritten = 2; } else { /* Save a 32 bit len */ buf[0] = (REDIS_RDB_32BITLEN<<6); if (rdbWriteRaw(fp,buf,1) == -1) return -1; len = htonl(len); if (rdbWriteRaw(fp,&len,4) == -1) return -1; nwritten = 1+4; } return nwritten; } |
也许你发现了,上边的表格中只有3种,还有一种哪去了呢?
把这种特别放开是因为这种比较特殊
是不是觉得这种长度类型很奇怪,为什么要这样做?
Redis在两种情况下需要对存储的内容进行编码
1.把字符串转成整数存储
比如:‘-100’需要4个字节存储,转换整数只需要一个字节
相关函数rdbTryIntegerEncoding(rdb.c:88)
2.使用lzf算法压缩字符串
相关函数lzf_compress(lzf_c.c:99),lzf的算法解释见lzf字符串压缩算法
当redis使用这两种编码对字符串进行编码时,在读取时需要区分改字符串有没有被编码过,对编码过的字符串需要特别处理,因为长度信息是存储在字符串的前面得,所以可以通过在存储长度的位置上加入编码类型的信息。
我们来看看相关代码
Rdb.c:557
uint32_t rdbLoadLen(FILE *fp, int *isencoded) { unsigned char buf[2]; uint32_t len; int type; if (isencoded) *isencoded = 0; if (fread(buf,1,1,fp) == 0) return REDIS_RDB_LENERR; type = (buf[0]&0xC0)>>6; if (type == REDIS_RDB_6BITLEN) { /* Read a 6 bit len */ return buf[0]&0x3F; } else if (type == REDIS_RDB_ENCVAL) { /* Read a 6 bit len encoding type */ if (isencoded) *isencoded = 1; return buf[0]&0x3F; } else if (type == REDIS_RDB_14BITLEN) { /* Read a 14 bit len */ if (fread(buf+1,1,1,fp) == 0) return REDIS_RDB_LENERR; return ((buf[0]&0x3F)<<8)|buf[1]; } else { /* Read a 32 bit len */ if (fread(&len,4,1,fp) == 0) return REDIS_RDB_LENERR; return ntohl(len); } } |
我们可以看到,在读取rdb文件时,当发现长度类型是REDIS_RDB_ENCVAL,把编码类型返回。
我们来看看知道编码类型后的处理
Rdb.c:633
robj *rdbGenericLoadStringObject(FILE*fp, int encode) { int isencoded; uint32_t len; sds val; len = rdbLoadLen(fp,&isencoded); if (isencoded) { switch(len) { case REDIS_RDB_ENC_INT8: case REDIS_RDB_ENC_INT16: case REDIS_RDB_ENC_INT32: return rdbLoadIntegerObject(fp,len,encode); case REDIS_RDB_ENC_LZF: return rdbLoadLzfStringObject(fp); default: redisPanic(“Unknown RDB encoding type”); } } if (len == REDIS_RDB_LENERR) return NULL; val = sdsnewlen(NULL,len); if (len && fread(val,len,1,fp) == 0) { sdsfree(val); return NULL; } return createObject(REDIS_STRING,val); } |
- 读取长度
- 如果长度类型是有编码信息的,则根据编码类型进行读取
- 如果长度类型是有效长度,则根据长度信息读取字符串
REDIS_EXPIRETIME类型
- 如果一个key被expire设置过,那么在该key与value的前面会有一个REDIS_EXPIRETIME类型与其对应的值。
- REDIS_EXPIRETIME类型对应的值是过期时间点的timestamp
- REDIS_EXPIRETIME类型与其值是可选的,不是必须的,只有被expire设置过的key才有这个值
假设有一个key被expire命令设置过,把这REDIS_EXPIRETIME类型代入到上边的rdb文件的格式中,那么rdb文件的整体格式变成为:
文件签名 | 版本号 | REDIS_SELECTDB类型 | db编号 | REDIS_EXPIRETIME类型 | timestamp | 类型 | 值 | … | REDIS_SELECTD 类型 | db编号 | 类型 | 值 | … | REDIS_EOF类型
数据类型
数据类型主要有以下类型:
- REDIS_STRING类型
- REDIS_LIST类型
- REDIS_SET类型
- REDIS_ZSET类型
- REDIS_HASH类型
- REDIS_VMPOINTER类型
- REDIS_HASH_ZIPMAP类型
- REDIS_LIST_ZIPLIST类型
- REDIS_SET_INTSET类型
- REDIS_ZSET_ZIPLIST类型
其中REDIS_HASH_ZIPMAP,REDIS_LIST_ZIPLIST,REDIS_SET_INTSET和REDIS_ZSET_ZIPLIST这四种数据类型都是只在rdb文件中才有的类型,其他的数据类型其实就是val对象中type字段存储的值。
下边以REDIS_STRING类型和REDIS_LIST类型为例进行详解,其他类型都类似
REDIS_STRING类型
假设rdb文件中有一个值是REDIS_STRING类型,比如执行了一个set mykey myval命令,则在rdb文件表示为:
REDIS_STRING类型 | 值
其中值包含了key的长度,key的值,val的长度和val的值,把REDIS_STRING类型值的格式代入得:
REDIS_STRING类型 | keylen | mykey | vallen | myval
长度的存储格式见rdb中长度的存储
REDIS_LIST类型
1.List
REDIS_LIST | listlen | len | value | len | value
Listlen是链表长度
Len是链表结点的值value的长度
Value是链表结点的值
2.Ziplist
REDIS_ENCODING_ZIPLIST | ziplist
Ziplist就是通过字符串来实现的,直接将其存储于rdb文件中即可
快照保存
我们接下来看看具体实现细节
不管是触发条件满足后通过fork子进程来保存快照还是通过save命令来触发,其实都是调用的同一个函数rdbSave(rdb.c:394)。
先来看看触发条件满足后通过fork子进程的实现保存快照的的实现
在每100ms调用一次的serverCron函数中会对快照保存的条件进行检查,如果满足了则进行快照保存
Redis.c:604
/* Check if a background saving or AOF rewrite in progress terminated */ if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) { int statloc; pid_t pid; if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { if (pid == server.bgsavechildpid) { backgroundSaveDoneHandler(statloc); } … updateDictResizePolicy(); } } else { time_t now = time(NULL); /* If there is not a background saving in progress check if * we have to save now */ for (j = 0; j < server.saveparamslen; j++) { struct saveparam *sp = server.saveparams+j; if (server.dirty >= sp->changes && now-server.lastsave > sp->seconds) { redisLog(REDIS_NOTICE,”%d changes in %d seconds. Saving…”, sp->changes, sp->seconds); rdbSaveBackground(server.dbfilename); break; } } … } |
如果后端有写rdb的子进程或者写aof的子进程,则检查rdb子进程是否退出了,如果退出了则进行一些收尾处理,比如更新脏数据计数server.dirty和最近快照保存时间server.lastsave。
如果后端没有写rdb的子进程且没有写aof的子进程,则判断下是否有触发写rdb的条件满足了,如果有条件满足,则通过调用rdbSaveBackground函数进行快照保存。
跟着进rdbSaveBackground函数里边看看
Rdb.c:499
int rdbSaveBackground(char *filename) { pid_t childpid; long long start; if (server.bgsavechildpid != -1) return REDIS_ERR; if (server.vm_enabled) waitEmptyIOJobsQueue(); server.dirty_before_bgsave = server.dirty; start = ustime(); if ((childpid = fork()) == 0) { /* Child */ if (server.vm_enabled) vmReopenSwapFile(); if (server.ipfd > 0) close(server.ipfd); if (server.sofd > 0) close(server.sofd); if (rdbSave(filename) == REDIS_OK) { _exit(0); } else { _exit(1); } } else { /* Parent */ server.stat_fork_time = ustime()-start; if (childpid == -1) { redisLog(REDIS_WARNING,”Can’t save in background: fork: %s”, strerror(errno)); return REDIS_ERR; } redisLog(REDIS_NOTICE,”Background saving started by pid %d”,childpid); server.bgsavechildpid = childpid; updateDictResizePolicy(); return REDIS_OK; } return REDIS_OK; /* unreached */ } |
对是否已经有写rdb的子进程进行了判断,如果已经有保存快照的子进程,则返回错误。
如果启动了虚拟内存,则等待所有处理换出换入的任务线程退出,如果还有vm任务在处理就会一直循环等待。一直到所有换入换出任务都完成且所有vm线程退出。
保存当前的脏数据计数,当快照保存完后用于更新当前的脏数据计数(见函数backgroundSaveDoneHandler,rdb.c:1062)
记下当前时间,用于统计fork一个进程需要的时间
Fork一个字进程,子进程调用rdbSave进行快照保存
父进程统计fork一个子进程消耗的时间: server.stat_fork_time = ustime()-start,这个统计可以通过info命令获得。
保存子进程ID和更新增量重哈希的策略,即此时不应该再进行增量重哈希,不然大量key的改变可能导致fork的copy-on-write进行大量的写。
到了这里我们知道,rdb的快照保存是通过函数rdbSave函数(rdb.c:394)来实现的。其实save命令也是通过调用这个函数来实现的。我们来简单看看
Db.c:323
void saveCommand(redisClient *c) { if (server.bgsavechildpid != -1) { addReplyError(c,”Background save already in progress”); return; } if (rdbSave(server.dbfilename) == REDIS_OK) { addReply(c,shared.ok); } else { addReply(c,shared.err); } |
最后我们进rdbSave函数看看
rdb.c:394
int rdbSave(char *filename) { … /* Wait for I/O therads to terminate, just in case this is a * foreground-saving, to avoid seeking the swap file descriptor at the * same time. */ if (server.vm_enabled) waitEmptyIOJobsQueue(); snprintf(tmpfile,256,”temp-%d.rdb”, (int) getpid()); fp = fopen(tmpfile,”w”); if (!fp) { redisLog(REDIS_WARNING, “Failed saving the DB: %s”, strerror(errno)); return REDIS_ERR; } if (fwrite(“REDIS0002”,9,1,fp) == 0) 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 */ if (rdbSaveType(fp,REDIS_SELECTDB) == -1) goto werr; if (rdbSaveLen(fp,j) == -1) goto werr; /* Iterate this DB writing every entry */ while((de = dictNext(di)) != NULL) { sds keystr = dictGetEntryKey(de); robj key, *o = dictGetEntryVal(de); time_t expiretime; initStaticStringObject(key,keystr); expiretime = getExpire(db,&key); /* Save the expire time */ if (expiretime != -1) { /* If this key is already expired skip it */ if (expiretime < now) continue; if (rdbSaveType(fp,REDIS_EXPIRETIME) == -1) goto werr; if (rdbSaveTime(fp,expiretime) == -1) goto werr; } /* Save the key and associated value. This requires special * handling if the value is swapped out. */ if (!server.vm_enabled || o->storage == REDIS_VM_MEMORY || o->storage == REDIS_VM_SWAPPING) { int otype = getObjectSaveType(o); /* Save type, key, value */ if (rdbSaveType(fp,otype) == -1) goto werr; if (rdbSaveStringObject(fp,&key) == -1) goto werr; if (rdbSaveObject(fp,o) == -1) goto werr; } else { /* REDIS_VM_SWAPPED or REDIS_VM_LOADING */ robj *po; /* Get a preview of the object in memory */ po = vmPreviewObject(o); /* Save type, key, value */ if (rdbSaveType(fp,getObjectSaveType(po)) == -1) goto werr; if (rdbSaveStringObject(fp,&key) == -1) goto werr; if (rdbSaveObject(fp,po) == -1) goto werr; /* Remove the loaded object from memory */ decrRefCount(po); } } dictReleaseIterator(di); } /* EOF opcode */ if (rdbSaveType(fp,REDIS_EOF) == -1) goto werr; /* Make sure data will not remain on the OS’s output buffers */ fflush(fp); fsync(fileno(fp)); fclose(fp); /* Use RENAME to make sure the DB file is changed atomically only * if the generate DB file is ok. */ 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; server.lastsave = time(NULL); 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; } |
对是否有vm线程进行再次判断,因为如果是通过save命令过来的是没有判断过vm线程的。
创建并打开临时文件
写入文件签名“REDIS”和版本号“0002”
遍历所有db中的所有key
对每个key,先判断是否设置了expireTime, 如果设置了,则保存expireTime到rdb文件中。然后判断该key对应的value是否则内存中,如果是在内存中,则取出来写入到rdb文件中保存,如果被换出到虚拟内存了,则从虚拟内存读取然后写入到rdb文件中。
不同类型有有不同的存储格式,详细见rdb文件格式
最后写入rdb文件的结束符
关闭文件并重命名临时文件名到正式文件名
更新脏数据计数server.dirty为0和最近写rdb文件的时间server.lastsave为当前时间,这个只是在通过save命令触发的情况下有用。因为如果是通过fork一个子进程来写rdb文件的,更新无效,因为更新的是子进程的数据。
如果是通过fork一个子进程来写rdb文件(即不是通过save命令触发的),在写rdb文件的过程中,可能又有一些数据被更改了,那此时的脏数据计数server.dirty怎么更新呢? redis是怎样处理的呢?
我们来看看写rdb的子进程推出时得处理
Redis.c:605
if (server.bgsavechildpid != -1 || server.bgrewritechildpid != -1) { int statloc; pid_t pid; if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) { if (pid == server.bgsavechildpid) { backgroundSaveDoneHandler(statloc); } else { backgroundRewriteDoneHandler(statloc); } updateDictResizePolicy(); } } |
如果捕捉到写rdb文件的子进程退出,则调用backgroundSaveDoneHandler进行处理
接着看看backgroundSaveDoneHandler函数
Rdb.c:1062
void backgroundSaveDoneHandler(int statloc) { int exitcode = WEXITSTATUS(statloc); int bysignal = WIFSIGNALED(statloc); if (!bysignal && exitcode == 0) { redisLog(REDIS_NOTICE, “Background saving terminated with success”); server.dirty = server.dirty – server.dirty_before_bgsave; server.lastsave = time(NULL); } else if (!bysignal && exitcode != 0) { redisLog(REDIS_WARNING, “Background saving error”); } else { redisLog(REDIS_WARNING, “Background saving terminated by signal %d”, WTERMSIG(statloc)); rdbRemoveTempFile(server.bgsavechildpid); } server.bgsavechildpid = -1; /* Possibly there are slaves waiting for a BGSAVE in order to be served * (the first stage of SYNC is a bulk transfer of dump.rdb) */ updateSlavesWaitingBgsave(exitcode == 0 ? REDIS_OK : REDIS_ERR); } |
更新脏数据计数server.dirty为0和最近写rdb文件的时间server.lastsave为当前时间
唤醒因为正在保存快照而等待的slave,关于slave的具体内容,见replication
快照导入
当redis因为停电或者某些原因挂掉了,此时重启redis时,我们就需要从rdb文件中读取快照文件,把保存到rdb文件中的数据重新导入到内存中。
先来看看启动时对快照导入的处理
Redis.c:1717
if (server.appendonly) { if (loadAppendOnlyFile(server.appendfilename) == REDIS_OK) redisLog(REDIS_NOTICE,”DB loaded from append only file: %ld seconds”,time(NULL)-start); } else { if (rdbLoad(server.dbfilename) == REDIS_OK) { redisLog(REDIS_NOTICE,”DB loaded from disk: %ld seconds”, time(NULL)-start); } else if (errno != ENOENT) { redisLog(REDIS_WARNING,”Fatal error loading the DB. Exiting.”); exit(1); } } |
如果保存了AOF文件,则使用AOF文件来恢复数据,AOF的具体内容见AOF
如果没有AOF,则使用rdb文件恢复数据,调用rdbLoad函数
接着看看rdbLoad函数
Rdb.c:929
int rdbLoad(char *filename) { … fp = fopen(filename,”r”); if (!fp) { errno = ENOENT; return REDIS_ERR; } if (fread(buf,9,1,fp) == 0) goto eoferr; buf[9] = ‘ |