简介

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,再比较一下