Redis 3.0 源码解析---底层数据结构分析(1)

引言:今天开始边看编写redis的源码分析,以前只利用c写过简单的小程序,还从来没有利用c来做过一个完成的工程,作为程序员,学的第一门编程语言,惭愧的是,现在用的却不怎么多,借此,也好好的复习一下自己的C语言知识,看看优秀的工程实现。

0.前言
        我是工作后才开始接触Redis的,以前在学校听说过memcached,但也只知道它是一个高性能的分布式内存缓存服务,一个很重要的点就是说内存缓存服务,也就是说它其实是把缓存的数据放在了内存中,既然放在了内存中,读取速度显然要快很多,再加上分布式,自然是高性能。在学校具体也没怎么用过,后来听说了NoSql(ps:NoSql的英文全称为,Not Only Sql),非关系型数据库,现在面对大数据这种杂乱无章的数据,关系型数据库似乎对他无能为力,于是出现了NoSql,就说用来存放那些没啥规律的数据。
        而我听到的第一个对Redis的定义(ps:Redis全称为 REmote  DIctionary  Server)是:一种基于内存的Key-Value数据库,用C语言实现。和memcached相比,它存储的Value类型相对更多,包括字符串(string),链表(list),集合(set),有序集合(zset),哈希(hash)。同memcache一样,它也是将数据放在了内存当中,以保证数据访问的效率。
        听到上面的数据类型,string,list,set,zset,hash,我们是再熟悉不过的了,每一本数据结构的书上来都会讲它们的实现原理,但是惭愧的是,笔者也仅仅实现过简单的string,list,hash,功能上相当不完善,主要也是因为现在主流面向对象编程语言基本上对这些数据结构进行很好的封装,要知道,程序员都是很懒的,既然有现成可用的,干嘛还要实现呢,况且自己实现的可能还bug一大堆。但是每一个数据结构实现的原理背后都应该有很精妙的编程方法,都有它考虑问题时所偏向的策略(比如list中有基于数组和链表,应该选择哪一种策略?还是两种策略都提供?),这些东西确实是值得我们学习的。于是,就从redis的基本的底层数据结构开始分析,一步一步向上看看redis是如何搭建起一个高性能的内存数据库的。

1.sds--- Simple  Dynamic  String   (在sds.h/sds.c中定义和实现)
        在redis中,最基本的数据类型就是Simple Dynamic String(简单动态字符串),简称sds,sds是用结构体定义的,其定义如下:
1
2
3
4
5
struct  sdshdr {
     unsigned  int  len; //buf中已占用空间的长度
     unsigned  int  free ; //buf中还没有被占用的即剩余空间长度
     char  buf[]; //用于存放数据
};
        在结构体的定义中,buf指的就是数据存储的空间,len是对应已经用掉的空间,而free是剩余空间,那么是不是说说len+free就是buf数据空间的大小呢?实际上Redis会为buf多申请一个字节的空间,也就是说len+free+1才是buf的实际空间大小。在前面我们说过sds是一种字符串,字符串和字符数组的最重要的区别就是,字符串在最后多了一个结束符'\0',即多的一个字节空间就是用来存放'\0',下面所示的是buf空间的某个时刻的状态,存放的是字符串Redis,那么在sds中对应的len的值为5,表示存了5个字符,free为5,还有5个剩余空间,而buf的总的空间长度为5+5+1=11.从sdshdr的定义上可以看出动态string的长度还是有范围的,即unsigned int 的范围减去1 
R e d i s
\0





现在有两个问题:
一.为什么redis不直接使用字符数组来作为最基本的数据类型?
       不使用C语言提供的字符数组,肯定是字符数组不能够满足redis所需要的性能需求
       1)在redis中,获取字符串的长度是非常普遍的操作如STRLEN操作,如果用字符数组的话,那么每一次执行一次strlen操作,strlen需要遍历一边字符串,也就是说跟字符串的长度有关,而利用sdshdr结构体的话,我们只需要获取len的值就可以在O(1)的时间内完成(以空间换取时间,基本不能再基本的策略)
       2)在redis中改变字符串如APPEND操作也是经常的事儿,对于APPEND操作,在C语言中只能重新分配内存,然后将旧字符串和新字符串copy到重新分配的内存当中。一两次操作还可以,但是如果操作过于频繁的话,显然效率上是不能够忍受的,而free字段的出现,降低了重新分配内存的次数,当APPEND操作时,只需要将字符串的添加到当前字符串的后面,只有当free的长度小于要添加的字符的长度时,才会重新分配内存。(后面我们会讲解Redis分配内存的策略)
