redis源码分析与思考(十八)——RDB持久化

版权声明:博主GitHub地址https://github.com/suyeq欢迎大家前来交流学习 https://blog.csdn.net/hackersuye/article/details/83152833

    redis是一个键值对的数据库服务器,服务器中包含着若干个非空的数据库,每个非空数据库里又包含着若干个键值对。因为redis是一个基于内存存贮的数据库,他将自己所存的数据存于内存中,如果不将这些数据及时的保存在硬盘中,当电脑关机或者进行清除内存的操作时,redis保存的数据一定会发生丢失的状况,这对于数据库来说,是一个灾难性的问题。为了解决这个问题,redis提出了RDB持久化来及时的保存数据。

    RDB全称Redis DataBase,也就是redis数据库的意思。当redis关闭其服务器时,redis会自动启动RDB持久化,将内存中的键值对数据转化为一个RDB文件保存下来,当服务器重新启动的时候,服务器会把RDB文件转化成内存中的键值对。RDB持久化可以手动输入指令启用,也可以在服务器设置里面定期RDB持久化。

RDB持久化触发的方式

  1. 手动触发:在客户端键入SAVE命令或者BGSAVE命令,手动启用,而BGSAVE是开一个子线程,将RDB持久化放置在后台进行;
  2. 服务器关闭或者启动且AOF持久化未开启:服务器启动时且AOF持久化未开启式,会自动载入、写入RDB文件,进行数据库的恢复;
  3. 定期存贮:redis的时间事件会定期的处理数据库中的"dirty"数据;

RDB持久化与服务器

    SAVE命令执行的时候,redis服务器会被阻塞,同理,其它从客户端发来的请求都会被阻塞:

void saveCommand(redisClient *c) {
    //如果没有BGSAVE子线程执行中
    // BGSAVE 已经在执行中,不能再执行 SAVE
    // 否则将产生竞争条件
    if (server.rdb_child_pid != -1) {
       //返回客户端错误
        return;
    }
    // 执行 
    if (rdbSave(server.rdb_filename) == REDIS_OK)//返回客户端命令成功执行

    而BGSAVE命令不同,BGSAVE命令是在后台开启一个子线程,所以可以接受来自客户端的命令。BGSAVE命令与SAVE命令一样的是,在BASAVE命令执行的时候,再次接收到BGSAVE与SAVE命令会被默认为不执行,但是BGWRITEAOF命令会在执行完BGSAVE命令后执行,因为两者都是都子线程来执行的,逻辑代码如下:

void bgsaveCommand(redisClient *c) {
    // 不能重复执行 BGSAVE
    if (server.rdb_child_pid != -1) {
      //返回错误信息给客户端
    // 不能在 BGREWRITEAOF 正在运行时执行
    } else if (server.aof_child_pid != -1) {
      //返回错误信息给客户端
    // 执行 BGSAVE
    } else if (rdbSaveBackground(server.rdb_filename) == REDIS_OK) {
        //返回成功信息给客户端
    } else {
        //返回错误信息给客户端
    }
}

    在服务器中有几个定义与RDB持久化有关在这里列出来:

struct redisServer {
//.......
// 这个值为真时,表示服务器正在进行载入
    int loading; 
    //保存RDB保存的条件
    struct saveparam *saveparams; 
    // 自从上次 SAVE 执行以来,数据库被修改的次数
    long long dirty;
    //记录上次RDB持久化成功的时间       
    time_t lastsave;
//......// 服务器的保存条件(BGSAVE 自动执行的条件)
struct saveparam {
    // 多少秒之内
    time_t seconds;
    // 发生多少次修改
    int changes;
};

    loading属性是表示RDB持久化载入时的状态,当该属性为1的时候,表示正在RDB载入时的状态,这时候服务器是阻塞的。saveparams数组保存着多个RDB持久化保存的条件,其中只要有一个被满足,那么就进行持久化,在服务器中serverCron会检测其是否满足数组中的状态:

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
//......
// 遍历所有保存条件,看是否需要执行 BGSAVE 命令
         for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;
            // 检查是否有某个保存条件已经满足了
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 REDIS_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == REDIS_OK))
            {
             //......
                // 执行 BGSAVE
                rdbSaveBackground(server.rdb_filename);
                break;
            }
         }
