简介
RDB是另一种持久化方式,RDB是把内存中的数据整个保存到文件
一般不会单独使用RDB这种方式
redis5对AOF重写做了优化,重写时使用RDB格式,重写完再追加AOF,相当于是混合模式
源码
和aof一样要用到 rio 结构体
rio.h
struct _rio {
/* Backend functions.
* Since this functions do not tolerate short writes or reads the return
* value is simplified to: zero on error, non zero on complete success. */
size_t (*read)(struct _rio *, void *buf, size_t len);
size_t (*write)(struct _rio *, const void *buf, size_t len);
off_t (*tell)(struct _rio *);
int (*flush)(struct _rio *);
/* The update_cksum method if not NULL is used to compute the checksum of
* all the data that was read or written so far. The method should be
* designed so that can be called with the current checksum, and the buf
* and len fields pointing to the new block of data to add to the checksum
* computation. */
void (*update_cksum)(struct _rio *, const void *buf, size_t len);
/* The current checksum */
uint64_t cksum;
/* number of bytes read or written */
size_t processed_bytes;
/* maximum single read or write chunk size */
size_t max_processing_chunk;
/* Backend-specific vars. */
union {
/* In-memory buffer target. */
struct {
sds ptr;
off_t pos;
} buffer;
/* Stdio file pointer target. 本次使用这个 */
struct {
FILE *fp;
off_t buffered; /* Bytes written since last fsync. */
off_t autosync; /* fsync after 'autosync' bytes written. */
} file;
/* Multiple FDs target (used to write to N sockets). */
struct {
int *fds; /* File descriptors. */
int *state; /* Error state of each fd. 0 (if ok) or errno. */
int numfds;
off_t pos;
sds buf;
} fdset;
} io;
};
typedef struct _rio rio;
配置rdb_checksum,表示rdb文件需不需要checksum,默认开启,redisServer->int rdb_checksum
开启的情况下,rio的update_cksum函数指针,指向rio.c -> void rioGenericUpdateChecksum(rio *r, const void *buf, size_t len)
/* This function can be installed both in memory and file streams when checksum
* computation is needed. */
void rioGenericUpdateChecksum(rio *r, const void *buf, size_t len) {
r->cksum = crc64(r->cksum,buf,len);
}
每次调用rioWrite的时候都会更新 rio->cksum
写入函数
rdbWriteRaw
就是调用rioWrite
rdbSaveType
int rdbSaveType(rio *rdb, unsigned char type) {
return rdbWriteRaw(rdb,&type,1);
}
很简单,就是写入一个字节
rdbSaveRawString
ssize_t rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {
int enclen;
ssize_t n, nwritten = 0;
/* Try integer encoding */
if (len <= 11) {
unsigned char buf[5];
if ((enclen = rdbTryIntegerEncoding((char*)s,len,buf)) > 0) {
if (rdbWriteRaw(rdb,buf,enclen) == -1) return -1;
return enclen;
}
}
/* Try LZF compression - under 20 bytes it's unable to compress even
* aaaaaaaaaaaaaaaaaa so skip it */
if (server.rdb_compression && len > 20) {
n = rdbSaveLzfStringObject(rdb,s,len);
if (n == -1) return -1;
if (n > 0) return n;
/* Return value of 0 means data can't be compressed, save the old way */
}
/* Store verbatim */
if ((n = rdbSaveLen(rdb,len)) == -1) return -1;
nwritten += n;
if (len > 0) {
if (rdbWriteRaw(rdb,s,len) == -1) return -1;
nwritten += len;
}
return nwritten;
}
保存字符串,可以转换为数字 或者 lzf压缩,以节省空间
- 长度小于12,转换为数字
调用strtoll尝试转为数字,并且需要 使用ll2string转回去是完全一致的
实际上,只有当这个字符串表示的数字,可以用4个字节来表示时才可以转,包括了负数;因此,可以转换的范围是 -2^31 到 2^31-1
保存时,还需要多一个字节来表示长度
110000xx
高2位为11,表示这个东西本来是string;低二位就是表示长度:00一个字节 01两个字节 10四个字节
- lzf,长度需大于20
首先写入数字时(int rdbSaveLen(rio *rdb, uint64_t len)),第一个字节的高2位表示长度
00表示6位长度,也就是总共1个字节
01表示14位长度,总共2个字节
10000000表示4个字节,从下一个字节开始
10000001表示8个字节,从下一个字节开始
回到lzf,lzf数据,第一个字节为固定11000011标识,然后是压缩后的长度(数字),然后是原始长度(数字),然后就是压缩的数据
- 以上条件都不满足,直接写入字符串
首先写入长度(数字),然后写入内容
总结一下,普通字符串:先写入长度(数字),后写入内容
转为数字的字符串:11开头,后面是数字
lzf压缩的字符串:11000011开头,压缩后长度、原始长度、压缩内容
有了上面的规则,我们可以猜测恢复数据的逻辑
首先判断第一个字节的高2位,如果是11,说明是编码成数字的字符串 或者 lzf
再判断低二位如果也是11,那就是lzf;否则是第一种情况
如果高2位不是11,那就是普通字符串
rdbSaveAuxField
有了前面的底层函数,接下来就是组合使用
//保存aux类型的 key value
//先写入一字节250,然后写入key value
#define RDB_OPCODE_AUX 250 /* RDB aux field. */
ssize_t rdbSaveAuxField(rio *rdb, void *key, size_t keylen, void *val, size_t vallen) {
ssize_t ret, len = 0;
if ((ret = rdbSaveType(rdb,RDB_OPCODE_AUX)) == -1) return -1;
len += ret;
if ((ret = rdbSaveRawString(rdb,key,keylen)) == -1) return -1;
len += ret;
if ((ret = rdbSaveRawString(rdb,val,vallen)) == -1) return -1;
len += ret;
return len;
}
strstr和strint
/* Wrapper for rdbSaveAuxField() used when key/val length can be obtained
* with strlen(). */
ssize_t rdbSaveAuxFieldStrStr(rio *rdb, char *key, char *val) {
return rdbSaveAuxField(rdb,key,strlen(key),val,strlen(val));
}
/* Wrapper for strlen(key) + integer type (up to long long range). */
ssize_t rdbSaveAuxFieldStrInt(rio *rdb, char *key, long long val) {
char buf[LONG_STR_SIZE];
int vlen = ll2string(buf,sizeof(buf),val);
return rdbSaveAuxField(rdb,key,strlen(key),buf,vlen);
}
再包了一层
RDB流程
现在来看一遍RDB的流程
RDB的触发还是在redis的定时任务serverCron里
for (j = 0; j < server.saveparamslen; j++) {
//saveparam表示配置文件里的 save 300 10 之类的
//saveparam->changes = 10
//saveparam->seconds = 300
struct saveparam *sp = server.saveparams+j;
/* Save if we reached the given amount of changes,
* the given amount of seconds, and if the latest bgsave was
* successful or if, in case of an error, at least
* CONFIG_BGSAVE_RETRY_DELAY seconds already elapsed. */
//判断是否满足其中一个条件
//由这里可以推断,rdb完成后,server.dirty = 0
//server.lastbgsave就是上一次RDB完成的时间戳
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))
{
serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
sp->changes, (int)sp->seconds);
rdbSaveInfo rsi, *rsiptr;
rsiptr = rdbPopulateSaveInfo(&rsi);
rdbSaveBackground(server.rdb_filename,rsiptr);
break;
}
}
rdbSaveInfo,和replication同步有关
typedef struct rdbSaveInfo {
/* Used saving and loading. */
int repl_stream_db; /* DB to select in server.master client. */
/* Used only loading. */
int repl_id_is_set; /* True if repl_id field is set. */
char repl_id[CONFIG_RUN_ID_SIZE+1]; /* Replication ID. */
long long repl_offset; /* Replication offset. */
} rdbSaveInfo;
rdbPopulateSaveInfo这个函数先跳过,只需要知道它初始化了rdbSaveInfo结构体就行了
rdbSave
和aof重写一样,需要开启子进程,以下的操作均在子进程
首先初始化rio结构体,就是保存一下fd,和file.autosync
现在真正开始写文件了
- rdbWriteRaw(rio,"REDIS{VERSION}",9)
首先写入REDIS版本号,因为不同版本的RDB文件格式有差异,需要标记
- rdbSaveInfoAuxFields(rio,0,rsi)
调用rdbSaveAuxField写入key value,包括:
key | value |
redis-ver | redis版本 |
redis-bits | 一个指针占多少位,32或者64 |
ctime | 时间戳 |
used-mem | zmalloc_used_memory() 已分配内存 |
aof-preamble | 是否为混合格式,由第二个参数带入 |
repl-stream-db | rdbSaveInfo字段 |
repl-id | rdbSaveInfo字段 |
repl-offset | rdbSaveInfo字段 |
- 写入db
遍历所有db,for(j : server.dbnum)
define RDB_OPCODE_SELECTDB 254 /* DB number of the following keys. */
首先写入一字节RDB_OPCODE_SELECTDB,再写入数字j,表示当前第j个db
define RDB_OPCODE_RESIZEDB 251 /* Hash table resize hint. */
写入一字节RDB_OPCODE_RESIZEDB,再写入db->dict的大小,再写入db->expires的大小
然后就是写入dict的所有key value
用到函数:int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime)
rdbSaveKeyValuePair
expiretime为过期时间,没有则为-1
如果有过期时间,写入#define RDB_OPCODE_EXPIRETIME_MS 252 /* Expire time in milliseconds. */,然后直接写入8字节过期时间,和前面不一样,这里不需要一字节的前缀
考虑到LRU,就是每个redisObject会保存最后一次访问的时间戳,这个值也需要保存,因此需要保存这个值相对当前时间的差
写入一字节#define RDB_OPCODE_IDLE 248 /* LRU idle time. */,然后写入数字(LRU_CLOCK() - o->lru)
redisObject是有不同的类型,所以需要一个标志
rdb.h
#define RDB_TYPE_STRING 0
#define RDB_TYPE_LIST 1
#define RDB_TYPE_SET 2
#define RDB_TYPE_ZSET 3
#define RDB_TYPE_HASH 4
#define RDB_TYPE_ZSET_2 5 /* ZSET version 2 with doubles stored in binary. */
#define RDB_TYPE_MODULE 6
#define RDB_TYPE_MODULE_2 7 /* Module value with annotations for parsing without
the generating module being loaded. */
/* Object types for encoded objects. */
#define RDB_TYPE_HASH_ZIPMAP 9
#define RDB_TYPE_LIST_ZIPLIST 10
#define RDB_TYPE_SET_INTSET 11
#define RDB_TYPE_ZSET_ZIPLIST 12
#define RDB_TYPE_HASH_ZIPLIST 13
#define RDB_TYPE_LIST_QUICKLIST 14
#define RDB_TYPE_STREAM_LISTPACKS 15
先写入一字节类型
然后写入字符串Key
然后写入value,由于value有很多种类型,需要分开讨论
- OBJ_STRING
直接按字符串那样保存
- OBJ_LIST
前面讲过,redis的list只有quicklist一种,quicklist是个链表,链表元素是ziplist
先写入链表的元素数量,然后遍历元素,如果元素没有压缩,直接把ziplist,按字符串那样写入(实际上为写入字节流);如果压缩过,按照前面讲的lzf方式写入存储
- OBJ_SET
SET有2中存储形式,OBJ_ENCODING_HT 和 OBJ_ENCODING_INTSET
OBJ_ENCODING_HT的话,先写入长度,再写入所有key string
OBJ_ENCODING_INTSET,则直接当成字节流写入
- OBJ_ZSET
OBJ_ENCODING_ZIPLIST,直接写入字节流
OBJ_ENCODING_SKIPLIST,先写入长度,从尾部开始遍历,写入字符串和score值,score值为double,直接写入
- OBJ_HASH
OBJ_ENCODING_ZIPLIST,字节流写入
OBJ_ENCODING_HT,先写入长度,再写入所有key value
rdbSaveKeyValuePair结束
写入完成
这些都写完之后,最后写入一字节#define RDB_OPCODE_EOF 255 /* End of the RDB file. */
如果开启了checksum,再写入rio->cksum
之后
fflush
fsync
fclose
//由于在子进程中,修改的是子进程的变量,实际上这些修改并不会影响到父进程
//如果执行SAVE,直接在redis进程执行RDB,不开子进程,这时这里的修改就会有效;然而没人会这样干
server.dirty = 0;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
与aof重写一样,子进程在最后会通过管道,给父进程发送childInfo
rdbSaveBackground
现在可以来看这个函数
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
openChildInfoPipe();
fork:
//子进程
//写入RDB文件
//发送childInfo
//exit
//父进程
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
updateDictResizePolicy();
}
父进程知道RDB完成
同样是在serverCron中定时检测,最终调用void backgroundSaveDoneHandlerDisk(int exitcode, int bysignal)
//server.dirty_before_bgsave在fork之前保存server.dirty
server.dirty = server.dirty - server.dirty_before_bgsave;
server.lastsave = time(NULL);
server.lastbgsave_status = C_OK;
server.rdb_child_pid = -1;
server.rdb_child_type = RDB_CHILD_TYPE_NONE;
server.rdb_save_time_last = time(NULL)-server.rdb_save_time_start;
server.rdb_save_time_start = -1;
保存一下完成时间戳,状态
加载RDB文件
redis启动时会加载AOF或者RDB
在server.c的main函数会调用loadDataFromDisk,在进入多路复用之前调用
server.c
void loadDataFromDisk(void) {
long long start = ustime();
if (server.aof_state == AOF_ON) {
if (loadAppendOnlyFile(server.aof_filename) == C_OK)
serverLog(LL_NOTICE,"DB loaded from append only file: %.3f seconds",(float)(ustime()-start)/1000000);
} else {
rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
if (rdbLoad(server.rdb_filename,&rsi) == C_OK) {
..........
}
}
}
如果aof开启,优先加载AOF,忽略RDB
rdbLoad就是按照前面存入的格式,一个一个的读出来,这个没什么难点
最后如果开启了checksum校验 并且 RDB文件有checksum,再比较一下