Redis是一个正在进行中的开源键值数据库项目,作者是antirez。redis与一般键值数据库相比,其最大特色是键值对中的值支持链表、集合、排序集等复合结构。这里我们来看看antirez是怎样实现通用链表结构的。

如果你熟悉Linux,应该知道Linux内核也实现了通用链表结构,它使用了GCC中超越了ANSI C规范的特性。但是Redis没有这样做,而是使用了类似C++ STL的做法。Redis实现的是双向非循环链表,牵涉到三种数据结构:结点listNode、迭代器listIter、链表本身list。代码如下。 




typedef  
   struct 
    listNode {
 
   void 
     
   *value 
   ;
 
   struct 
    listNode  
   * 
   prev;
 
   struct 
    listNode  
   * 
   next;
}listNode;



 value指向具体的数据,从后面两个指针可以看出这是个双向链表,至于是否循环要从其他代码判断。

 




typedef  
   struct 
    listIter {
 
   struct 
    listNode  
   * 
   next;
 
   int 
    direction;
}listIter;



这是个迭代器,当然需要一个指针来指向结点咯,就是next;direction表示该迭代器的前进方向,AL_START_HEAD表示从头开始,AL_START_TAIL表示从尾开始。

好,链表结构是时候出来了



adlist.h 
      
typedef      struct 
     list {
 listNode      * 
    head;
 listNode      * 
    tail;

     void 
    * 
     ( 
    * 
    dup)( 
    void 
      
    * 
    ptr);
     void 
     ( 
    * 
    free)( 
    void 
      
    * 
    ptr);
     int 
     ( 
    * 
    match)( 
    void 
      
    * 
    ptr,  
    void 
    * 
    key);

 unsigned      int 
     len;
}list;


head、tail和len很好理解,中间三个变量是干嘛的呢,它们是函数变量,dup是复制函数,你可以自己定义复制策略,free是释放策略,match是如何在链表中查询内容的策略。

与链表相关的数据结构就是这三个,下面来看看此链表的API是如何实现的,API详情可查看adlist.h文件。

 

list* listCreate(void);  毫无疑问这是链表的构造函数

adlist.c       
list     *      listCreate( 
    void 
    )
{
     struct      list  
    * 
    list;
     if      ((list  
    = 
     zmalloc( 
    sizeof 
    ( 
    * 
    list)))  
    == 
     NULL)
     return      NULL;

 list     ->     head  
    = 
     list 
    -> 
    tail  
    = 
     NULL;
 list     ->     len  
    = 
      
    0 
    ;
 list     ->     dup  
    = 
     NULL;
 list     ->     free  
    = 
     NULL;
 list     ->     match  
    = 
     NULL;

     return      list;
}


没什么好说的,从堆中动态分配一个list结构,初始化成员变量,然后返回list的指针,这样我们就能控制堆中的链表了。注意zmalloc是Redis自己封装的内存分配函数,如果感兴趣可以查看zmalloc.c文件

有构造函数,就自然有析构函数了,如下:

void listRelease(list *list); 
     
    
void      listRelease(list      * 
    list)
{
 unsigned      int      len;
 listNode      *     current,  
    * 
    next;

 current      =      list 
    -> 
    head;
 len      =      list 
    -> 
    len;
     while      (len 
    -- 
    ) {
 next      =      current 
    -> 
    next;
     if      (list 
    -> 
    free) list 
    -> 
    free(current 
    -> 
    value);
 zfree(current);
 current      =      next;
 }
 zfree(list);
}

zfree一样是Redis封装的函数,这里要注意的是list->free,它是用来释放结点中的数据的,说明如果你的listNode中的value是指向堆中的内存的话,就一定要为链表定义释放函数,否则会造成内存泄漏。

 

现在来看看怎样为链表增加结点,Redis只提供了在链表头部或尾部添加结点的方法,可能这些对Redis已经够用了吧。

list* listAddNodeHead(list* list, void *value); 
     
