原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51111230

今天为大家带来Redis中zipmap数据结构的分析,该结构定义在zipmap.h和zipmap.c文件里。我把zipmap称作“压缩字典”(不知道这样称呼正不对)是因为zipmap利用字符串实现了一个简单的hash_table结构,又通过固定的字节表示节省空间。

zipmap和前面介绍的ziplist结构十分相似,我们能够对照地进行学习:

  • Redis中定义了双向链表list,可是这样的链表的每一个节点须要存储两个指针来指向前一个节点和下一个节点,存在着空间利用率不高的缺点(特别是在存储少量数据的情况下)。为了解决问题,Redis引入了压缩列表ziplist,通过存储上一个节点长度和当前节点长度从而在大多数情况下节省了链表指针的存储空间开销。

  • Redis中定义了字典dict来实现非常多高级语言中的Map结构。这样的结构相对来说比較复杂,并且须要两张散列表来实现rehash操作。在存储少量元素的时候空间利用率也比較低。为此,Redis引入了压缩字典zipmap,就是我们今天要介绍的数据结构。

    zipmap实质上是用一个字符串来依次保存key和value。有良好的空间利用率和相对不错的查找速度(以下我们会详细解说)。

假设你还不知道什么是ziplist和dict,请參看我前面的博文:

Redis是基于内存的高效的数据库。从list到ziplist,从dict到zipmap,我们能够看到Redis的作者们为了高效地利用内存空间真是无所不用其极。

以下我们就进入今天的主题,介绍zipmap的实现。

再次强调。zipmap和ziplist有非常多相似的地方,请大家注意比对学习。


1、zipmap的存储结构

zipmap是用连续内存保存key-value对的结构,查询时是依次遍列每一个key-value对,直到查到为止。其存储结构例如以下所看到的:

各部分的含义例如以下:

(1)、zmlen字段

zmlen是1个字节的无符号整型数,表示zipmap当前的键值对数量,最多仅仅能表示253个键值对。当zipmap中的元素数量大于或等于254时,仅仅能通过遍历一遍zipmap来确定其大小。

(2)、key length和value length字段

这两个length字段的编码方式和ziplist有些相似。能够用1个字节或5个字节编码表示,详细例如以下:

  • 假设随后的字符串(key或value)长度小于或等于253,直接用1个字节表示其长度。
  • 假设随后的字符串(key或value)长度超过254。则用5个字节表示,当中第一个字节值为254,接下来的4个字节才是字符串长度。

(3)、free字段

free表示随后的value后面的空暇字节数。

比方:假设zipmap存在”foo” => “bar”这样一个键值对,随后我们将“bar”设置为“hi”。此时free = 1,表示value字符串后面有1个字节大小的空暇空间。

free字段是一个占1个字节的整型数,它的值一般都比較小。假设空暇区间太大,zipmap会进行调整以使整个map尽可能小,这个过程发生在zipmapSet操作中。

(4)、end字段

end是zipmap的结尾符,占用1个字节,其值为255。

从zipmap的存储结构中我们能够看到,zipmap中的键值对就是顺序存放的key和value字符串。中间加入了相关的字符串长度编码信息以方便我们获取相关的key和value。

而整个zipmap就是在一块内存区中依次存放key-value字符串。

2、zipmap的几个重要操作

与ziplist相比。zipmap的操作要简单很多,Redis甚至未定义额外的结构体。仅仅是通过简单的字符串操作来维护一个zipmap结构。以下我们就来介绍一下zipmap相关的几个操作:

2.1、创建一个空zipmap

一个空的zipmap结构仅仅有两个字节(即zmlen字段和end字段)。结构例如以下:

创建空的zipmap非常easy,这里拿出来讲是为了让大家更好地理解zipmap的存储结构。

  1. unsigned char *zipmapNew(void) {
  2. // 初始化时仅仅有2个字节,第1个字节表示zipmap保存的key-value对的个数。第2个字节为结尾符
  3. unsigned char *zm = zmalloc(2);
  4. // 当前保存的键值对个数为0
  5. zm[0] = 0; /* Length */
  6. zm[1] = ZIPMAP_END;
  7. return zm;
  8. }

所以zipmap是不是非常easy?

2.2、查找操作

作为一个hash_table结构。其最基本的操作是依据给定的key值查找对应的value值。

在zipmap中。key-value是顺序存放的,所以仅仅能从前往后遍历以查找目标键值对,但因为每一个键值对中都记录了key和value的长度,所以查找起来也非常方便。

详细例如以下所看到的:

  1. /* 按关键字key查找zipmap,假设totlen不为NULL,函数返回后存放zipmap占用的字节数 */
  2. static unsigned char *zipmapLookupRaw(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned int *totlen) {
  3. // zipmap中第1个字节是zmlen字段。zm+1跳过第1个字节
  4. unsigned char *p = zm+1, *k = NULL;
  5. unsigned int l,llen;
  6. // 从前往后查找
  7. while(*p != ZIPMAP_END) {
  8. unsigned char free;
  9. /* Match or skip the key */
  10. // 确定key字符串的长度
  11. l = zipmapDecodeLength(p);
  12. // 确定保存key字符串长度所须要的字节数,也就是len字段所须要的字节数
  13. llen = zipmapEncodeLength(NULL,l);
  14. // 比較当前key与给定key是否匹配
  15. if (key != NULL && k == NULL && l == klen && !memcmp(p+llen,key,l)) {
  16. /* Only return when the user doesn't care
  17. * for the total length of the zipmap. */
  18. // 假设totlen为NULL。表示函数调用者不关心zipmap占用的字节数,此时直接返回p,否则先记录下p指针然后继续遍历
  19. if (totlen != NULL) {
  20. k = p;
  21. } else {
  22. return p;
  23. }
  24. }
  25. // p加上llen和l。到了value节点处
  26. p += llen+l;
  27. /* Skip the value as well */
  28. // 确定value字符串的长度
  29. l = zipmapDecodeLength(p);
  30. // 确定保存value字符串长度所须要的字节数,也就是len字段所须要的字节数
  31. p += zipmapEncodeLength(NULL,l);
  32. // 读出free字段的值(前面我们讲过:free仅仅占用一个字节)
  33. free = p[0];
  34. // 跳到下一个key节点的
  35. p += l+1+free; /* +1 to skip the free byte */
  36. }
  37. // 到这里遍历完整个zipmap。得到其占用的字节数
  38. if (totlen != NULL) *totlen = (unsigned int)(p-zm)+1;
  39. return k;
  40. }