二.既然有了len和free,为什么还要在每一个buf数组后面指定一个结束符呢?        
       既然是字符串的话,Redis遵循了以'\0'结尾的习惯,这样就可以直接利用C语言提供的库函数如strlen,strcat,strcpy等对buf进行操作,就不需要redis自己编写使用sdshdr的函数,在redis的源码中随处可以见到这些函数的使用,牺牲一个字节的空间换取代码实现与操作上的简洁性相信也是值得的。

      熟悉C语言的人应该就会发现一个问题,sizeof(struct sdshdr)的值到底是多少呢?
      这涉及到char *a与char b[]的区别。在c语言中,我们对a和b进行sizeof计算,我们会发现sizeof(a)=4,sizeof(b)=0。在C语言中,a指的是一个指针变量,指针的大小是4个字节,存放着一个地址,而对于字符数组来讲,编译器在翻译b的时候,直接把它翻译成一个地址0xXXXXXXXX的形式(可以使用gdb等调试器进行调试具体看一下),也就是对于b来讲他是一个地址,不过这个地址指向的是数组的第一个元素。而对于char b[]这样的声明,等同于char b[0],这个数组的大小自然为0.那么sizeof(struct sdshdr)=8,仅仅只包含了前两个成员变量len和free的大小。但是这里有一个小 trick,buf指向的其实struct的结尾元素的地址。那么buf的地址如何获取呢?显然可以通过struc sdshdr变量来获取,如下所示:
1
2
3
struct  sdshdr *tmp=( struct  sdshdr *) malloc ( sizeof ( struct  sdshdr)); //声明一个sdsdhr变量
tmp->buf; //这样就可以直接指向了结构体定义的结尾指针
      那这么做有什么好处呢?
      1)我可以直接在结构体的后面声明一个字符数组作为数据存储,在逻辑地址上结构体和动态变化存储空间是连续的。
      2)buf相对结构体的偏移就是字符数组的开始地址,在减少了存放字符地址空间(4个字节)的同时也减少了cpu的寻址次数。如果我们以char *a来存储的话,cpu会首先需找a的地址,然后根据a的地址中的地址值去需找数组的第一个元素的地址。
      这样做了之后显然是不能直接声明sruct sdshdr的数组了,因为struct sdshdr后面的字符长度是动态变化的.
      
      在介绍具体的动态内存分配策略,我们先看redis所定义的一个类型sds:
1
2
typedef  char  *sds;
      本身sds是一个字符指针,他指向的是 sruct sdshdr结构体中buf的位置。也就是说在redis中,sds指向的地址前面保存着len和free的值,后面保存着字符串的值。以下的函数初始化操作的
1
2
3
sds  sdsnewlen ( const  void  *init,  size_t  initlen);//初始化一个长度为initlen的sdshdr,值为init
sds  sdsnew ( const  char  *init);//初始化一个初始值为init的sdshdr
sds  sdsempty ( void );//初始化一个空sdshdr
        注意他们返回的都是sds,也就是指向buf的字符指针。
        redis把对sdshdr的动态扩展放在了函数sdsMakeRoomFor里面,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sds  sdsMakeRoomFor (sds s,  size_t  addlen) {
     struct  sdshdr *sh, *newsh;
     size_t  free  sdsavail (s); //sdsavail是一个内联函数,根据sds获取free的值的
     size_t  len, newlen;
 
     if  ( free  >= addlen)  return  s; //如果剩余空间free够用的话,就直接返回
     len =  sdslen (s); //sdslen获得已用空间len的值
     sh = ( void *) (s-( sizeof ( struct  sdshdr)));
     newlen = (len+addlen);
     if  (newlen < SDS_MAX_PREALLOC)    //SDS_MAX_PREALLOC的值为1024*1024=1M
         newlen *= 2;   //新空间大小翻一倍
     else
         newlen += SDS_MAX_PREALLOC;  //新空间大小原来的加上1M
     newsh =  zrealloc (sh,  sizeof ( struct  sdshdr)+newlen+1); 
     if  (newsh == NULL)  return  NULL;
 
     newsh-> free  = newlen - len;
     return  newsh->buf;
}
       从代码中我们可以看出动态扩展的策略为:如果增加字符后的总字符长度小于1M的话,那么就将总的空间申请为总字符的一倍,如果大于1M的话,就多分配1M的空间,以保证以后再APPEND的时候过于频繁的重新申请内存空间。
      redis同时还提供了很多其他的有用的对字符串操作的函数,如sdscat,sdscpy等,有兴趣的同学可以下载redis源码细读一下,sds的实现技巧性还是比较强的