adlist.c       
list      *     listAddNodeHead(list      * 
    list,  
    void 
      
    * 
    value)
{
 listNode      *     node;

     if      ((node      = 
     zmalloc( 
    sizeof 
    ( 
    * 
    node)))  
    == 
     NULL)
     return      NULL;
 node     ->     value      = 
     value;
     if      (list     -> 
    len  
    == 
      
    0 
    ) {
 list     ->     head      = 
     list 
    -> 
    tail  
    = 
     node;
 node     ->     prev      = 
     node 
    -> 
    next  
    = 
     NULL;
 }      else      {
 node     ->     prev      = 
     NULL;
 node     ->     next      = 
     list 
    -> 
    head;
 list     ->     head     -> 
    prev  
    = 
     node;
 list     ->     head      = 
     node;
 }
 list     ->     len     ++ 
    ;
     return      list;
}

我们只需要为链表提供真正的数据value即可,该函数会自己创建listNode结构。这个函数的声明有点儿意思,它返回的是list指针,通常我们会返回int,用0表示成功,-1表示失败。而Redis中如果链表插入失败会返回NULL,原来的链表不会受影响;如果插入成功,那么就返回改变后的链表。listAddNodeTail类似。

 

有插入自然就有删除咯,插入时我们只提供了listNode中value,但删除就要提供listNode的地址了。与listRelease一样,如果value指向的内存在堆中,就要提供你自己定义的free函数,否则会有内存泄漏。

void listDelNode(list *list, listNode *node); 
     
adlist.c

这个函数为什么不像插入函数一样返回list指针呢,源代码中说这个函数不可能失败,所以没必要返回值。不过我认为返回list指针也好啊,可以保持接口的一致性。不知道antirez是怎么想的,难道是考虑性能。 

 

 

Redis中是通过迭代器访问结点的,同STL一样,那么必须要有个为链表生成迭代器的函数

listIter* listGetIter(list *list, int direction);
     
adlist.c       
listIter      *     listGetIterator(list      *     list,      int 
     direction)
{
 listIter      *     iter;

     if      ((iter      =      zmalloc(     sizeof 
    ( 
    * 
    iter)))  
    == 
     NULL)  
    return 
     NULL;
     if      (direction      ==      AL_START_HEAD)
 iter     ->     next      =      list     -> 
    head;
     else     
 iter     ->     next      =      list     -> 
    tail;
 iter     ->     direction      =      direction;
     return      iter;
}

 

迭代器不是链表的一部分,而是帮助程序员操作链表结点的,所以它需要一个单独的析构函数

void listReleaseIterator(listIter *iter);
     
void     listReleaseIterator(listIter     *    iter) {
 zfree(iter);
}


现在我们要通过迭代器访问链表结点,这个函数或许是理解链表最重要的吧,他会返回迭代器当前所指的结点,并且根据direction前进或后退

listNode* listNext(listIter *iter);  
      
adlist.c  

       iter = listGetIterator(list); 
  
    while ( (node=listNext(iter))!=NULL ) {   
        doSomethingWith(listNodeValue(node));  //listNodeValue是adlist.h中的宏,从结点中取出值    
    }


我觉得这个框架有点儿别扭,与STL好像不太一样,关于这个接口的设计我会在总结中谈到。


 

 

 

 


总结:


 


通用链表结构的基本API已经讲述完毕,想要透彻了解最好去看Redis的源码。但是从listDelNode开始,我嗅到了一丝丝的不和谐,listDelNode需要我们传一个listNode型的指针,但是在一个好的通用链表中用户的概念中应该只有三种数据结构:链表,迭代器,和与具体应用相关的value的类型。在用户的概念之中value应该就是链表中的结点,而没有必要知道value其实是listNode中的一个指针,所以在用户接口中listNode根本就不应该出现。基于这样的理念,我们可以更改两个接口:
  

void listDelNode(listNode *node) 应改为 void listDelNode(listIter *iter)   
    listNode* listNext(listIter *iter)应改为 listIter* listNext(listIter *iter)


这样遍历框架就成了

iter = listGetIter(list, direction);   
    while ( !listIterNull(iter) ) {   
        doSomethingWith(listIterValue(iter));   
        listNext(iter);   
    }


 看,这样遍历框架是不是顺眼多了。我们还要实现框架中的listIterNull和listIterValue,都很简单
 

#define listIterNull(iter) (iter->next)   
    #define listIterValue(iter) (iter->next->value)