redis源码学习笔记(一)--数据类型

最近在通过阅读开源项目redis的源码的方式提升C语言和数据结构的功底。redis是一个比较有名的开源nosql数据库系统,所谓nosql,即not only sql(非关系型数据库)。相对于关系型数据库格式化的数据结构,非关系型数据库主要以键值对的方式进行储存,其结构不固定,每一个元组可以有不同的字段。说简单些,比如一张用户表,在关系型数据库中,有用户id,密码,性别,余额,注册日期,居住地址等字段,那么所有该表的元组都具有相同的所有字段,无论这个用户是否真的拥有以上所有信息。而在非关系型数据库中不同的用户元组则可以有不同的键值对组合,比如有的用户不想留下自己的性别,有的用户不想留下自己的住址,那么这个用户对应的元组就没有性别或者住址这个字段。

有点意思哈。。。

由于我在项目中仅仅用过mysql数据库系统,而且也就是“用过”而已,底层实现啥的以前完全没考虑过,故本文暂时不做redis和mysql的比较的讨论。扯了好多废话,下面我们进入正题。

我阅读redis源码学习的首步是了解其数据类型的构成和实现,做过数据库开发的朋友应该都会有体会,数据类型系统是数据库系统实现其功能的基石。

redisObject是redis数据类型系统的核心成员,它是redis自定义的一种数据类型,我们从代码块A中可以看出它其实是一个结构体,其定义在文件目录中的redis.c(我使用的redis版本为1.2.6,比较老,新版的定义位置貌似有所不同,在redis.h),源代码中并未对结构体中的各个属性做出解释,我结合网上的一些资料对注释做了补充。(A中红色注释)

代码块A:

/* A redis object, that is a type able to hold a string / list / set */
typedef struct redisObject {
    void *ptr;//指向对象值的指针
    unsigned char type;//对象类型
    unsigned char encoding;//编码方式
    unsigned char notused[2];//对齐位
    int refcount;//引用计数
} robj;

很容易理解,对于数据库功能的实现而言,*ptr,type,encoding是最重要的三个属性。

我们先来看type,type顾名思义就是对象所储存的值的类型,相当于Mysql我们设计表时各个字段的类型(int,varchar,text)啥的。redis将对象储存值的类型的定义在redis.c,如下代码块B

代码块B:

/* Object types */
#define REDIS_STRING 0//字符串
#define REDIS_LIST 1//列表
#define REDIS_SET 2//集合
#define REDIS_ZSET 3//有序表
#define REDIS_HASH 4//哈希表

对象存储的值的编码方式同样定义在redis.c(高版本应该是在redis.h),redis将其命名为encoding,定义代码如下代码块C(新版本redis增加了哈希表,双端链表等多种类型)

代码块C

/* Objects encoding */
#define REDIS_ENCODING_RAW 0    /* Raw representation */ //编码为字符串	
#define REDIS_ENCODING_INT 1    /* Encoded as integer */ //编码为整形


那么*ptr是干嘛用的呢?我们来举个例子,如果一个对象(redisObject)的type是REDIS_STRING,encoding是REDIS_RAW,那么这个对象的类型是列表,在内存中的编码方式为字符串。那么当我们需要查找这个对象的时候怎么去找到它呢?我们需要一个指向它的内存地址的指针,因为它在内存中的编码方式是字符串,所以*ptr就是指向这个字符串的指针。

下面来看看redis的字符串类型是如何实现的。redis底层使用了一种叫做SDS(simple nynamic string,简单动态字符串)的自定义数据结构,它在实现字符串类型的同时在整个redis系统中几乎完全替代了char*类型。redis对sds的定义位于sds.h,入代码块D中的结构体。

代码块D

typedef char *sds;

struct sdshdr {
    long len;//buf已占用长度
    long free;//buf剩余长度
    char buf[];//实际存储字符串的位置
};

redis把char* 弄了个别名叫sds,结构体中只有3个属性,看起来还是很简单的对吧。

我们先来看个例子:我用redis命令创建一个键值对:

redis>SET love "Manchester United"

这时这个键值对的键(love),值(Manchester United)都是字符串对象sds。字符串对像生成函数的声明位于sds.h,其函数体位于sds.c,如下代码快E。我结合自己的理解稍微补充了一些注释。

代码块E

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;//建立新sdshdr结构体

    sh = zmalloc(sizeof(struct sdshdr)+initlen+1);//开辟相应内存空间
#ifdef SDS_ABORT_ON_OOM
    if (sh == NULL) sdsOomAbort();//若内存空间不足抛出异常
#else
    if (sh == NULL) return NULL;
#endif
    sh->len = initlen;//将len赋值为新字符串长度
    sh->free = 0;//未占用空间赋值为0
    if (initlen) {
        if (init) memcpy(sh->buf, init, initlen);//将字符串赋值到buf[]
        else memset(sh->buf,0,initlen);
    }
    sh->buf[initlen] = '\0';
    return (char*)sh->buf;
}
那么我们刚刚建立的键值对的值所对应的结构体就是这个样子咯:

struct sdshdr{

len = 17;

free = 0;

buf = " Manchester United\0"//注意因为C字符串以'/0'收尾,故buf实际长度比len多一位

}

有没有发现sds结构比用char*好在哪呢?首先,对于数据库系统而言,计算字符串长度是相当频繁的操作,对于char*字符串而言,每计算一次字符串长度相当与遍历一次字符数组,时间复杂度O(n),而sds把字符串长度存在结构体中,系统可以直接获取,时间复杂度O(1)。我以前写涉及字符串的C程序时也喜欢用结构体存储对应字符串和其长度呢,可以看看英文文本词频处理的那篇文章,瞬间觉得自己“高大上”了有木有,哈哈。

此外,我们在做Mysql开发时,使用insert操作时很频繁的,这对于数据库系统而言就意味着频繁的内存重新分配的操作。比如我现在要在刚刚建立的键值对后面加点东西:

redis>APPEND love " forever"

此时我们新添加的字符串将会被加到原来字符串的后面。redis使用sdsMakeRoomFor函数优化了整个内存预分配流程,其函数体同样位于sds.c。如下代码块F,博主水平有限,注释仅做参考

代码块F

static sds sdsMakeRoomFor(sds s, size_t addlen) {
    struct sdshdr *sh, *newsh;
    size_t free = sdsavail(s);//或取原sds对象未使用空间长度(free属性)
    size_t len, newlen;

    if (free >= addlen) return s;//若未使用空间足够本次分配,直接返回
    len = sdslen(s);//若不够则先查找原字符串占用长度
    sh = (void*) (s-(sizeof(struct sdshdr)));
    newlen = (len+addlen)*2;//分配两倍于新长度的空间给该对象
    newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);//开辟相应空间
#ifdef SDS_ABORT_ON_OOM
    if (newsh == NULL) sdsOomAbort();//若内存空间不足,抛出异常
#else
    if (newsh == NULL) return NULL;
#endif

    newsh->free = newlen - len;//重新计算free值(这个计算逻辑不是很理解哦。。。)
    return newsh->buf;
}

   点睛之笔就在于
 newlen = (len+addlen)*2;
   添加了“ forever”,我们的结构体变成这样了:

struct sdshdr{

len = 25;

free =25;

buf = " Manchester United forever\0                    "//空白部分为预分配空间,此时总长度为25+25+1

}

    应该看出来了吧,如果我下次想再添加一个“forever”或者其他字符串,只要不超过free值,系统就不用再次进行内存分配操作。这种方法在一定程度上降低了追加字符串操作时系统进行内存重新分配的次数。

    好,这次暂时先写到这吧。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值