2.3、插入 & 更新操作

zipmap将插入和更新操作实如今同一个函数zipmapSet中,即存在指定key的键值对则运行更新操作,否则运行插入操作。

函数zipmapSet的运行流程例如以下:

  • 调用zipmapLookupRaw函数确定指定key的键值对是否已经在zipmap中。
  • 又一次分配空间以确保有足够的空间来容纳新的键值对(插入操作)或新的value值(更新操作)。假设是插入操作,就在zipmap尾部添加空间,假设是更新操作,则通过memmove移动在原键值对处留出足够的空间。

  • 在前面两步中已经确定了键值对的位置并有足够的空间。直接写入或更新键值对的信息就可以。
  1. /* 依据key设置value,假设key不存在则创建对应的键值对。參数update用来辨别更新操作和加入操作。 */
  2. unsigned char *zipmapSet(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned char *val, unsigned int vlen, int *update) {
  3. unsigned int zmlen, offset;
  4. // 计算存储key和value所须要的字节数
  5. unsigned int freelen, reqlen = zipmapRequiredLength(klen,vlen);
  6. unsigned int empty, vempty;
  7. unsigned char *p;
  8. /************************************************************************
  9. * 以下这段代码用于在zipmap留出足够的空间来容纳新插入的键值对或新的value值。尚未写入
  10. ************************************************************************/
  11. freelen = reqlen;
  12. if (update) *update = 0;
  13. // 在zipmap中查找key。函数返回后zmlen中保存了zipmap所占用的字节数。
  14. p = zipmapLookupRaw(zm,key,klen,&zmlen);
  15. if (p == NULL) {
  16. /* Key not found: enlarge */
  17. // 假设key指定的键值对不存在,则对zipmap扩容,为容纳新的键值对准备内存空间
  18. // zipmapResize运行的是realloc操作
  19. zm = zipmapResize(zm, zmlen+reqlen);
  20. // 此时p指向扩容前zipmap的结尾符,将从这里加入新的键值对
  21. p = zm+zmlen-1;
  22. // 更新zipmap所占用的内存空间大小
  23. zmlen = zmlen+reqlen;
  24. /* Increase zipmap length (this is an insert) */
  25. // 更新zipmap中保存的键值对数量,即zmlen字段
  26. if (zm[0] < ZIPMAP_BIGLEN) zm[0]++;
  27. } else {
  28. /* Key found. Is there enough space for the new value? */
  29. /* 找到可对应的键值对。运行更新操作。这里须要考虑value节点的空间大小能否够容纳新值 */
  30. /* Compute the total length: */
  31. if (update) *update = 1;
  32. // 求出旧value节点的空间大小
  33. freelen = zipmapRawEntryLength(p);
  34. if (freelen < reqlen) {
  35. /* Store the offset of this key within the current zipmap, so
  36. * it can be resized. Then, move the tail backwards so this
  37. * pair fits at the current position. */
  38. // 旧节点的空间太小,须要扩容操作,zipmapResize函数会又一次分配空间,所以须要记录p指针的偏移量
  39. offset = p-zm;
  40. zm = zipmapResize(zm, zmlen-freelen+reqlen);
  41. p = zm+offset;
  42. /* The +1 in the number of bytes to be moved is caused by the
  43. * end-of-zipmap byte. Note: the *original* zmlen is used. */
  44. // 移动旧value节点以后的元素以确保有足够的空间容纳新值( +1是将尾部结尾符一起移动)
  45. memmove(p+reqlen, p+freelen, zmlen-(offset+freelen+1));
  46. zmlen = zmlen-freelen+reqlen;
  47. freelen = reqlen;
  48. }
  49. }
  50. /* We now have a suitable block where the key/value entry can
  51. * be written. If there is too much free space, move the tail
  52. * of the zipmap a few bytes to the front and shrink the zipmap,
  53. * as we want zipmaps to be very space efficient. */
  54. // freelen表示经上步骤后流出来的空余空间大小,reqlen表示插入或更新键值对所须要的空间。两者的差就是free字段的
  55. // 的值,假设该值过大zipmap会自己主动调整。以下这段代码就是完毕调整功能。
  56. empty = freelen-reqlen;
  57. if (empty >= ZIPMAP_VALUE_MAX_FREE) {
  58. /* First, move the tail <empty> bytes to the front, then resize
  59. * the zipmap to be <empty> bytes smaller. */
  60. offset = p-zm;
  61. memmove(p+reqlen, p+freelen, zmlen-(offset+freelen+1));
  62. zmlen -= empty;
  63. zm = zipmapResize(zm, zmlen);
  64. p = zm+offset;
  65. vempty = 0;
  66. } else {
  67. vempty = empty;
  68. }
  69. /******************************************
  70. * 以下的操作是讲key和value写入zipmap指定位置
  71. *******************************************/
  72. /* Just write the key + value and we are done. */
  73. /* Key: */
  74. // 对key的长度编码并写入zipmap中
  75. p += zipmapEncodeLength(p,klen);
  76. // 写入key字符串
  77. memcpy(p,key,klen);
  78. // 移动指针到value写入位置
  79. p += klen;
  80. /* Value: */
  81. // 对value的长度编码并写入zipmap中
  82. p += zipmapEncodeLength(p,vlen);
  83. // 写入free字段
  84. *p++ = vempty;
  85. // 写入value
  86. memcpy(p,val,vlen);
  87. return zm;
  88. }