//......
}

    dirty属性是记录在上次RDB持久化后对数据修改了的次数,而lastsave属性记录了上次RDB持久化完成的时间,是一个UNIX时间戳。

rio流

    在讨论RDB的文件结构以及其它时,先来了解一下rio流。什么是rio流呢?

rio.c is a simple stream-oriented I/O abstraction that provides an interface to write code that can consume/produce data using different concrete input and output devices.

    上述是redis作者的原话,翻译过来就是RIO 是一个自定义的可以面向流、可用于对多种不同的输入(目前是文件和内存字节)进行编程的抽象。简单来说rio流的作用是用来结构化读取和写入RDB文件。列出rio流的结构:

struct _rio {
    // API,读取、写入以及获取偏移量的函数
    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 *);
    // 校验和计算函数,每次有写入/读取新数据时都要计算一次
    void (*update_cksum)(struct _rio *, const void *buf, size_t len);
    // 当前校验和
    uint64_t cksum;
    /* 读入或者写入rio的字节数 */
    size_t processed_bytes;
    /* 每次最大读取和 写入的字节数*/
    size_t max_processing_chunk;
    /* Backend-specific vars. */
    union {
        struct {
            // 缓存指针
            sds ptr;
            // 偏移量
            off_t pos;
        } buffer;
        struct {
            // 被打开文件的指针
            FILE *fp;
            // 最近一次 fsync() 以来,写入的字节量
            off_t buffered; 
            // 写入多少字节之后,才会自动执行一次 fsync()
            off_t autosync; 
        } file;
    } io;
};

    cksum属性是用来校验数据的多少的,当完成RDB持久化时,服务器根据配置来决定是否启用校验函数来校验RDB文件数据的正确性。而io联合定义了一个缓冲区,以及一个文件,缓冲区的pos代表着从RDB文件中读取或者写入RDB文件中的进度,表示写入/读取了多少个字节,而文件则指向RDB文件,在这里设置同步属性是防止数据的写入丢失,因为操作系统在写入数据的时候,会创建一个缓冲区,当缓冲区里面的数据满了以后才会将数据从缓冲区里写入到文件中去。下面列出两个rio重要的函数,读取以及写入:

/*
 * 将 buf 中的 len 字节写入到 r 中。
 * 写入成功返回实际写入的字节数,写入失败返回 -1 。
 */
static inline size_t rioWrite(rio *r, const void *buf, size_t len) {
    while (len) {
        size_t bytes_to_write = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
        if (r->update_cksum) r->update_cksum(r,buf,bytes_to_write);
        if (r->write(r,buf,bytes_to_write) == 0)
            return 0;
        //buf指针往前移动bytes_to_write个字节,
        buf = (char*)buf + bytes_to_write;
        len -= bytes_to_write;
        r->processed_bytes += bytes_to_write;
    }
    return 1;
}

/*
 * 从 r 中读取 len 字节,并将内容保存到 buf 中。
 * 读取成功返回 1 ,失败返回 0 。
 */
static inline size_t rioRead(rio *r, void *buf, size_t len) {
    while (len) {
        size_t bytes_to_read = (r->max_processing_chunk && r->max_processing_chunk < len) ? r->max_processing_chunk : len;
        if (r->read(r,buf,bytes_to_read) == 0)
            return 0;
        if (r->update_cksum) r->update_cksum(r,buf,bytes_to_read);
        buf = (char*)buf + bytes_to_read;
        len -= bytes_to_read;
        r->processed_bytes += bytes_to_read;
    }
    return 1;
}

    rio流有几点好处,第一就是可以直接得到写入与读取的进度,第二点就是可以同时对内存或文件的RDB数据进行读写。

RDB文件

    RDB文件是一个压缩过的二进制文件,它与压缩列表以及整数集合一样,分为几大部分,如图示:
在这里插入图片描述
    1. REDIS部分长度为5个字节,保存着"REDIS"五个字符,通过这部分,程序可判断出该文件是否是RDB文件。
    2. RDB_VERSION部分长度为4个字节,这一部分保存着RDB文件的版本号。
    3. DATABASE部分对应着各个数据库,按顺序排列,保存着数据库中的数据,长度不定。
    4. EOF部分代表着程序读到这一块的时候,表示已经没有数据库的数据可读了,占一个字节。
    5. CHECK_SUM部分是用来检验的,当RDB文件载入时,会将载入文件计算得出的校验和与该部分的数据进行对比,以此来确认RDB文件是否损坏,占8个字节。
    上述分析可以从rdb.c/rdbSave函数即RDB写入函数得出:

