设为首页 收藏本站
查看: 1565|回复: 0

[经验分享] Redis源码分析(十四)--- rdb.c本地数据库操作

[复制链接]
累计签到:1 天
连续签到:1 天
发表于 2014-11-5 09:17:23 | 显示全部楼层 |阅读模式
       过去2,3天内把redis内部的测试相关包分析了一遍,总体感觉还是比较容易的,总共5个文件,也让我们涨了一下见识,什么叫内置的测试函数。今天,我把目标进行了转移,下面我准备继续学习与代码逻辑稍稍无关的模块,数据层,在我的分类中,就是在Data的文件包。在这个里面,首当其冲,我研究了rdb.c,直接与数据库操作相关。什么叫数据库操作相关呢,最直接的意思就是,数据库的相关操作到最后到会直接映射到这个文件中的函数操作。所以在理解这些操作之前,我得先介绍一下里面的一些东西,免得会比较乱。我们知道,redis内部支持很多中类型,

1.list列表

2.hash类型

3.set类型

4.string类型

其中如果是list列表类型,其实内部的编码方式又可分为2种,linkedList普通链表模式,ziplist压缩列表模式,所以说里面的代码里的类型是非常多的,所以建议读者阅读学习源码的时候,不要搞混了。rdb中的数据存储的基本格式为[len][data],前面使用字节表示的长度,后面是真实的数据,当然我这说的是普通的字符串类型的key:value的值,如果是纯数字,直接用字节表示值,根据值的大小分配不同的字节表示,不得不说,redis在数据存储方面上,把数据存储的内存消耗降到了极致。比如只要是在数据库中保存的长度等数字的,必须经过计算判断,然后再分配相应的字节保存(跟前面压缩列表等的原理类型):



    /* Load an encoded length. The "isencoded" argument is set to 1 if the length
     * is not actually a length but an "encoding type". See the REDIS_RDB_ENC_*
     * definitions in rdb.h for more information. */  
    /* 加载长度,也需要根据编码方式,读取不同的buf获取长度 */  
    uint32_t rdbLoadLen(rio *rdb, int *isencoded) {  
        unsigned char buf[2];  
        uint32_t len;  
        int type;  
      
        if (isencoded) *isencoded = 0;  
        if (rioRead(rdb,buf,1) == 0) return REDIS_RDB_LENERR;  
        type = (buf[0]&0xC0)>>6;  
        if (type == REDIS_RDB_ENCVAL) {  
            /* Read a 6 bit encoding type. */  
            if (isencoded) *isencoded = 1;  
            return buf[0]&0x3F;  
        } else if (type == REDIS_RDB_6BITLEN) {  
            /* Read a 6 bit len. */  
            return buf[0]&0x3F;  
        } else if (type == REDIS_RDB_14BITLEN) {  
            /* Read a 14 bit len. */  
            if (rioRead(rdb,buf+1,1) == 0) return REDIS_RDB_LENERR;  
            return ((buf[0]&0x3F)<<8)|buf[1];  
        } else {  
            /* Read a 32 bit len. */  
            if (rioRead(rdb,&len,4) == 0) return REDIS_RDB_LENERR;  
            return ntohl(len);  
        }  
    }  


只要通过编码方式存储的字符串,普通字符串都要先经过压缩再存入,取出的时候先做解压操作:



    /* rdb加载字符串对象的泛型方法 */  
    robj *rdbGenericLoadStringObject(rio *rdb, int encode) {  
        int isencoded;  
        uint32_t len;  
        sds val;  
      
        len = rdbLoadLen(rdb,&isencoded);  
        if (isencoded) {  
            //返回值主要为加载数值对象,和获取解压后的字符串对象  
            switch(len) {  
            case REDIS_RDB_ENC_INT8:  
            case REDIS_RDB_ENC_INT16:  
            case REDIS_RDB_ENC_INT32:  
                return rdbLoadIntegerObject(rdb,len,encode);  
            case REDIS_RDB_ENC_LZF:  
                return rdbLoadLzfStringObject(rdb);  
            default:  
                redisPanic("Unknown RDB encoding type");  
            }  
        }  
      
        //无编码方式,直接读取rdb  
        if (len == REDIS_RDB_LENERR) return NULL;  
        val = sdsnewlen(NULL,len);  
        if (len && rioRead(rdb,val,len) == 0) {  
            sdsfree(val);  
            return NULL;  
        }  
        return createObject(REDIS_STRING,val);  
    }  

综上,我总结了几点,redis数据量在存储数据上的做的调优

1.长度等数值数据存储,根据数值大小的不同,分配不同的字节存储,1个字节,2个字节,后面直接到5个字节,避免直接像int32,int64一样,直接占去4,8个字节。一般字符串的长度都是比较小的,如果每个字符串的长度是10,你用4,8个字节去存的话,大大的浪费空间了。
2.字符串等非数值存储,redis在这里采用了lzf压缩算法,当然取出的时候,你要进行解压,或者你从最开始的时候不选择的压缩存储,而是直接存储。

所以,这样的设计非常棒,数据库的任何操作结果都会最终赋值到robj->ptr上:



    if (o->encoding == REDIS_ENCODING_INTSET) {  
                    /* Fetch integer value from element */  
                    if (isObjectRepresentableAsLongLong(ele,&llval) == REDIS_OK) {  
                        //最后都会通过吧值赋在obj->ptr上  
                        o->ptr = intsetAdd(o->ptr,llval,NULL);  
                    } else {  
                        setTypeConvert(o,REDIS_ENCODING_HT);  
                        dictExpand(o->ptr,len);  
                    }  
                }  