3、总结

zipmap主要是为了节省内存空间而设计的字符串-字符串映射结构(哈希结构)。它的实现非常easy,代码仅仅有400余行。

zipmap通过在一块连续的内存空间上依次存放key-value对来维持key和value的映射结构,但它跟我们传统意义上的散列结构不同。仅仅能通过依次遍历每一个key-value节点来查找给定key值得value信息。

可是。相比于字典dict,zipmap通过固定的字节编码表示节省了不少空间。另外。因为使用了一块连续的内存空间,zipmap的每一次插入、删除、更新操作都有可能造成空间的又一次分配。

从zipmap的实现上我们能够看到,zipmap不适合用来存放大量的key-value对。

在运用上,Redis中内置的Hash结构在保存的元素数量较少时会採用zipmap来存放键值对,当元素的数量到达给定值后才会转为用哈希表来存储以节省内存。


以下贴出凝视版的代码,供大家參考:

  1. /* String -> String Map data structure optimized for size.
  2. * zipmap是为了节省内存空间而设计的字符串-字符串映射结构(哈希结构)
  3. *
  4. * This file implements a data structure mapping strings to other strings
  5. * implementing an O(n) lookup data structure designed to be very memory
  6. * efficient.
  7. * zipmap实现了一种将字符串与字符串之间映射起来的数据结构,它支持O(n)的查找效率并具有良好的空间利用率。
  8. *
  9. * The Redis Hash type uses this data structure for hashes composed of a small
  10. * number of elements, to switch to a hash table once a given number of
  11. * elements is reached.
  12. * Redis中内置的Hash结构在保存的元素数量较少时会採用zipmap来存放键值对,当元素的数量到达给定值
  13. * 后才会转为用哈希表来存储以节省内存。
  14. *
  15. * Given that many times Redis Hashes are used to represent objects composed
  16. * of few fields, this is a very big win in terms of used memory.
  17. * Redis中的Hash结构常常被用来保存仅仅有少量字段的对象,这样的情况下使用zipmap能非常大程度上节省内存
  18. *
  19. * --------------------------------------------------------------------------
  20. *
  21. * Copyright (c) 2009-2010, Salvatore Sanfilippo <antirez at gmail dot com>
  22. * All rights reserved.
  23. *
  24. * Redistribution and use in source and binary forms, with or without
  25. * modification, are permitted provided that the following conditions are met:
  26. *
  27. * * Redistributions of source code must retain the above copyright notice,
  28. * this list of conditions and the following disclaimer.
  29. * * Redistributions in binary form must reproduce the above copyright
  30. * notice, this list of conditions and the following disclaimer in the
  31. * documentation and/or other materials provided with the distribution.
  32. * * Neither the name of Redis nor the names of its contributors may be used
  33. * to endorse or promote products derived from this software without
  34. * specific prior written permission.
  35. *
  36. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  37. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  38. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  39. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  40. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  41. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  42. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  43. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  44. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  45. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  46. * POSSIBILITY OF SUCH DAMAGE.
  47. */
  48. /* Memory layout of a zipmap, for the map "foo" => "bar", "hello" => "world":
  49. * zipmap的内存空间布局例如以下(假设存放着"foo" => "bar", "hello" => "world"数据):
  50. *
  51. * <zmlen><len>"foo"<len><free>"bar"<len>"hello"<len><free>"world"
  52. *
  53. * <zmlen> is 1 byte length that holds the current size of the zipmap.
  54. * When the zipmap length is greater than or equal to 254, this value
  55. * is not used and the zipmap needs to be traversed to find out the length.
  56. * zmlen是1个字节的无符号整型数,表示zipmap当前的键值对数量。最多仅仅能表示253个键值对。当zipmap中的元素数量大于
  57. * 或等于254时。仅仅能通过遍历一遍zipmap来确定其大小。
  58. *
  59. * <len> is the length of the following string (key or value).
  60. * len表示随后的字符串的长度,这些字符串可能是key或者value
  61. *
  62. * <len> lengths are encoded in a single value or in a 5 bytes value.
  63. * If the first byte value (as an unsigned 8 bit value) is between 0 and
  64. * 253, it's a single-byte length. If it is 254 then a four bytes unsigned
  65. * integer follows (in the host byte ordering). A value of 255 is used to
  66. * signal the end of the hash.
  67. * len字段的编码方式和ziplist有些相似,能够用1个字节或5个字节编码表示,详细例如以下:
  68. * (1)假设随后的字符串长度小于或等于253。直接用1个字节表示其长度
  69. * (2)假设随后的字符串长度超过254,则用5个字节表示。当中第一个字节值为254,接下来的4个字节才是字符串长度
  70. *
  71. * <free> is the number of free unused bytes after the string, resulting
  72. * from modification of values associated to a key. For instance if "foo"
  73. * is set to "bar", and later "foo" will be set to "hi", it will have a
  74. * free byte to use if the value will enlarge again later, or even in
  75. * order to add a key/value pair if it fits.
  76. * free表示随后的value的空暇字节数。比方:假设zipmap存在"foo" => "bar"这样一个键值对,随后我们将“bar”设置为
  77. * ”hi",此时free = 1,表示value字符串后面有1个字节大小的空暇空间。
  78. *
  79. * <free> is always an unsigned 8 bit number, because if after an
  80. * update operation there are more than a few free bytes, the zipmap will be
  81. * reallocated to make sure it is as small as possible.
  82. * free字段是一个占1个字节的整型数,它的值一般都比較小,假设空暇区间太大。zipmap会进行调整以使整个map尽可能小。
  83. *
  84. * zipmap也存在一个结尾符。占用1个字节,其值为255。
  85. *
  86. * 从上面的介绍能够看出:zipmap实质上是用一个字符串数组来依次保存key和value,查询时是依次遍列每一个key-value对,直到查到为止。
  87. *
  88. * The most compact representation of the above two elements hash is actually:
  89. *
  90. * "\x02\x03foo\x03\x00bar\x05hello\x05\x00world\xff"
  91. *
  92. * Note that because keys and values are prefixed length "objects",
  93. * the lookup will take O(N) where N is the number of elements
  94. * in the zipmap and *not* the number of bytes needed to represent the zipmap.
  95. * This lowers the constant times considerably.
  96. */
  97. #include <stdio.h>
  98. #include <string.h>
  99. #include "zmalloc.h"
  100. #include "endianconv.h"
  101. /* 说明:为了描写叙述方便,这里将key-value键值对的key称为key节点,value称为value节点 */
  102. #define ZIPMAP_BIGLEN 254 // zipmap的元素个数超过253时的标识符
  103. #define ZIPMAP_END 255 // zipmap的结尾符
  104. /* The following defines the max value for the <free> field described in the
  105. * comments above, that is, the max number of trailing bytes in a value. */
  106. /* free字段的最大值,也就是value后面的最大空暇字节数 */
  107. #define ZIPMAP_VALUE_MAX_FREE 4
  108. /* The following macro returns the number of bytes needed to encode the length
  109. * for the integer value _l, that is, 1 byte for lengths < ZIPMAP_BIGLEN and
  110. * 5 bytes for all the other lengths. */
  111. /* 工具宏,用来确定len字段所占用的字节数。简单地測试第一个字节的值与254的大小关系 */
  112. #define ZIPMAP_LEN_BYTES(_l) (((_l) < ZIPMAP_BIGLEN) ?
  113. 1 : sizeof(unsigned int)+1)
  114. /* Create a new empty zipmap. */
  115. /* 创建一个空的zipmap结构 */
  116. unsigned char *zipmapNew(void) {
  117. // 初始化时仅仅有2个字节,第1个字节表示zipmap保存的key-value对的个数,第2个字节为结尾符
  118. unsigned char *zm = zmalloc(2);
  119. // 当前保存的键值对个数为0
  120. zm[0] = 0; /* Length */
  121. zm[1] = ZIPMAP_END;
  122. return zm;
  123. }
  124. /* Decode the encoded length pointed by 'p' */
  125. /* 获取len字段的数值(即随后字符串的长度),其原理非常easy:查看第一个字节的数值。假设该数值小于254,则直接返回,
  126. 否则读取接下来的4个字节内容表示的数值。 */
  127. static unsigned int zipmapDecodeLength(unsigned char *p) {
  128. unsigned int len = *p;
  129. if (len < ZIPMAP_BIGLEN) return len;
  130. // 读取随后4个字节的内容
  131. memcpy(&len,p+1,sizeof(unsigned int));
  132. // 统一转化为小端模式表示
  133. memrev32ifbe(&len);
  134. return len;
  135. }
  136. /* Encode the length 'l' writing it in 'p'. If p is NULL it just returns
  137. * the amount of bytes required to encode such a length. */
  138. /* 将长度len编码到p指针指向的内存空间 */
  139. static unsigned int zipmapEncodeLength(unsigned char *p, unsigned int len) {
  140. if (p == NULL) {
  141. return ZIPMAP_LEN_BYTES(len);
  142. } else {
  143. if (len < ZIPMAP_BIGLEN) {
  144. // 长度小于254。仅仅须要1个字节表示
  145. p[0] = len;
  146. return 1;
  147. } else {
  148. // 长度大于等于254。第一个字节赋值为254,接下来的4歌字节才是真正的长度值
  149. p[0] = ZIPMAP_BIGLEN;
  150. memcpy(p+1,&len,sizeof(len));
  151. memrev32ifbe(p+1);
  152. return 1+sizeof(len);
  153. }
  154. }
  155. }
  156. /* Search for a matching key, returning a pointer to the entry inside the
  157. * zipmap. Returns NULL if the key is not found.
  158. *
  159. * If NULL is returned, and totlen is not NULL, it is set to the entire
  160. * size of the zimap, so that the calling function will be able to
  161. * reallocate the original zipmap to make room for more entries. */
  162. /* 按关键字key查找zipmap,假设totlen不为NULL,函数返回后存放zipmap占用的字节数 */
  163. static unsigned char *zipmapLookupRaw(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned int *totlen) {
  164. // zipmap中第1个字节是zmlen字段,zm+1跳过第1个字节
  165. unsigned char *p = zm+1, *k = NULL;
  166. unsigned int l,llen;
  167. // 从前往后查找
  168. while(*p != ZIPMAP_END) {
  169. unsigned char free;
  170. /* Match or skip the key */
  171. // 确定key字符串的长度
  172. l = zipmapDecodeLength(p);
  173. // 确定保存key字符串长度所须要的字节数。也就是len字段所须要的字节数
  174. llen = zipmapEncodeLength(NULL,l);
  175. // 比較当前key与给定key是否匹配
  176. if (key != NULL && k == NULL && l == klen && !memcmp(p+llen,key,l)) {
  177. /* Only return when the user doesn't care
  178. * for the total length of the zipmap. */
  179. // 假设totlen为NULL,表示函数调用者不关心zipmap占用的字节数,此时直接返回p,否则先记录下p指针然后继续遍历
  180. if (totlen != NULL) {
  181. k = p;
  182. } else {
  183. return p;
  184. }
  185. }
  186. // p加上llen和l,到了value节点处
  187. p += llen+l;
  188. /* Skip the value as well */
  189. // 确定value字符串的长度
  190. l = zipmapDecodeLength(p);
  191. // 确定保存value字符串长度所须要的字节数,也就是len字段所须要的字节数
  192. p += zipmapEncodeLength(NULL,l);
  193. // 读出free字段的值(前面我们讲过:free仅仅占用一个字节)
  194. free = p[0];
  195. // 跳到下一个key节点的
  196. p += l+1+free; /* +1 to skip the free byte */
  197. }
  198. // 到这里遍历完整个zipmap,得到其占用的字节数
  199. if (totlen != NULL) *totlen = (unsigned int)(p-zm)+1;
  200. return k;
  201. }
  202. /* 存储由长度为klen的key和长度为vlen的value组成的键值对所须要的字节数*/
  203. static unsigned long zipmapRequiredLength(unsigned int klen, unsigned int vlen) {
  204. unsigned int l;
  205. l = klen+vlen+3;
  206. if (klen >= ZIPMAP_BIGLEN) l += 4;
  207. if (vlen >= ZIPMAP_BIGLEN) l += 4;
  208. return l;
  209. }
  210. /* Return the total amount used by a key (encoded length + payload) */
  211. /* 获取key节点占用的字节数,即len字段 + key字符串长度 */
  212. static unsigned int zipmapRawKeyLength(unsigned char *p) {
  213. // 获取key字符串的长度
  214. unsigned int l = zipmapDecodeLength(p);
  215. // 加上保存key字符串长度所须要的字节数
  216. return zipmapEncodeLength(NULL,l) + l;
  217. }
  218. /* Return the total amount used by a value
  219. * (encoded length + single byte free count + payload) */
  220. /* 获取value节点占用的字节数,即len字段 + 1个字节free字段 + value字符串长度 + 空暇空间大小 */
  221. static unsigned int zipmapRawValueLength(unsigned char *p) {
  222. // 获取value字符串的长度
  223. unsigned int l = zipmapDecodeLength(p);
  224. unsigned int used;
  225. // 获取保存value字符串长度所须要的字节数
  226. used = zipmapEncodeLength(NULL,l);
  227. // p[used]里面存储着空暇空间的大小
  228. used += p[used] + 1 + l;
  229. return used;
  230. }
  231. /* If 'p' points to a key, this function returns the total amount of
  232. * bytes used to store this entry (entry = key + associated value + trailing
  233. * free space if any). */
  234. /* 假设p指向key节点,则该函数返回存储整个键值对所占用的字节数,包含key节点长度 + value节点长度 + 空暇字节数(假设有) */
  235. static unsigned int zipmapRawEntryLength(unsigned char *p) {
  236. unsigned int l = zipmapRawKeyLength(p);
  237. return l + zipmapRawValueLength(p+l);
  238. }
  239. /* 又一次调整zipmap的大小 */
  240. static inline unsigned char *zipmapResize(unsigned char *zm, unsigned int len) {
  241. // 又一次分配空间。注意是realloc,即在原空间又一次分配
  242. zm = zrealloc(zm, len);
  243. // 设置结尾符
  244. zm[len-1] = ZIPMAP_END;
  245. return zm;
  246. }
  247. /* Set key to value, creating the key if it does not already exist.
  248. * If 'update' is not NULL, *update is set to 1 if the key was
  249. * already preset, otherwise to 0. */
  250. /* 依据key设置value。假设key不存在则创建对应的键值对。參数update用来辨别更新操作和加入操作。 */
  251. unsigned char *zipmapSet(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned char *val, unsigned int vlen, int *update) {
  252. unsigned int zmlen, offset;
  253. // 计算存储key和value所须要的字节数
  254. unsigned int freelen, reqlen = zipmapRequiredLength(klen,vlen);
  255. unsigned int empty, vempty;
  256. unsigned char *p;
  257. /************************************************************************
  258. * 以下这段代码用于在zipmap留出足够的空间来容纳新插入的键值对或新的value值。尚未写入
  259. ************************************************************************/
  260. freelen = reqlen;
  261. if (update) *update = 0;
  262. // 在zipmap中查找key,函数返回后zmlen中保存了zipmap所占用的字节数。
  263. p = zipmapLookupRaw(zm,key,klen,&zmlen);
  264. if (p == NULL) {
  265. /* Key not found: enlarge */
  266. // 假设key指定的键值对不存在,则对zipmap扩容。为容纳新的键值对准备内存空间
  267. // zipmapResize运行的是realloc操作
  268. zm = zipmapResize(zm, zmlen+reqlen);
  269. // 此时p指向扩容前zipmap的结尾符,将从这里加入新的键值对
  270. p = zm+zmlen-1;
  271. // 更新zipmap所占用的内存空间大小
  272. zmlen = zmlen+reqlen;
  273. /* Increase zipmap length (this is an insert) */
  274. // 更新zipmap中保存的键值对数量。即zmlen字段
  275. if (zm[0] < ZIPMAP_BIGLEN) zm[0]++;
  276. } else {
  277. /* Key found. Is there enough space for the new value?
  278. */
  279. /* 找到可对应的键值对,运行更新操作。这里须要考虑value节点的空间大小能否够容纳新值 */
  280. /* Compute the total length: */
  281. if (update) *update = 1;
  282. // 求出旧value节点的空间大小
  283. freelen = zipmapRawEntryLength(p);
  284. if (freelen < reqlen) {
  285. /* Store the offset of this key within the current zipmap, so
  286. * it can be resized. Then, move the tail backwards so this
  287. * pair fits at the current position. */
  288. // 旧节点的空间太小,须要扩容操作,zipmapResize函数会又一次分配空间。所以须要记录p指针的偏移量
  289. offset = p-zm;
  290. zm = zipmapResize(zm, zmlen-freelen+reqlen);
  291. p = zm+offset;
  292. /* The +1 in the number of bytes to be moved is caused by the
  293. * end-of-zipmap byte. Note: the *original* zmlen is used. */
  294. // 移动旧value节点以后的元素以确保有足够的空间容纳新值( +1是将尾部结尾符一起移动)
  295. memmove(p+reqlen, p+freelen, zmlen-(offset+freelen+1));
  296. zmlen = zmlen-freelen+reqlen;
  297. freelen = reqlen;
  298. }
  299. }
  300. /* We now have a suitable block where the key/value entry can
  301. * be written. If there is too much free space, move the tail
  302. * of the zipmap a few bytes to the front and shrink the zipmap,
  303. * as we want zipmaps to be very space efficient. */
  304. // freelen表示经上步骤后流出来的空余空间大小,reqlen表示插入或更新键值对所须要的空间,两者的差就是free字段的
  305. // 的值。假设该值过大zipmap会自己主动调整。以下这段代码就是完毕调整功能。
  306. empty = freelen-reqlen;
  307. if (empty >= ZIPMAP_VALUE_MAX_FREE) {
  308. /* First, move the tail <empty> bytes to the front, then resize
  309. * the zipmap to be <empty> bytes smaller. */
  310. offset = p-zm;
  311. memmove(p+reqlen, p+freelen, zmlen-(offset+freelen+1));
  312. zmlen -= empty;
  313. zm = zipmapResize(zm, zmlen);
  314. p = zm+offset;
  315. vempty = 0;
  316. } else {
  317. vempty = empty;
  318. }
  319. /******************************************
  320. * 以下的操作是讲key和value写入zipmap指定位置
  321. *******************************************/
  322. /* Just write the key + value and we are done. */
  323. /* Key: */
  324. // 对key的长度编码并写入zipmap中
  325. p += zipmapEncodeLength(p,klen);
  326. // 写入key字符串
  327. memcpy(p,key,klen);
  328. // 移动指针到value写入位置
  329. p += klen;
  330. /* Value: */
  331. // 对value的长度编码并写入zipmap中
  332. p += zipmapEncodeLength(p,vlen);
  333. // 写入free字段
  334. *p++ = vempty;
  335. // 写入value
  336. memcpy(p,val,vlen);
  337. return zm;
  338. }
  339. /* Remove the specified key. If 'deleted' is not NULL the pointed integer is
  340. * set to 0 if the key was not found, to 1 if it was found and deleted. */
  341. /* 依据key删除指定的键值对 */
  342. unsigned char *zipmapDel(unsigned char *zm, unsigned char *key, unsigned int klen, int *deleted) {
  343. unsigned int zmlen, freelen;
  344. // 看推断该键值对是否在zipmap中,假设不存在则直接返回
  345. unsigned char *p = zipmapLookupRaw(zm,key,klen,&zmlen);
  346. if (p) {
  347. // 以下三句代码运行删除操作,事实上就是内存块的移动操作
  348. freelen = zipmapRawEntryLength(p);
  349. memmove(p, p+freelen, zmlen-((p-zm)+freelen+1));
  350. zm = zipmapResize(zm, zmlen-freelen);
  351. /* Decrease zipmap length */
  352. if (zm[0] < ZIPMAP_BIGLEN) zm[0]--;
  353. if (deleted) *deleted = 1;
  354. } else {
  355. if (deleted) *deleted = 0;
  356. }
  357. return zm;
  358. }
  359. /* Call before iterating through elements via zipmapNext() */
  360. /* zipmapNext迭代器函数,还记得前面我们分析过zipmap第一个字节是zmlen字段吗?以下这个函数就是跳过第一个字节返回
  361. 指向第一个键值对的首地址 */
  362. unsigned char *zipmapRewind(unsigned char *zm) {
  363. return zm+1;
  364. }
  365. /* This function is used to iterate through all the zipmap elements.
  366. * In the first call the first argument is the pointer to the zipmap + 1.
  367. * In the next calls what zipmapNext returns is used as first argument.
  368. * Example:
  369. *
  370. * unsigned char *i = zipmapRewind(my_zipmap);
  371. * while((i = zipmapNext(i,&key,&klen,&value,&vlen)) != NULL) {
  372. * printf("%d bytes key at $p\n", klen, key);
  373. * printf("%d bytes value at $p\n", vlen, value);
  374. * }
  375. */
  376. /* zipmap的迭代器式遍历函数,典型使用方法例如以下:
  377. *
  378. * unsigned char *i = zipmapRewind(my_zipmap);
  379. * while((i = zipmapNext(i,&key,&klen,&value,&vlen)) != NULL) {
  380. * printf("%d bytes key at $p\n", klen, key);
  381. * printf("%d bytes value at $p\n", vlen, value);
  382. * }
  383. */
  384. unsigned char *zipmapNext(unsigned char *zm, unsigned char **key, unsigned int *klen, unsigned char **value, unsigned int *vlen) {
  385. // 假设达到尾部。直接返回NULL
  386. if (zm[0] == ZIPMAP_END) return NULL;
  387. // 获取key
  388. if (key) {
  389. *key = zm;
  390. *klen = zipmapDecodeLength(zm);
  391. *key += ZIPMAP_LEN_BYTES(*klen);
  392. }
  393. zm += zipmapRawKeyLength(zm);
  394. // 获取value
  395. if (value) {
  396. // +1是为了跳过free字段,该字段占用一个字节
  397. *value = zm+1;
  398. *vlen = zipmapDecodeLength(zm);
  399. *value += ZIPMAP_LEN_BYTES(*vlen);
  400. }
  401. // 此时zm指向下一个键值对的首地址
  402. zm += zipmapRawValueLength(zm);
  403. return zm;
  404. }
  405. /* Search a key and retrieve the pointer and len of the associated value.
  406. * If the key is found the function returns 1, otherwise 0. */
  407. /* 依据key值查找对应的value值,事实上是对zipmapLookupRaw的包装 */
  408. int zipmapGet(unsigned char *zm, unsigned char *key, unsigned int klen, unsigned char **value, unsigned int *vlen) {
  409. unsigned char *p;
  410. if ((p = zipmapLookupRaw(zm,key,klen,NULL)) == NULL) return 0;
  411. p += zipmapRawKeyLength(p);
  412. *vlen = zipmapDecodeLength(p);
  413. *value = p + ZIPMAP_LEN_BYTES(*vlen) + 1;
  414. return 1;
  415. }
  416. /* Return 1 if the key exists, otherwise 0 is returned. */
  417. /* 推断某个key是否存在 */
  418. int zipmapExists(unsigned char *zm, unsigned char *key, unsigned int klen) {
  419. return zipmapLookupRaw(zm,key,klen,NULL) != NULL;
  420. }
  421. /* Return the number of entries inside a zipmap */
  422. /* 返回zipmap中键值对的个数,假设zmlen字段的值小于254。值zmlen的值就是所要求得返回值,否则须要遍历整个zipmap */
  423. unsigned int zipmapLen(unsigned char *zm) {
  424. unsigned int len = 0;
  425. if (zm[0] < ZIPMAP_BIGLEN) {
  426. len = zm[0];
  427. } else {
  428. unsigned char *p = zipmapRewind(zm);
  429. while((p = zipmapNext(p,NULL,NULL,NULL,NULL)) != NULL) len++;
  430. /* Re-store length if small enough */
  431. if (len < ZIPMAP_BIGLEN) zm[0] = len;
  432. }
  433. return len;
  434. }
  435. /* Return the raw size in bytes of a zipmap, so that we can serialize
  436. * the zipmap on disk (or everywhere is needed) just writing the returned
  437. * amount of bytes of the C array starting at the zipmap pointer. */
  438. /* 获取整个zipmap占用的字节数。事实上是对zipmapLookupRaw的包装 */
  439. size_t zipmapBlobLen(unsigned char *zm) {
  440. unsigned int totlen;
  441. zipmapLookupRaw(zm,NULL,0,&totlen);
  442. return totlen;
  443. }
  444. #ifdef ZIPMAP_TEST_MAIN
  445. /* 格式化输出函数 */
  446. void zipmapRepr(unsigned char *p) {
  447. unsigned int l;
  448. printf("{status %u}",*p++);
  449. while(1) {
  450. if (p[0] == ZIPMAP_END) {
  451. printf("{end}");
  452. break;
  453. } else {
  454. unsigned char e;
  455. l = zipmapDecodeLength(p);
  456. printf("{key %u}",l);
  457. p += zipmapEncodeLength(NULL,l);
  458. if (l != 0 && fwrite(p,l,1,stdout) == 0) perror("fwrite");
  459. p += l;
  460. l = zipmapDecodeLength(p);
  461. printf("{value %u}",l);
  462. p += zipmapEncodeLength(NULL,l);
  463. e = *p++;
  464. if (l != 0 && fwrite(p,l,1,stdout) == 0) perror("fwrite");
  465. p += l+e;
  466. if (e) {
  467. printf("[");
  468. while(e--) printf(".");
  469. printf("]");
  470. }
  471. }
  472. }
  473. printf("\n");
  474. }
  475. /* 以下是一些測试代码 */
  476. int main(void) {
  477. unsigned char *zm;
  478. zm = zipmapNew();
  479. zm = zipmapSet(zm,(unsigned char*) "name",4, (unsigned char*) "foo",3,NULL);
  480. zm = zipmapSet(zm,(unsigned char*) "surname",7, (unsigned char*) "foo",3,NULL);
  481. zm = zipmapSet(zm,(unsigned char*) "age",3, (unsigned char*) "foo",3,NULL);
  482. zipmapRepr(zm);
  483. zm = zipmapSet(zm,(unsigned char*) "hello",5, (unsigned char*) "world!",6,NULL);
  484. zm = zipmapSet(zm,(unsigned char*) "foo",3, (unsigned char*) "bar",3,NULL);
  485. zm = zipmapSet(zm,(unsigned char*) "foo",3, (unsigned char*) "!",1,NULL);
  486. zipmapRepr(zm);
  487. zm = zipmapSet(zm,(unsigned char*) "foo",3, (unsigned char*) "12345",5,NULL);
  488. zipmapRepr(zm);
  489. zm = zipmapSet(zm,(unsigned char*) "new",3, (unsigned char*) "xx",2,NULL);
  490. zm = zipmapSet(zm,(unsigned char*) "noval",5, (unsigned char*) "",0,NULL);
  491. zipmapRepr(zm);
  492. zm = zipmapDel(zm,(unsigned char*) "new",3,NULL);
  493. zipmapRepr(zm);
  494. printf("\nLook up large key:\n");
  495. {
  496. unsigned char buf[512];
  497. unsigned char *value;
  498. unsigned int vlen, i;
  499. for (i = 0; i < 512; i++) buf[i] = 'a';
  500. zm = zipmapSet(zm,buf,512,(unsigned char*) "long",4,NULL);
  501. if (zipmapGet(zm,buf,512,&value,&vlen)) {
  502. printf(" <long key> is associated to the %d bytes value: %.*s\n",
  503. vlen, vlen, value);
  504. }
  505. }
  506. printf("\nPerform a direct lookup:\n");
  507. {
  508. unsigned char *value;
  509. unsigned int vlen;
  510. if (zipmapGet(zm,(unsigned char*) "foo",3,&value,&vlen)) {
  511. printf(" foo is associated to the %d bytes value: %.*s\n",
  512. vlen, vlen, value);
  513. }
  514. }
  515. printf("\nIterate through elements:\n");
  516. {
  517. unsigned char *i = zipmapRewind(zm);
  518. unsigned char *key, *value;
  519. unsigned int klen, vlen;
  520. while((i = zipmapNext(i,&key,&klen,&value,&vlen)) != NULL) {
  521. printf(" %d:%.*s => %d:%.*s\n", klen, klen, key, vlen, vlen, value);
  522. }
  523. }
  524. return 0;
  525. }
  526. #endif