int rdbSave(char *filename) {

    ......
  
    // 设置校验和函数
    if (server.rdb_checksum)
        rdb.update_cksum = rioGenericUpdateChecksum;
    // 写入 REDIS部分以及RDB_VERSIOn部分
    snprintf(magic,sizeof(magic),"REDIS%04d",REDIS_RDB_VERSION);
    if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
    // 遍历所有数据库
    for (j = 0; j < server.dbnum; j++) {
       //写入DATABASES部分
    }
    
    .......
  
    /* 
     * 写入 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;
    }
    
    .......
    
}

    我们着重讲解分析DATABASES部分,该部分保存每个数据库的部分又分为三个部分,如图示:
在这里插入图片描述
    1. SELECTDB部分占一个字节,这个部分的意义在于当RDB文件载入的时候,程序读到该值,就会明白它下一个字节或者几个字节是保存着数据库的号码的。该值以及其它的重要的值宏定义如下:

/*
 * 数据库特殊操作标识符
 */
// 以 MS 计算的过期时间
#define REDIS_RDB_OPCODE_EXPIRETIME_MS 252
// 以秒计算的过期时间
#define REDIS_RDB_OPCODE_EXPIRETIME 253
// 选择数据库
#define REDIS_RDB_OPCODE_SELECTDB   254
// 数据库的结尾(但不是 RDB 文件的结尾)
#define REDIS_RDB_OPCODE_EOF        255

    2. DB_NUMBER部分是保存着数据库的号码,它视自身的大小占1个字节、2个字节或者5个字节,关于它如何的判断其占几个字节,是属于值的长度编码,放在下面讲解,在RDB写入函数写入DATABASE部分可以找到该段代码:

 /* 
  * 写入 DB 选择器
  */
//写入
  if (rdbSaveType(&rdb,REDIS_RDB_OPCODE_SELECTDB) == -1) goto werr;
  if (rdbSaveLen(&rdb,j) == -1) goto werr;

    3. KEY_VALUE_PAIRS是保存数据的地方,我们要先明白一点,不管是上面所说的,还是接下来所描述的,它们都是二进制存贮的,而且redis中所有的键值对,不管5种对象里面的哪个对象,究其根本,都保存着的是字符串对象,所以RDB持久化是基于以字符串对象来保存数据的。它分为5部分:
在这里插入图片描述
    前两个部分只有设置了过期时间的键才有,后面三个部分是通用的。
    EXPIRETIME部分占一个字节,用来在RDB载入时提示后面8个字节表示是键的过期时间。
    MS部分占8个字节,用来描述键的过期时间。TYPE部分占一个字节,用来表达保存的键是5种类型中的哪一种。
    KEY部分保存键,也就是保存了一个字符串对象,VALUES部分保存了5种对象的数据。如下代码所示:

/*
 * @param rdb rio
 * @param key 键对象
 * @param val 值对象
 * @param expiretime 过期时间
 * @param now 现在时间
 * @return 
 */
int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val,
                        long long expiretime, long long now)
{
     //保存键的过期时间
    if (expiretime != -1) {
         // 不写入已经过期的键
        if (expiretime < now) return 0;
        if (rdbSaveType(rdb,REDIS_RDB_OPCODE_EXPIRETIME_MS) == -1) return -1;
        if (rdbSaveMillisecondTime(rdb,expiretime) == -1) return -1;
    }
     // 保存类型,键,值
    if (rdbSaveObjectType(rdb,val) == -1) return -1;
    if (rdbSaveStringObject(rdb,key) == -1) return -1;
    if (rdbSaveObject(rdb,val) == -1) return -1;
    return 1;
}

值的编码

    不管是键对象,还是值对象,都要进行特定得到编码才能存贮进RDB文件,在上文中提到RDB文件保存的是字符串,所以任何的对象都要转换成字符串来保存,值的编码种类有三种:整数类型、未压缩的字符串类型以及压缩过的字符串类型。在详细讲解这三种类型时,先来看看几个封装了rio流的重要的函数:

/*
 * 将长度为 len 的字符数组 p 写入到 rdb 中。
 * 写入成功返回 len ,失败返回 -1 。
 */
static int rdbWriteRaw(rio *rdb, void *p, size_t len) {
    if (rdb && rioWrite(rdb,p,len) == 0)
        return -1;
    return len;
}

 // 将长度为 1 字节的字符 type 写入到 rdb 文件中。
int rdbSaveType(rio *rdb, unsigned char type) {
    return rdbWriteRaw(rdb,&type,1);
}

/*
 * 函数即可以用于载入键的类型(rdb.h/REDIS_RDB_TYPE_*),
 * 也可以用于载入特殊标识号(rdb.h/REDIS_RDB_OPCODE_*)
 */
int rdbLoadType(rio *rdb) {
    unsigned char type;
    if (rioRead(rdb,&type,1) == 0) return -1;
    return type;
}

    再来看看对长度的编码,长度编码的设计是因为字符串不能明确的表示在二进制中的界限,也就是为了防止读取混乱设计的,长度的编码不定,有1个字节,2个字节以及5个字节,长度编码的种类:

#define REDIS_RDB_6BITLEN 0
#define REDIS_RDB_14BITLEN 1
#define REDIS_RDB_32BITLEN 2

    分别对应:


编码种类 对应格式
REDIS_RDB_6BITLEN 00******,前两位表示哪种种类的编码,后6位保存保存字符串需要的字节多少
REDIS_RDB_14BITLEN 01****** ********,前两位表示哪种种类的编码,后14位保存保存字符串需要的字节多少
REDIS_RDB_32BITLEN 10****** [**…总共32位],前两位表示哪种种类的编码,后32位保存保存字符串需要的字节多少

    对长度编码的转换代码如下:

//返回需要长度编码所占的字节数
int rdbSaveLen(rio *rdb, uint32_t len) {
    unsigned char buf[2];
    size_t nwritten;
    //第一个buf前两位存贮编码格式,后面跟着的字节保存需要保存字符串的字节数多少
    if (len < (1<<6)) {
        /* Save a 6 bit len */
        buf[0] = (len&0xFF)|(REDIS_RDB_6BITLEN<<6);
        if (rdbWriteRaw(rdb,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(rdb,buf,2) == -1) return -1;
        nwritten = 2;
    } else {
        /* Save a 32 bit len */
        buf[0] = (REDIS_RDB_32BITLEN<<6);
        if (rdbWriteRaw(rdb,buf,1) == -1) return -1;
        //本地字节顺序转化为网络字节序列
        len = htonl(len);
        if (rdbWriteRaw(rdb,&len,4) == -1) return -1;
        nwritten = 1+4;
    }
    return nwritten;
}

整数编码

    整数编码保存的格式有两部分,ENCODING部分与INTEGER部分,ENCODING部分保存整数的编码格式,占一个字节,INTERGER部分保存值,占1个、2个、5个字节不等。编码格式有三种:

#define REDIS_RDB_ENC_INT8 0        /* 8 bit signed integer */
#define REDIS_RDB_ENC_INT16 1       /* 16 bit signed integer */
#define REDIS_RDB_ENC_INT32 2       /* 32 bit signed integer */

    8位整数、16位整数以及32位整数,从客户端传来的整数会被默认为long long的类型,这时,需要多整数的编码转换,使其节约空间,因为假如整数值是1,那么保存它只需要短短的一个位,但是默认保存却需要32位来保存,其它的31位就浪费了,整数的编码转换就是完成这个任务的,其对应表如下:


编码种类 对应格式
REDIS_RDB_ENC_INT8 11000000 ********,前一节保存编码种类,后一字节保存值
REDIS_RDB_ENC_INT6 11000001 ******** ********,前一节保存编码种类,后2字节保存值
REDIS_RDB_ENC_INT32 11000010 ******…总共32位,前一节保存编码种类,后4字节保存值

    转换代码如下:

#define REDIS_RDB_ENCVAL 3
int rdbEncodeInteger(long long value, unsigned char *enc) {
    if (value >= -(1<<7) && value <= (1<<7)-1) {
        enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT8;
        enc[1] = value&0xFF;
        return 2;
    } else if (value >= -(1<<15) && value <= (1<<15)-1) {
        enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT16;
        enc[1] = value&0xFF;
        enc[2] = (value>>8)&0xFF;
        return 3;
    } else if (value >= -((long long)1<<31) && value <= ((long long)1<<31)-1) {
        enc[0] = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_INT32;
        enc[1] = value&0xFF;
        enc[2] = (value>>8)&0xFF;
        enc[3] = (value>>16)&0xFF;
        enc[4] = (value>>24)&0xFF;
        return 5;
    } else {
        return 0;
    }
}

    虽然保存一个字节的数还要用一个字节来保存其类型,但是相比原来的4个字节数已将好很多了。

压缩的字符串编码

    未压缩的字符串编码在此不讲,与整数编码很相似,分为两个部分,第一个部分用一个字节来存贮保存字符串需要的字节数,第二部分保存字符串的值。而是否压缩的界限是当字符串的长度超过了20个字节时,就需要采用压缩:

int rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {
  
  ......
  
    /* 
     * 如果字符串长度大于 20 ,并且服务器开启了 LZF 压缩,
     * 那么在保存字符串到数据库之前,先对字符串进行 LZF 压缩。
     */
    if (server.rdb_compression && len > 20) {
        // 尝试压缩
        n = rdbSaveLzfStringObject(rdb,s,len);

        if (n == -1) return -1;
        if (n > 0) return n;
      
      ......
}

    压缩后的字符串编码分为4个部分:
    1.REDIS_RDB_ENC_LZF:该部分占一个字节,是一个常量,表示该字符串已经被压缩过了,该常量值如下:

#define REDIS_RDB_ENC_LZF 3    

    2.COMPRESSED_LEN:该部分表示压缩过后的字符串长度,所占字节由长度编码决定。
    3.ORIGIN_LEN:该部分表示被压缩之前字符串的长度,所占字节由长度编码决定。
    4.COMPRESSED_STRING:该部分表示被压缩过后的字符串。
    在尝试压缩的函数里,我们可以得出这个结论:

/*
 * 尝试对输入字符串 s 进行压缩,
 * 如果压缩成功,那么将压缩后的字符串保存到 rdb 中。
 * 函数在成功时返回保存压缩后的 s 所需的字节数,
 * 压缩失败或者内存不足时返回 0 ,
 * 写入失败时返回 -1 。
 */
int rdbSaveLzfStringObject(rio *rdb, unsigned char *s, size_t len) {
    size_t comprlen, outlen;
    unsigned char byte;
    int n, nwritten = 0;
    void *out;
    //LZF算法至少的初始化字符串长度要大于4
    // 压缩字符串
    if (len <= 4) return 0;
    outlen = len-4;
    if ((out = zmalloc(outlen+1)) == NULL) return 0;
    comprlen = lzf_compress(s, len, out, outlen);
    if (comprlen == 0) {
        zfree(out);
        return 0;
    }
     // 保存压缩后的字符串到 rdb 。
    // 写入类型,说明这是一个 LZF 压缩字符串
    byte = (REDIS_RDB_ENCVAL<<6)|REDIS_RDB_ENC_LZF;
    if ((n = rdbWriteRaw(rdb,&byte,1)) == -1) goto writeerr;
    nwritten += n;
    // 写入字符串压缩后的长度
    if ((n = rdbSaveLen(rdb,comprlen)) == -1) goto writeerr;
    nwritten += n;
    // 写入字符串未压缩时的长度
    if ((n = rdbSaveLen(rdb,len)) == -1) goto writeerr;
    nwritten += n;
    // 写入压缩后的字符串
    if ((n = rdbWriteRaw(rdb,out,comprlen)) == -1) goto writeerr;
    nwritten += n;
    zfree(out);
    return nwritten;
writeerr:
    zfree(out);
    return -1;
}

    整数编码与字符串编码之间存在着转换,当整数超过了最大整数时,32位已经无法保存该数,所以采取字符串编码保存,对整数编码的完整流程代码如下:

int rdbSaveLongLongAsStringObject(rio *rdb, long long value) {
    unsigned char buf[32];
    int n, nwritten = 0;
    // 尝试以节省空间的方式编码整数值 value 
    int enclen = rdbEncodeInteger(value,buf);
    // 编码成功,直接写入编码后的缓存
    // 比如,值 1 可以编码为 11 00 0001
    if (enclen > 0) {
        return rdbWriteRaw(rdb,buf,enclen);
    // 编码失败,将整数值转换成对应的字符串来保存
    // 比如,值 999999999 要编码成 "999999999" ,
    // 因为这个值没办法用节省空间的方式编码
    } else {
        // 转换成字符串表示
        enclen = ll2string((char*)buf,32,value);
        redisAssert(enclen < 32);
        // 写入字符串长度
        if ((n = rdbSaveLen(rdb,enclen)) == -1) return -1;
        nwritten += n;
        // 写入字符串
        if ((n = rdbWriteRaw(rdb,buf,enclen)) == -1) return -1;
        nwritten += n;
    }
    // 返回长度
    return nwritten;
}

    而同样,字符串在满足可以转换为整数的情况下,会采用整数来保存,字符串编码的完成流程代码如下:

int rdbSaveRawString(rio *rdb, unsigned char *s, size_t len) {
    int enclen;
    int n, nwritten = 0;
     // 尝试进行整数值编码
    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;
        }
    }
    /* 
     * 如果字符串长度大于 20 ,并且服务器开启了 LZF 压缩,
     * 那么在保存字符串到数据库之前,先对字符串进行 LZF 压缩。
     */
    if (server.rdb_compression && len > 20) {
        // 尝试压缩
        n = rdbSaveLzfStringObject(rdb,s,len);
        if (n == -1) return -1;
        if (n > 0) return n;
    }
    // 执行到这里,说明值 s 既不能编码为整数
    // 也不能被压缩
    // 那么直接将它写入到 rdb 中
    /* 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;
}

    关于加载RDB文件本篇不做阐述,判断其编码格式,与所占内存的大小就可以了,而浮点数也是转换为字符串编码存贮的。

字符串对象的存贮

    压缩列表实现的列表、哈希表以及整数集合实现的集合可以用直接用字符串存贮,因为它们本质上就是一个字符串,字符串存贮的代码:

int rdbSaveStringObject(rio *rdb, robj *obj) {
    // 尝试对 INT 编码的字符串进行特殊编码
    if (obj->encoding == REDIS_ENCODING_INT) {
        return rdbSaveLongLongAsStringObject(rdb,(long)obj->ptr);
    // 保存 STRING 编码的字符串
    } else {
        redisAssertWithInfo(NULL,obj,sdsEncodedObject(obj));
        return rdbSaveRawString(rdb,obj->ptr,sdslen(obj->ptr));
    }
}

其它编码格式对象的存贮

    这里之所说其它编码格式对象的存贮,是因为其原理是一样的,就拿链表实现的列表来说吧,组成分为两部分,第一部分保存节点数的多少,所占字节由长度编码计算得出,第二部分保存若干个列表节点,也就是若干个字符串对象,代码如下:

 if (o->type == REDIS_LIST) {  
        if (o->encoding == REDIS_ENCODING_ZIPLIST) {
           
           ......
           
        } else if (o->encoding == REDIS_ENCODING_LINKEDLIST) {
            list *list = o->ptr;
            listIter li;
            listNode *ln;

            if ((n = rdbSaveLen(rdb,listLength(list))) == -1) return -1;
            nwritten += n;
            // 遍历所有列表项
            listRewind(list,&li);
            while((ln = listNext(&li))) {
                robj *eleobj = listNodeValue(ln);
                // 以字符串对象的形式保存列表项
                if ((n = rdbSaveStringObject(rdb,eleobj)) == -1) return -1;
                nwritten += n;
            }
        } else {
            redisPanic("Unknown list encoding");
        }
    // 保存集合对象
    } 
    
    ......
    

总结

1. RDB文件是一个二进制的压缩文件,由多个部分组成,用于保存和恢复数据;
2. SAVE会阻塞服务器,而BGSAVE则不会;
3. 对于不同的键值类型,redis会采用不同的方式来存贮。

猜你喜欢

转载自blog.csdn.net/hackersuye/article/details/83152833
今日推荐