在这些个方法里面,还有一个比较特殊的后台保存到数据库的方法,为什么会有这样的操作呢,因为redis其实和mencached一样,是内存数据库,如果对数据的操作都直接是写入磁盘,I/O开销肯定很大,所以一般内存数据库都是先把操作结构都存放在内存中,等到了内存的数据满了,再持久化到磁盘中,就是保存数据库操作到文件中了。redis在这里还很人性化的提供了backgroundSave()的方式:,如果这个问题出现在java里面,我的直接做法肯定开个线程让他直接运行Save的方法就行了,但是想在C语言中实现这种类似多线程的操作,我还真想不出来,最终他的答案是fork(),在Linux编程中,肯定接触过了这个方法,在C语言的应用编程中基本没看到过,我也是头次领略到fork方法还能这么用,先看看原方法调用细节:



    /* 后台进行rbd保存操作 */  
    int rdbSaveBackground(char *filename) {  
        pid_t childpid;  
        long long start;  
      
        if (server.rdb_child_pid != -1) return REDIS_ERR;  
      
        server.dirty_before_bgsave = server.dirty;  
        server.lastbgsave_try = time(NULL);  
      
        start = ustime();  
        //利用fork()创建子进程用来实现rdb的保存操作  
        //此时有2个进程在执行这段函数的代码,在子进行程返回的pid为0,  
        //所以会执行下面的代码,在父进程中返回的代码为孩子的pid,不为0,所以执行else分支的代码  
        //在父进程中放返回-1代表创建子进程失败  
        if ((childpid = fork()) == 0) {  
            //在这个if判断的代码就是在子线程中后执行的操作  
            int retval;  
      
            /* Child */  
            closeListeningSockets(0);  
            redisSetProcTitle("redis-rdb-bgsave");  
            //这个就是刚刚说的rdbSave()操作  
            retval = rdbSave(filename);  
            if (retval == REDIS_OK) {  
                size_t private_dirty = zmalloc_get_private_dirty();  
      
                if (private_dirty) {  
                    redisLog(REDIS_NOTICE,  
                        "RDB: %zu MB of memory used by copy-on-write",  
                        private_dirty/(1024*1024));  
                }  
            }  
            exitFromChild((retval == REDIS_OK) ? 0 : 1);  
        } else {  
            //执行父线程的后续操作  
            /* Parent */  
            server.stat_fork_time = ustime()-start;  
            server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */  
            latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);  
            if (childpid == -1) {  
                server.lastbgsave_status = REDIS_ERR;  
                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.rdb_save_time_start = time(NULL);  
            server.rdb_child_pid = childpid;  
            updateDictResizePolicy();  
            return REDIS_OK;  
        }  
        return REDIS_OK; /* unreached */  
    }  


父进程fork()出的子线程是基本完全复用父亲线程的,所以也就是说,父子线程都会执行这个函数,但是唯一的区别是执行fork函数返回值是不同的,子线程因为是被fork出来的,返回的就是0代表自身,父亲线程就是返回子线程的PID,然后根据返回的PID不同,执行不同的操作,子线程就完全独立于父亲线程,做自己的保存操作。这也是头次我知道了fork还能这么用。下面亮出.h头文件中的API,其实和.c文件里的差了很多的方法:



    int rdbSaveType(rio *rdb, unsigned char type); /* 保存类型操作 */  
    int rdbLoadType(rio *rdb); /* 加载RDB中的格式类型 */  
    int rdbSaveTime(rio *rdb, time_t t);  
    time_t rdbLoadTime(rio *rdb); /* 加载时间,都是间接调用的是rioRead()方法 */  
    int rdbSaveLen(rio *rdb, uint32_t len); /* 保存一个字符串对象的长度时,根据长度的不同,分不同的编码方式 */  
    uint32_t rdbLoadLen(rio *rdb, int *isencoded); /* 加载长度,也需要根据编码方式,读取不同的buf获取长度 */  
    int rdbSaveObjectType(rio *rdb, robj *o); /* 根据robj中的编码方式,保存到rbd中 */  
    int rdbLoadObjectType(rio *rdb); /* 加载rbd中的obj Type */  
    int rdbLoad(char *filename); /* 加载rdb数据库文件 */  
    int rdbSaveBackground(char *filename); /* 后台进行rbd保存操作 */  
    void rdbRemoveTempFile(pid_t childpid); /* 移除子进程操作的相关保存rdb文件 */  
    int rdbSave(char *filename); /* 保存rdb数据库的内容到磁盘中 */  
    int rdbSaveObject(rio *rdb, robj *o); /* 保存redis obj对象到rdb中 */  
    off_t rdbSavedObjectLen(robj *o); /* 获取保存后的长度,其实就是获取了保存数据时计算的偏移量 */  
    off_t rdbSavedObjectPages(robj *o);  
    robj *rdbLoadObject(int type, rio *rdb); /* 加载redis obj对象,有特定的Type类型 */  
    void backgroundSaveDoneHandler(int exitcode, int bysignal); /* 后台保存数据库操作完成后的处理方法 */  
    int rdbSaveKeyValuePair(rio *rdb, robj *key, robj *val, long long expiretime, long long now);  
    robj *rdbLoadStringObject(rio *rdb); /* 无编码方式加载字符串对象 */  
    void saveCommand(redisClient *c) /* 将保存操作封装成命令的形式 */  
    void bgsaveCommand(redisClient *c) /* 将后台保存数据库操作封装成命令的模式 */


运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-27093-1-1.html 上篇帖子: Redis源码分析(十三)--- redis-benchmark性能测试 下篇帖子: Redis源码解析(十五)--- aof-append only file解析 数据库操作
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表