【Redis源代码剖析】 - Redis内置数据结构之压缩字典zipmap的更多相关文章

  1. Python的4个内置数据结构

    Python提供了4个内置数据结构(内置指可以直接使用,无需先导入),可以保存任何对象集合,分别是列表.元组.字典和集合. 一.列表有序的可变对象集合. 1.列表的创建例子 list1 = []lis ...

  2. python面试总结4(算法与内置数据结构)

    算法与内置数据结构 常用算法和数据结构 sorted dict/list/set/tuple 分析时间/空间复杂度 实现常见数据结构和算法 数据结构/算法 语言内置 内置库 线性结构 list(列表) ...

  3. Python第五章-内置数据结构05-集合

    Python内置数据结构 五.集合(set) python 还提供了另外一种数据类型:set. set用于包含一组无序的不重复对象.所以set中的元素有点像dict的key.这是set与 list的最 ...

  4. Python第五章-内置数据结构01-字符串

    Python 内置的数据结构 ​ 到目前为止,我们如果想保存一些数据,只能通过变量.但是如果遇到较多的数据要保存,这个时候时候用变量就变的不太现实. ​ 我们需要能够保存大量数据的类似变量的东东,这种 ...

  5. Python的内置数据结构

    Python内置数据结构一共有6类: 数字 字符串 列表 元组 字典 文件 一.数字 数字类型就没什么好说的了,大家自行理解 二.字符串 1.字符串的特性(重要): 序列化特性:字符串具有一个很重要的 ...

  6. Redis源代码剖析和凝视(八)--- 对象系统(redisObject)

    Redis 对象系统 1. 介绍 redis中基于双端链表.简单动态字符串(sds).字典.跳跃表.整数集合.压缩列表.高速列表等等数据结构实现了一个对象系统,而且实现了5种不同的对象,每种对象都使用 ...

  7. python的四种内置数据结构

    对于每种编程语言一般都会规定一些容器来保存某些数据,就像java的集合和数组一样python也同样有这样的结构 而对于python他有四个这样的内置容器来存储数据,他们都是python语言的一部分可以 ...

  8. 【Redis源代码剖析】 - Redis内置数据结构之字典dict

    原创作品,转载请标明:http://blog.csdn.net/Xiejingfa/article/details/51018337 今天我们来讲讲Redis中的哈希表. 哈希表在C++中相应的是ma ...

  9. Redis源代码剖析--对象object

    前面一系列的博客分析了Redis的基本数据结构,有动态字符串sds.双端链表sdlist.字典dict.跳跃表skiplist.整数集合intset和压缩列表ziplist等,这些数据结构对于用户来说 ...

