redis代码结构之三类型库-list

来源:互联网 发布:淘宝工具软件 编辑:程序博客网 时间:2024/06/05 07:33

redis代码结构之三类型库-list 

1. REDIS_LIST(t_list.c)

该类型的命令包括:lpush,rpush,lpop,rpop等等。这里我们只介绍lpush命令,它相应的命令回调函数为void lpushCommand(redisClient *c) { pushGenericCommand(c,REDIS_HEAD);}我们直接看pushGenericCommand
[cpp] view plaincopy
  1. void pushGenericCommand(redisClient *c, int where) {  
  2.     int j, addlen = 0, pushed = 0;  
  3.     robj *lobj = lookupKeyWrite(c->db,c->argv[1]); //查找key是否存在,存在的话返回的是它的subobject  
  4.     int may_have_waiting_clients = (lobj == NULL); //key不存在  
  5.   
  6.     if (lobj && lobj->type != REDIS_LIST) {//如果key存在,则它的type必定是REDIS_LIST  
  7.         addReply(c,shared.wrongtypeerr);  
  8.         return;  
  9.     }  
  10.   
  11.     for (j = 2; j < c->argc; j++) {  
  12.         c->argv[j] = tryObjectEncoding(c->argv[j]);  
  13.         if (may_have_waiting_clients) {  
  14.             if (handleClientsWaitingListPush(c,c->argv[1],c->argv[j])) { //判断是否有block的pop key,这种情况是由于blpop引起的  
  15.                 addlen++;  
  16.                 continue;  
  17.             } else {  
  18.                 may_have_waiting_clients = 0;  
  19.             }  
  20.         }  
  21.         if (!lobj) { //如果该key现在没有value,只调用一次  
  22.             lobj = createZiplistObject(); //一开始总是使用ziplist,并赋值,跟进去可以看到其实ziplist是字符串数组  
  23.             dbAdd(c->db,c->argv[1],lobj); //将key,robj value加入dict  
  24.         }  
  25.         listTypePush(lobj,c->argv[j],where); //将每个value加入到value list,并且该函数会判断是否需要转换存储类型  
  26.         pushed++;  
  27.     }  
  28.     addReplyLongLong(c,addlen + (lobj ? listTypeLength(lobj) : 0));  
  29.     if (pushed) signalModifiedKey(c->db,c->argv[1]);  
  30.     server.dirty += pushed;  
  31. }  
上面的几个value有点别扭,其实通过key得到的都是一个robj value,而这个里面又包含了一个list或ziplist,这个list又是由多个robj value对象组成。下面我们看一下listTypePush函数:
[cpp] view plaincopy
  1. void listTypePush(robj *subject, robj *value, int where) {  
  2.     /* Check if we need to convert the ziplist */  
  3.     listTypeTryConversion(subject,value); //查看是否需要将ziplist转换为普通的LINKEDLIST  
  4.     //如果当前是ziplist类型,并且长度已经大于配置的list_max_ziplist_entries时,则将ziplist转换为LINKEDLIST  
  5.     if (subject->encoding == REDIS_ENCODING_ZIPLIST &&  
  6.         ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)   
  7.             listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);  
  8.   
  9.     if (subject->encoding == REDIS_ENCODING_ZIPLIST) {//仍然可以使用ziplist  
  10.         int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;  
  11.         value = getDecodedObject(value); //获得string类型  
  12.         subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);//ziplist的插入  
  13.         decrRefCount(value);  
  14.     } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {//list类型,就是我们常见的双向链表插入,这里我们不再解释  
  15.         if (where == REDIS_HEAD) {  
  16.             listAddNodeHead(subject->ptr,value);  
  17.         } else {  
  18.             listAddNodeTail(subject->ptr,value);  
  19.         }  
  20.         incrRefCount(value);  
  21.     } else {  
  22.         redisPanic("Unknown list encoding");  
  23.     }  
  24. }  
  25.     //转换类型,如果要保存的value的长度大于配置的list_max_ziplist_value时也必须把原来的ziplist转换为list  
  26. void listTypeTryConversion(robj *subject, robj *value) {  
  27.     if (subject->encoding != REDIS_ENCODING_ZIPLIST) return;  
  28.     if (value->encoding == REDIS_ENCODING_RAW &&  
  29.         sdslen(value->ptr) > server.list_max_ziplist_value)  
  30.             listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);  
  31. }  
通过上面的代码我们可以发现当要插入的value的长度(实际字节长度)大于配置的list_max_ziplist_value时或者ziplist的长度(元素大小)大于server.list_max_ziplist_entries时把ziplist转换为list类型。下面我们主要介绍一下ziplist的相关操作。

1.1   ziplist

