请选择 进入手机版 | 继续访问电脑版

Redis中国用户组(CRUG)论坛

 找回密码
 立即注册

扫一扫,访问微社区

搜索
热搜: 活动 交友 discuz
查看: 1403|回复: 1

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

[复制链接]
  • TA的每日心情
    开心
    2017-8-30 15:46
  • 签到天数: 94 天

    [LV.6]常住居民II

    371

    主题

    480

    帖子

    3840

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    3840

    最佳新人活跃会员宣传达人突出贡献优秀版主荣誉管理论坛元老

    发表于 2016-4-5 10:13:03 | 显示全部楼层 |阅读模式

    过去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在数据存储方面上,把数据存储的内存消耗降到了极致。比如只要是在数据库中保存的长度等数字的,必须经过计算判断,然后再分配相应的字节保存(跟前面压缩列表等的原理类型):

    1. /* Load an encoded length. The "isencoded" argument is set to 1 if the length
    2. * is not actually a length but an "encoding type". See the REDIS_RDB_ENC_*
    3. * definitions in rdb.h for more information. */  
    4. /* 加载长度,也需要根据编码方式,读取不同的buf获取长度 */  
    5. uint32_t rdbLoadLen(rio *rdb, int *isencoded) {  
    6.     unsigned char buf[2];  
    7.     uint32_t len;  
    8.     int type;  
    9.   
    10.     if (isencoded) *isencoded = 0;  
    11.     if (rioRead(rdb,buf,1) == 0) return REDIS_RDB_LENERR;  
    12.     type = (buf[0]&0xC0)>>6;  
    13.     if (type == REDIS_RDB_ENCVAL) {  
    14.         /* Read a 6 bit encoding type. */  
    15.         if (isencoded) *isencoded = 1;  
    16.         return buf[0]&0x3F;  
    17.     } else if (type == REDIS_RDB_6BITLEN) {  
    18.         /* Read a 6 bit len. */  
    19.         return buf[0]&0x3F;  
    20.     } else if (type == REDIS_RDB_14BITLEN) {  
    21.         /* Read a 14 bit len. */  
    22.         if (rioRead(rdb,buf+1,1) == 0) return REDIS_RDB_LENERR;  
    23.         return ((buf[0]&0x3F)<<8)|buf[1];  
    24.     } else {  
    25.         /* Read a 32 bit len. */  
    26.         if (rioRead(rdb,&len,4) == 0) return REDIS_RDB_LENERR;  
    27.         return ntohl(len);  
    28.     }  
    29. }  
    复制代码

    只要通过编码方式存储的字符串,普通字符串都要先经过压缩再存入,取出的时候先做解压操作:
    1. /* rdb加载字符串对象的泛型方法 */  
    2. robj *rdbGenericLoadStringObject(rio *rdb, int encode) {  
    3.     int isencoded;  
    4.     uint32_t len;  
    5.     sds val;  
    6.   
    7.     len = rdbLoadLen(rdb,&isencoded);  
    8.     if (isencoded) {  
    9.         //返回值主要为加载数值对象,和获取解压后的字符串对象  
    10.         switch(len) {  
    11.         case REDIS_RDB_ENC_INT8:  
    12.         case REDIS_RDB_ENC_INT16:  
    13.         case REDIS_RDB_ENC_INT32:  
    14.             return rdbLoadIntegerObject(rdb,len,encode);  
    15.         case REDIS_RDB_ENC_LZF:  
    16.             return rdbLoadLzfStringObject(rdb);  
    17.         default:  
    18.             redisPanic("Unknown RDB encoding type");  
    19.         }  
    20.     }  
    21.   
    22.     //无编码方式,直接读取rdb  
    23.     if (len == REDIS_RDB_LENERR) return NULL;  
    24.     val = sdsnewlen(NULL,len);  
    25.     if (len && rioRead(rdb,val,len) == 0) {  
    26.         sdsfree(val);  
    27.         return NULL;  
    28.     }  
    29.     return createObject(REDIS_STRING,val);  
    30. }  
    复制代码

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

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

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

    1. if (o->encoding == REDIS_ENCODING_INTSET) {  
    2.                 /* Fetch integer value from element */  
    3.                 if (isObjectRepresentableAsLongLong(ele,&llval) == REDIS_OK) {  
    4.                     //最后都会通过吧值赋在obj->ptr上  
    5.                     o->ptr = intsetAdd(o->ptr,llval,NULL);  
    6.                 } else {  
    7.                     setTypeConvert(o,REDIS_ENCODING_HT);  
    8.                     dictExpand(o->ptr,len);  
    9.                 }  
    10.             }  
    复制代码

    在这些个方法里面,还有一个比较特殊的后台保存到数据库的方法,为什么会有这样的操作呢,因为redis其实和mencached一样,是内存数据库,如果对数据的操作都直接是写入磁盘,I/O开销肯定很大,所以一般内存数据库都是先把操作结构都存放在内存中,等到了内存的数据满了,再持久化到磁盘中,就是保存数据库操作到文件中了。redis在这里还很人性化的提供了backgroundSave()的方式:,如果这个问题出现在java里面,我的直接做法肯定开个线程让他直接运行Save的方法就行了,但是想在C语言中实现这种类似多线程的操作,我还真想不出来,最终他的答案是fork(),在Linux编程中,肯定接触过了这个方法,在C语言的应用编程中基本没看到过,我也是头次领略到fork方法还能这么用,先看看原方法调用细节:
    1. /* 后台进行rbd保存操作 */  
    2. int rdbSaveBackground(char *filename) {  
    3.     pid_t childpid;  
    4.     long long start;  
    5.   
    6.     if (server.rdb_child_pid != -1) return REDIS_ERR;  
    7.   
    8.     server.dirty_before_bgsave = server.dirty;  
    9.     server.lastbgsave_try = time(NULL);  
    10.   
    11.     start = ustime();  
    12.     //利用fork()创建子进程用来实现rdb的保存操作  
    13.     //此时有2个进程在执行这段函数的代码,在子进行程返回的pid为0,  
    14.     //所以会执行下面的代码,在父进程中返回的代码为孩子的pid,不为0,所以执行else分支的代码  
    15.     //在父进程中放返回-1代表创建子进程失败  
    16.     if ((childpid = fork()) == 0) {  
    17.         //在这个if判断的代码就是在子线程中后执行的操作  
    18.         int retval;  
    19.   
    20.         /* Child */  
    21.         closeListeningSockets(0);  
    22.         redisSetProcTitle("redis-rdb-bgsave");  
    23.         //这个就是刚刚说的rdbSave()操作  
    24.         retval = rdbSave(filename);  
    25.         if (retval == REDIS_OK) {  
    26.             size_t private_dirty = zmalloc_get_private_dirty();  
    27.   
    28.             if (private_dirty) {  
    29.                 redisLog(REDIS_NOTICE,  
    30.                     "RDB: %zu MB of memory used by copy-on-write",  
    31.                     private_dirty/(1024*1024));  
    32.             }  
    33.         }  
    34.         exitFromChild((retval == REDIS_OK) ? 0 : 1);  
    35.     } else {  
    36.         //执行父线程的后续操作  
    37.         /* Parent */  
    38.         server.stat_fork_time = ustime()-start;  
    39.         server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024); /* GB per second. */  
    40.         latencyAddSampleIfNeeded("fork",server.stat_fork_time/1000);  
    41.         if (childpid == -1) {  
    42.             server.lastbgsave_status = REDIS_ERR;  
    43.             redisLog(REDIS_WARNING,"Can't save in background: fork: %s",  
    44.                 strerror(errno));  
    45.             return REDIS_ERR;  
    46.         }  
    47.         redisLog(REDIS_NOTICE,"Background saving started by pid %d",childpid);  
    48.         server.rdb_save_time_start = time(NULL);  
    49.         server.rdb_child_pid = childpid;  
    50.         updateDictResizePolicy();  
    51.         return REDIS_OK;  
    52.     }  
    53.     return REDIS_OK; /* unreached */  
    54. }  
    复制代码

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

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



    转自:http://blog.csdn.net/androidlushangderen/article/details/40266579
    上一篇:Redis源码分析(十三)--- redis-benchmark性能测试
    下一篇:Redis源码解析(十五)--- aof-append only file解析

    该用户从未签到

    0

    主题

    1

    帖子

    23

    积分

    新手上路

    Rank: 1

    积分
    23
    发表于 2016-4-20 16:30:53 | 显示全部楼层
    作分析的真是一位大神
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    阿里云
    阿里云

    Archiver|手机版|小黑屋|Redis中国用户组 ( 京ICP备15003959号

    GMT+8, 2018-4-19 21:29 , Processed in 0.261488 second(s), 33 queries .

    Powered by Discuz! X3.2

    © 2001-2013 Comsenz Inc.

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