随机推荐

  1. 洛谷 P2392 kkksc03考前临时抱佛脚

    P2392 kkksc03考前临时抱佛脚 题目背景 kkksc03的大学生活非常的颓废,平时根本不学习.但是,临近期末考试,他必须要开始抱佛脚,以求不挂科. 题目描述 这次期末考试,kkksc03需要 ...

  2. 论Node在构建超媒体API中的作用

    论Node在构建超媒体API中的作用 作者:chszs,转载需注明. 博客主页:http://blog.csdn.net/chszs 超媒体即Hypermedia,是一种採用非线性网状结构对块状多媒体 ...

  3. JXL.jar简单封装Excel读写操作

    1.分析 一个excel文件能够有多页,每页excel中能够有多行,每行中能够有多列.用面向对象的思想能够把一行中的某列看作是一个String对象,一行看作是一个包括多个列的对象.一页是包括多行的对面 ...

  4. php课程 12-42 php中类的关键字有哪些

    php课程 12-42 php中类的关键字有哪些 一.总结 一句话总结:const.final.static 1.类常量-const2.最终版本-final3.静态成员-static 1.php中类常 ...

  5. javascript类型系统之基本数据类型与包装类型

    javascript的数据类型可以分为两种:原始类型和引用类型 原始类型也称为基本类型或简单类型,因为其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈(stack)中(按值访问) ...

  6. BZOJ2118: 墨墨的等式(最短路构造/同余最短路)

    Description 墨墨突然对等式很感兴趣,他正在研究a1x1+a2y2+…+anxn=B存在非负整数解的条件,他要求你编写一个程序,给定N.{an}.以及B的取值范围,求出有多少B可以使等式存在 ...

  7. 【Codeforces Round #452 (Div. 2) A】 Splitting in Teams

    [链接] 我是链接,点我呀:) [题意] 在这里输入题意 [题解] 贪心 1优先和2组队. 如果1没有了 就结束. 如果1还有多余的. 那么就自己3个3个组队 [代码] #include <bi ...

  8. Redmine安装

    http://www.itnpc.com/news/web/146433249372595.html http://www.cnblogs.com/iluzhiyong/p/redmine.html ...

  9. Vue 学习记录<1>

    1.环境搭建:(前提node.js搭建) # 全局安装 vue-cli $ npm install --global vue-cli   # 创建一个基于 webpack 模板的新项目 $ vue i ...

  10. Activity启动模式的深入分析

    网上关于Activity启动模式的文章许多.可是看起来都千篇一律,看完之后我们都能理解这4种启动模式.只是官方api对singleTask这个启动模式解释有些争议,导致我事实上并没有真正理解这几种模式 ...