2.adlist---A Double Linked List   (在adlist.h/adlist.c中定义和实现)
      数据结构中最基本的双向链表的实现,在几乎所有的数据结构数据中都会有其相应的介绍,我们首先来看一下双向链表的结构体定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 双向链表的节点定义:指向前置节点和后置节点的指针,和一个保持value的指针 */
typedef  struct  listNode {
     struct  listNode *prev;  
     struct  listNode *next;
     void  *value;
} listNode;
/* 双向链表的迭代器,用于遍历链表 */
typedef  struct  listIter {
     listNode *next;
     int  direction;
} listIter;
/* 双向链表,拥有表头和表尾的节点指针和链表长度,同时拥有三个函数指针
    dup复制节点值操作
    free释放节点
    match节点值比对函数
  */
typedef  struct  list {
     listNode *head;
     listNode *tail;
     void  *(* dup )( void  *ptr);
     void  (* free )( void  *ptr);
     int  (* match )( void  *ptr,  void  *key);
     unsigned  long  len;
} list;
       listNode节点的定义中redis使用一个void*来保存指向具体值的指针,这样的话,有一个好处就是,节点可以保存任意类型的值,listNode保存的是这些值的地址。
       listIter,链表迭代器,用于遍历链表使用,可以正向或反向遍历,通过direction指定
       list,拥有表头,表尾节点的指针,和链表长度,同时定义了三个函数指针。看到这个定义,想起了大学老师上C语言课程的时候说的话:现在的面向对象编程语言很火,但是其实用C语言同样可以写出具有面向对象特点的程序,当时还不甚理解。面向对象,无非就是对一个客观存在的实体抽象出来一定的属性和行为,在编程语言里就是变量以及对这些变量进行操作的函数。双向链表list,拥有了head,tail和len变量,而dup,free,match就是对这些变量进行操作的函数。同样,它还具有多态的的特性,针对不同的节点中的value值,我们可以实现不同的dup,free,match函数。多么有趣,我们随处都使用的特性原来在C语言里也有它特定的表达方式。
       以下是redis提供的对list的操作函数,  基本上都是简单的链表操作。有兴趣的可以查看源码的实现,实现的还是比较精妙的,包括迭代器的运用,指针的的操作等
1
2
3
4
5
6
7
8
9
10
11
12
13
list *listCreate( void ); //创建一个新的list,list里面len为0,指针元素全部为NULL
void  listRelease(list *list); //释放整个list,node节点里value的值使用list里的free函数释放
list *listAddNodeHead(list *list,  void  *value); //从链表头加入新节点(LPUSH)
list *listAddNodeTail(list *list,  void  *value); //从链表尾加入新节点(RPUSH)
list *listInsertNode(list *list, listNode *old_node,  void  *value,  int  after); //加入到指定节点的前面或后面
void  listDelNode(list *list, listNode *node); //删除指定节点
listIter *listGetIterator(list *list,  int  direction); //返回list的迭代器
void  listReleaseIterator(listIter *iter) ; //释放迭代器
listNode *listNext(listIter *iter); //返回迭代器的下一个listNode
list *listDup(list *orig); //复制整个链表
listNode *listSearchKey(list *list,  void  *key); //查找给定的key是否存在与链表中
listNode *listIndex(list *list,  long  index); //返回index的listNode,支持正向和反向,index为正或负表示
void  listRotate(list *list); //翻转链表

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值