[cpp] view plaincopy
  1. /* Create a new empty ziplist. */  
  2. unsigned char *ziplistNew(void) {  
  3.     unsigned int bytes = ZIPLIST_HEADER_SIZE+1;  
  4.     unsigned char *zl = zmalloc(bytes);  
  5.     ZIPLIST_BYTES(zl) = bytes;  
  6.     ZIPLIST_TAIL_OFFSET(zl) = ZIPLIST_HEADER_SIZE;  
  7.     ZIPLIST_LENGTH(zl) = 0;  
  8.     zl[bytes-1] = ZIP_END;  
  9.     return zl;  
  10. }  
  11.   
  12. //往ziplist中增加entry,zl就是ziplist字符串数组;s为要插入的value的实际内容;slen为value长度,where是首或尾  
  13. unsigned char *ziplistPush(unsigned char *zl, unsigned char *s, unsigned int slen, int where) {  
  14.     unsigned char *p;//p为实际把插入的位置head的话就是从header后开始,TAIL就是从最后的一个字节处开始  
  15.     p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);  
  16.     return __ziplistInsert(zl,p,s,slen);  
  17. }  

通过上面的new可以看到ziplist的结构:ziplist是一个字符串数组,它的格式为:
[header:<zlbytes><zltail><zllen>][body:<entry>…<entry>][end:<zlend>],其中前面三个又称为ziplist header
zlbytes: uint32_t,保存该字符串数组的总长度,包括header,body,end;
zltail:uint32_t,用来保存最后一个entry的偏移,即最后一个节点的位置,允许pop操作,而不需要遍历
zllen:uint16_t,用来保存ziplist的entry个数,所以最多2^16-1个。
zlend:1byte结束标志[255]
下面我们再来看一下body:entry的结构:(每个body也是由三个部分组成)
prevrawlen:保存前一个节点的长度(这个len就是一个entry总长度),占用1或5个字节,当len少于254时占1个字节,否则前面一个字节保存254,后面4个字节保存实际的长度
lensize[curencode|curlen]:保存当前节点使用的encode类型,如果是字符串类型还必须包括长度;整型的话不需要长度,因为整型的长度是固定的。该字段可能占用:1,2,5,1,1,1个字节。当内容不能转换为long long的时候,必须使用字符串来encode,此时如果串的长度小于63(2^6-1),则可以使用ZIP_STR_06B来encode,这个字段就占用1个字节,其中前面两位表示ZIP_STR_06B(00),后面6位表示实际的长度;如果串的长度小于16384(2^14-1),则可以使用ZIP_STR_14B来encode,这个字段就占用2个字节,其中前面两位表示ZIP_STR_14B(01),后面14位表示实际的长度;如果串的长度大于等于16384,则使用ZIP_STR_32B来encode,这个字段就占用5个字节,其中前面一个字节表示ZIP_STR_32B(1000,0000),后面4个字节(32位)表示实际的长度;
如果内容能够转换为long long的时候,则把它转换为相应的类型int16_t,int32_t,int64_t,这时该字段就只需要占用一个字节,它们的高4位用来表示encode类型,分别(1100,1101,1110),此时不再需要内容的长度,因为每个int,它的大小是可能通过类型来获得的。
value:保存实际的内容,它就根据前面的encode来存储的。占用的长度,如果是字符串,则由串的长度决定,如果的整形,则由每种类型自己决定(2,4,8)。如下图:


图1 ziplist结构

下面我们看一下ziplist是怎么进行插入:

[cpp] view plaincopy
  1. static zlentry zipEntry(unsigned char *p) {  
  2.     zlentry e;  
  3.     e.prevrawlen = zipPrevDecodeLength(p,&e.prevrawlensize); //返回上一个节点的总共长度,并且把保存这个长度值所占用的字节数保存到e.prevrawlensize,即1或者5(见上面的解释)  
  4.     e.len = zipDecodeLength(p+e.prevrawlensize,&e.lensize); //返回当前节点的内容的长度,并且把lensize占用的大小保存到e.lensize,即1,2,5,1,1,1  
  5.     e.headersize = e.prevrawlensize+e.lensize; //即每个节点的前面两个字段的长度,这个值加上上面的e.len就是该节点的实际长度  
  6.     e.encoding = zipEntryEncoding(p+e.prevrawlensize); //返回当前节点的encode  
  7.     e.p = p;  
  8.     return e;  
  9. }  
  10. //zl为要操作的ziplist,p为当前操作的位置,该值会根据head|tail而不同,如果是head则是ziplist: header之后的位置;如果是tail则在ziplist:end:<zlend>的位置,s为实际内容,slen为实际长度  
  11. static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p, unsigned char *s, unsigned int slen) {  
  12.     size_t curlen = ZIPLIST_BYTES(zl), reqlen, prevlen = 0;  
  13.     size_t offset;  
  14.     int nextdiff = 0;  
  15.     unsigned char encoding = 0;  
  16.     long long value;  
  17.     zlentry entry, tail;  
  18.   
  19.     /* Find out prevlen for the entry that is inserted. */  
  20.     if (p[0] != ZIP_END) {//这说明是从header insert  
  21.         entry = zipEntry(p); //见上面的注释  
  22.         prevlen = entry.prevrawlen; //返回上一个节点的长度,如果从head insert的话显然这个都是0  
  23.     } else {//从tail insert  
  24.         unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl); //获得最后一个节点的起始位置  
  25.         if (ptail[0] != ZIP_END) { //如果当前list非空  
  26.             prevlen = zipRawEntryLength(ptail); //获得最后一个节点的总共空间大小  
  27.         }  
  28.     }  
  29.   
  30.     /* See if the entry can be encoded 计算出存储该内容实际应该占用多少字节 reqlen为该节点总共要使用的空间*/  
  31.     if (zipTryEncoding(s,slen,&value,&encoding)) { //整形2,4,8  
  32.         /* 'encoding' is set to the appropriate integer encoding */  
  33.         reqlen = zipIntSize(encoding);  
  34.     } else { //字符串实际的长度  
  35.         /* 'encoding' is untouched, however zipEncodeLength will use the 
  36.          * string length to figure out how to encode it. */  
  37.         reqlen = slen;  
  38.     }  
  39.     /* We need space for both the length of the previous entry and 
  40.      * the length of the payload. */  
  41.     reqlen += zipPrevEncodeLength(NULL,prevlen); //保存上一个节点长度这个值需要占用多少空间,即body第一个字段占有用的空间1或5  
  42.     reqlen += zipEncodeLength(NULL,encoding,slen);//body第二个字段占用的空间1,2,5,1,1,1  
  43.   
  44.     /* When the insert position is not equal to the tail, we need to 
  45.      * make sure that the next entry can hold this entry's length in 
  46.      * its prevlen field. */  
  47.     nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p,reqlen) : 0; //如果是head insert的话,显然如果此时的头节点的prevrawlen就要改变,并且当要插入的节点的长度>245时,就需要增加4个字节,原来的头因为它的前一个是空所以只有一个字节保存0,而现在需要5个字节来保存这个新插入的节点的长度  
  48.   
  49.     /* Store offset because a realloc may change the address of zl. */  
  50.     offset = p-zl;  
  51.     zl = ziplistResize(zl,curlen+reqlen+nextdiff); //重新分配内存reqlen是当前插入节点的长度,nextdiff为需要增加的长度  
  52.     p = zl+offset;  
  53.   
  54.     /* Apply memory move when necessary and update tail offset. */  
  55.     if (p[0] != ZIP_END) { //header insert,显然之前的内容还在前面,所以需要把它们往后移  
  56.         /* Subtract one because of the ZIP_END bytes */  
  57.         memmove(p+reqlen,p-nextdiff,curlen-offset-1+nextdiff); //这里使用nextdiff是为了说明这nextdiff也是属于之前的节点的内容  
  58.   
  59.         /* Encode this entry's raw length in the next entry. */  
  60.         zipPrevEncodeLength(p+reqlen,reqlen); //更新之前head node的prevlen  
  61.   
  62.         /* Update offset for tail */  
  63.         ZIPLIST_TAIL_OFFSET(zl) += reqlen;  
  64.   
  65.         /* When the tail contains more than one entry, we need to take 
  66.          * "nextdiff" in account as well. Otherwise, a change in the 
  67.          * size of prevlen doesn't have an effect on the *tail* offset. */  
  68.         tail = zipEntry(p+reqlen);  
  69.         if (p[reqlen+tail.headersize+tail.len] != ZIP_END)  
  70.             ZIPLIST_TAIL_OFFSET(zl) += nextdiff;  
  71.     } else { //tail insert  
  72.         /* This element will be the new tail. */  
  73.         ZIPLIST_TAIL_OFFSET(zl) = p-zl;  
  74.     }  
  75.   
  76.     /* When nextdiff != 0, the raw length of the next entry has changed, so 
  77.      * we need to cascade the update throughout the ziplist */  
  78.     if (nextdiff != 0) { //如果新插入的节点len大于254时,即前一个节点的prevlen需要扩展到5个节点来保存时,需要依次去遍历整个list去修改每个node的prevlen,当前这个遍历可能不会很长,因为从head开始也就是增加4个字节,当某一个prevlen+4小于254时,该遍历就结束了  
  79.         offset = p-zl;  
  80.         zl = __ziplistCascadeUpdate(zl,p+reqlen);  
  81.         p = zl+offset;  
  82.     }  
  83.   
  84.     /* Write the entry */  
  85.     p += zipPrevEncodeLength(p,prevlen); //保存当前插入的新节点的prevlen  
  86.     p += zipEncodeLength(p,encoding,slen); //保存当前插入新节点的lensize  
  87.     if (ZIP_IS_STR(encoding)) { //保存实际的内容  
  88.         memcpy(p,s,slen);  
  89.     } else {  
  90.         zipSaveInteger(p,value,encoding);  
  91.     }  
  92.     ZIPLIST_INCR_LENGTH(zl,1);  
  93.     return zl;  
  94. }  
总而言之,ziplist其本质是一个字符串数组,它通过保存每个节点的上一个节点的大小,以及当前结节的encode及实际长度,来达到两个方面的遍历。另外,从上面的操作可以看出当从head插入的时候,整个insert需要更多的过程:memmove,计算扩展值nextdiff,从而导致遍历性的修改整个list的prevlen。但不管是从head还tail都需要一个realloc的过程,所以虽然ziplist可以节省内存,但是它是以cpu换mem。如果想减少使用,可以改小上面说的两个配置。并且尽量使用rpush的操作,来得到更好的性能。
0 0
原创粉丝点击