redis 源码系列(3):最不像链表的链表 --- ziplist

本文讲解 redis 中的 ziplist 数据结构。涉及到的代码文件有 src/ziplist.c src/ziplist.h

关于 ziplist 的实现,源代码中作者添加了非常详细明了的注释,这些注释会成为我们后面学习这份代码的臂助。

what

/*
 *The ziplist is a special encoded dually linked list that is designed to be very memory efficent.
 *It stores both strings and integer values, where intgers are encoded as acutal integers instead of a series of characters.
*/

ziplist 是一个用来存储字符串的、经过内存占用优化的双向链表。因为这个内存优化,我们会
看到这个 ziplist 一点都不像链表,反而像一个数组。

how

overview

我们先来看看 ziplist 的大体布局:

/*
 * ZIPLIST OVERALL LAYOUT:
 *
 * The general layout of the ziplist is as follows:
 *
 * <zlbytes><zltail><zllen><entry><entry><zlend>
 *
 * <zlbytes> is an unsigned integer to hold the number of bytes that the
 * ziplist occupies. This value needs to be stored to be able to resize the
 * entire structure without the need to traverse it first.
 *
 * <zltail> is the offset to the last entry in the list. This allows a pop
 * operation on the far side of the list without the need for full traversal.
 *
 * <zllen> is the number of entries.When this value is larger than 2**16-2,
 * we need to traverse the entire list to know how many items it holds.
 *
 * <zlend> is a single byte special value, equal to 255, which indicates the
 * end of the list.
*/

按照注释说明,代码中定义了如下宏用于获取 ziplist 中的字段:

// zlbytes uint32_t
#define ZIPLIST_BYTES(zl) (*((uint32_t *)(zl))) 
// zloffset uint32_t
#define ZIPLIST_TAIL_OFFSET(zl) (*((uint32_t *)((zl) + sizeof(uint32_t)))) 
// zllength uint16_t
#define ZIPLIST_LENGTH(zl) (*((uint16_t *)((zl) + sizeof(uint32_t) * 2))) 
// zlend 为固定的 255(1byte),这个暗示我们在 ziplist 中,除了 zlend,不会出现 255 这个 byte
#define ZIP_END 255 
// ziplist header 大小,包括 zlbytes,zloffset,zlend
#define ZIPLIST_HEADER_SIZE (sizeof(uint32_t) * 2 + sizeof(uint16_t)) 

有了这几个宏,我们就不难理解构造函数了:

unsigned char *ziplistNew(void) {

  // ZIPLIST_HEADER_SIZE 是 ziplist 表头的大小
  // 1 字节是表末端 ZIP_END 的大小
  unsigned int bytes = ZIPLIST_HEADER_SIZE + 1;

  // 为表头和表末端分配空间
  unsigned char *zl = zmalloc(bytes);

  // 初始化表属性, 注意 interv32ifbe,因为我们所有的整数都是用
  // 小端表示,这个函数会在当机器是大端表示时进行转换(对于本身
  // 是小端表示的机器则不做任何处理)
  ZIPLIST_BYTES(zl) = intrev32ifbe(bytes);
  ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(ZIPLIST_HEADER_SIZE);
  ZIPLIST_LENGTH(zl) = 0;

  // 设置表末端
  zl[bytes - 1] = ZIP_END;

  return zl;
}

从构造函数我们可以看到,其实并不存在一个 ziplist 结构体,其只是一片
连续内存而已。
下面我们再来看几个宏的定义,可以帮我们快速定位到头部和尾部的 entry

/*
 首先来看一个非空 ziplist 的示例图
area        |<---- ziplist header ---->|<----------- entries------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN |zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |        |
                                                                  |  ZIPLIST_ENTRY_END
                                                        ZIPLIST_ENTRY_TAIL
*/
// 指向第一个 entry 的指针
#define ZIPLIST_ENTRY_HEAD(zl) ((zl) + ZIPLIST_HEADER_SIZE)
// 指向最后一个 entry 的指针,还记得前面说的 zloffset 记录的是最后一个 entry 的偏移吧?
#define ZIPLIST_ENTRY_TAIL(zl) ((zl) + intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)))
// zlend 的位置(zlbytes-1)
#define ZIPLIST_ENTRY_END(zl) ((zl) + intrev32ifbe(ZIPLIST_BYTES(zl)) - 1)

entry

通过前面的布局图我们看到了,ziplist 内存储这若干个 entry,来看一下 entry 的定义

/*
 * Every entry in the ziplist is prefixed by a header that contains two pieces
 * of information. First, the length of the previous entry is stored to be
 * able to traverse the list from back to front. Second, the encoding with an
 * optional string length of the entry itself is stored.
 
 * 一个 zlentry 的内存布局如下
            +--------------------------+--------------------------+----------------+
component   | prevlen encoding data    | entrylen encoding data   |underlaying data|
            +--------------------------+--------------------------+----------------+
*/

typedef struct zlentry {
  // prevrawlen :前置节点的长度                      
  // prevrawlensize :编码 prevrawlen 所需的字节大小
  unsigned int prevrawlensize, prevrawlen;  // 从 prevlen encoding data 解析得到

  // len :当前节点值的长度
  // lensize :编码 len 所需的字节大小
  unsigned int lensize, len;       // --->|
  // 当前节点值所使用的编码类型    //     | 从 entrylen encoding data 解析得到
  unsigned char encoding;          // --->|

  // 当前节点 header 的大小
  // 等于 prevrawlensize + lensize
  unsigned int headersize;

  // 指向当前节点编码后的起始地址
  unsigned char *p; 
} zlentry;

关于 ziplist 比较复杂的地方在于,虽然在布局图上我们画出了一个个 entry,但是
在 ziplist 实际中存储的,并不是上面定义的 zlentry 结构体。而是 zlentry 经过
特定编码格式以后的一个 unsigned char。通过将lzentry
encoding,可以减少内存的占用。
先看一下 prevlen 的编码方式:

/*
 * The length of the previous entry is encoded in the following way:
 * If this length is smaller than 254 bytes, it will only consume a single
 * byte that takes the length as value. When the length is greater than or
 * equal to 254, it will consume 5 bytes. The first byte is set to 254 to
 * indicate a larger value is following. The remaining 4 bytes take the
 * length of the previous entry as value.

 即 prevlen 有两种编码格式分别占用 1 byte 和 5 bytes,用于表示小与 254 和
 大于 254 的 unsinged int

 1). prevlen < 254
 size          1 byte  
            +---------+
component   | prevlen |
            +---------+
 2). prevlen > 254 
size          1 byte    4 bytes
            +---------+--------+
component   | 254     | prevlen|
            +---------+--------+
*/

#define ZIP_BIGLEN 254
// prevlen 编码函数
static unsigned int zipPrevEncodeLength(unsigned char *p, unsigned int len) {

  // 仅返回编码 len 所需的字节数量
  if (p == NULL) {
    return (len < ZIP_BIGLEN) ? 1 : sizeof(len) + 1;

    // 写入并返回编码 len 所需的字节数量
  } else {

    // 1 字节
    if (len < ZIP_BIGLEN) {
      p[0] = len;
      return 1;

      // 5 字节
    } else {
      // 添加 5 字节长度标识
      p[0] = ZIP_BIGLEN;
      // 写入编码
      memcpy(p + 1, &len, sizeof(len));
      // 如果有必要的话,进行大小端转换
      memrev32ifbe(p + 1);
      // 返回编码长度
      return 1 + sizeof(len);
    }
  }
}

// prevlen 解码使用宏
#define ZIP_DECODE_PREVLENSIZE(ptr, prevlensize)                               \
  do {                                                                         \
    if ((ptr)[0] < ZIP_BIGLEN) {                                               \
      (prevlensize) = 1;                                                       \
    } else {                                                                   \
      (prevlensize) = 5;                                                       \
    }                                                                          \
  } while (0);

#define ZIP_DECODE_PREVLEN(ptr, prevlensize, prevlen)                          \
  do {                                                                         \
                                                                               \
    /* 先计算被编码长度值的字节数 */                              \
    ZIP_DECODE_PREVLENSIZE(ptr, prevlensize);                                  \
                                                                               \
    /* 再根据编码字节数来取出长度值 */                           \
    if ((prevlensize) == 1) {                                                  \
      (prevlen) = (ptr)[0];                                                    \
    } else if ((prevlensize) == 5) {                                           \
      assert(sizeof((prevlensize)) == 4);                                      \
      memcpy(&(prevlen), ((char *)(ptr)) + 1, 4);                              \
      memrev32ifbe(&prevlen);                                                  \
    }                                                                          \
  } while (0);

上面的两个宏的逻辑配合注释很容易理解。主要是编码不同大小的整数时候使用
不同的编码方式。在处理小于 254 的整数时,我们只使用了 1byte,节省了3bytees,
虽然在对于大于 254 的整数浪费了一个字节,但是因为其实在大多数程序中,对于小
整数的使用频率是远远高于大整数的。所以使用这种编码在一般可以节省很多空间
(相同思想的还有 google 的 varint)。
节点本身编码:

/*
 * The other header field of the entry itself depends on the contents of the
 * entry. When the entry is a string, the first 2 bits of this header will hold
 * the type of encoding used to store the length of the string, followed by the
 * actual length of the string. When the entry is an integer the first 2 bits
 * are both set to 1. The following 2 bits are used to specify what kind of
 * integer will be stored after this header. An overview of the different
 * types and encodings is as follows(The first byte is always enough
 * to determine the kind of entry):

 * |00pppppp| - 1 byte
 *      String value with length less than or equal to 63 bytes (6 bits).
 *      "pppppp" represents the unsigned 6 bit length.
 * |01pppppp|qqqqqqqq| - 2 bytes
 *      String value with length less than or equal to 16383 bytes (14 bits).
 *      IMPORTANT: The 14 bit number is stored in big endian.
 * |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 bytes
 *      String value with length greater than or equal to 16384 bytes.
 *      Only the 4 bytes following the first byte represents the length
 *      up to 32^2-1. The 6 lower bits of the first byte are not used and
 *      are set to zero.
 *      IMPORTANT: The 32 bit number is stored in big endian.
 * |11000000| - 3 bytes
 *      Integer encoded as int16_t (2 bytes).
 * |11010000| - 5 bytes
 *      Integer encoded as int32_t (4 bytes).
 * |11100000| - 9 bytes
 *      Integer encoded as int64_t (8 bytes).
 * |11110000| - 4 bytes
 *      Integer encoded as 24 bit signed (3 bytes).
 * |11111110| - 2 bytes
 *      Integer encoded as 8 bit signed (1 byte).
 * |1111xxxx| - (with xxxx between 0000 and 1101) immediate 4 bit integer.
 *      Unsigned integer from 0 to 12. The encoded value is actually from
 *      1 to 13 because 0000 and 1111 can not be used, so 1 should be
 *      subtracted from the encoded 4 bit value to obtain the right value.
 * |11111111| - End of ziplist special entry.
*/
#define ZIP_STR_MASK 0xc0 // 上面注释指出 string 只有三种编码方式分别以 00、01、10 开头
#define ZIP_INT_MASK 0x30 

#define ZIP_STR_06B (0 << 6)
#define ZIP_STR_14B (1 << 6)
#define ZIP_STR_32B (2 << 6)

/*
 * 整数编码类型
 */
#define ZIP_INT_16B (0xc0 | 0 << 4)
#define ZIP_INT_32B (0xc0 | 1 << 4)
#define ZIP_INT_64B (0xc0 | 2 << 4)
#define ZIP_INT_24B (0xc0 | 3 << 4)
#define ZIP_INT_8B 0xfe

#define ZIP_IS_STR(enc) (((enc)&ZIP_STR_MASK) < ZIP_STR_MASK)

// entry encoding
static unsigned int zipEncodeLength(unsigned char *p, unsigned char encoding,
                                    unsigned int rawlen) {
  unsigned char len = 1, buf[5];

  // 编码字符串
  if (ZIP_IS_STR(encoding)) {
    /* Although encoding is given it may not be set for strings,
     * so we determine it here using the raw length. */
    if (rawlen <= 0x3f) {
      if (!p)
        return len;
      buf[0] = ZIP_STR_06B | rawlen;
    } else if (rawlen <= 0x3fff) {
      len += 1;
      if (!p)
        return len;
      buf[0] = ZIP_STR_14B | ((rawlen >> 8) & 0x3f);
      buf[1] = rawlen & 0xff;
    } else {
      len += 4;
      if (!p)
        return len;
      buf[0] = ZIP_STR_32B;
      buf[1] = (rawlen >> 24) & 0xff;
      buf[2] = (rawlen >> 16) & 0xff;
      buf[3] = (rawlen >> 8) & 0xff;
      buf[4] = rawlen & 0xff;
    }

    // 编码整数
  } else {
    /* Implies integer encoding, so length is always 1. */
    if (!p)
      return len;
    buf[0] = encoding;
  }

  /* Store this length at p */
  // 将编码后的长度写入 p
  memcpy(p, buf, len);

  // 返回编码所需的字节数
  return len;
}

// entry decoding
#define ZIP_ENTRY_ENCODING(ptr, encoding)                                      \
  do {                                                                         \
    (encoding) = (ptr[0]);                                                     \
    if ((encoding) < ZIP_STR_MASK)                                             \
      (encoding) &= ZIP_STR_MASK;                                              \
  } while (0)

#define ZIP_DECODE_LENGTH(ptr, encoding, lensize, len)                         \
  do {                                                                         \
                                                                               \
    /* 取出值的编码类型 */                                             \
    ZIP_ENTRY_ENCODING((ptr), (encoding));                                     \
                                                                               \
    if ((encoding) < ZIP_STR_MASK) {                                           \
      /* 字符串编码 */                                                      \
      if ((encoding) == ZIP_STR_06B) {                                         \
        (lensize) = 1;                                                         \
        (len) = (ptr)[0] & 0x3f;                                               \
      } else if ((encoding) == ZIP_STR_14B) {                                  \
        (lensize) = 2;                                                         \
        (len) = (((ptr)[0] & 0x3f) << 8) | (ptr)[1];                           \
      } else if (encoding == ZIP_STR_32B) {                                    \
        (lensize) = 5;                                                         \
        (len) = ((ptr)[1] << 24) | ((ptr)[2] << 16) | ((ptr)[3] << 8) |        \
                ((ptr)[4]);                                                    \
      } else {                                                                 \
        assert(NULL);                                                          \
      }                                                                        \
    } else {                                                                   \
      /* 整数编码 */                                                       \
      (lensize) = 1;                                                           \
      (len) = zipIntSize(encoding);                                            \
    }                                                                          \
  } while (0);

static unsigned int zipIntSize(unsigned char encoding) {

  switch (encoding) {
  case ZIP_INT_8B:
    return 1;
  case ZIP_INT_16B:
    return 2;
  case ZIP_INT_24B:
    return 3;
  case ZIP_INT_32B:
    return 4;
  case ZIP_INT_64B:
    return 8;
  default:
    return 0; /* 4 bit immediate */
  }
  assert(NULL);
  return 0;
}

解码代码结合注释看还是很直观的,我们可以看到 entry 自身信息的编码主要有:
其内容类型(因为 ziplist 里面同时可以存储字符串和整数)和其长度的编码。不难
发现,entry 自身信息的编码因为要加入 encoding 字段,所以比 prevlen 的编码
更为复杂。
有了上面几个 decode 相关辅助函数,我们就可以将一个 zlentry 解码出来了:

zlentry zipEntry(unsigned char *p) {
    zlentry e;
    // 解码 prevlen 信息
    ZIP_DECODE_PREVLEN(p, e->prevrawlensize, e->prevrawlen);
    // 解码 entry encoding len 信息
    ZIP_DECODE_LENGTH(p + e->prevrawlensize, e->encoding, e->lensize, e->len);
    e->headersize = e->prevrawlensize + e->lensize;
    e->p = p;
    return e;
}

注意这个函数将 zlentry 整个结构体按值返回,具有一定的性能问题,在新版本的 redis
代码中已经修改为按照应用传值(传入指针)。

insert

现在我们来看一下 ziplist 如何进行数据的插入:

// 尝试将字符串编码成整数, 注意这里的 entry 不是 zlentry,值得是要编码的数据
// 比如 "123" 需要占用 3 字节,但是其实按照前面提到的编码方式,我们只用一个字节
// 就够了
static int zipTryEncoding(unsigned char *entry, unsigned int entrylen,
                          long long *v, unsigned char *encoding) {
  long long value;

  // 忽略太长或太短的字符串
  if (entrylen >= 32 || entrylen == 0)
    return 0;

  // 尝试转换
  if (string2ll((char *)entry, entrylen, &value)) {
    // 转换成功,以从小到大的顺序检查适合值 value 的编码方式
    // 编码逻辑查看上节介绍
    if (value >= 0 && value <= 12) {
      *encoding = ZIP_INT_IMM_MIN + value;
    } else if (value >= INT8_MIN && value <= INT8_MAX) {
      *encoding = ZIP_INT_8B;
    } else if (value >= INT16_MIN && value <= INT16_MAX) {
      *encoding = ZIP_INT_16B;
    } else if (value >= INT24_MIN && value <= INT24_MAX) {
      *encoding = ZIP_INT_24B;
    } else if (value >= INT32_MIN && value <= INT32_MAX) {
      *encoding = ZIP_INT_32B;
    } else {
      *encoding = ZIP_INT_64B;
    }

    // 记录值到指针
    *v = value;

    // 返回转换成功标识
    return 1;
  }

  return 0;
}

// 计算新旧 prevlen encoding 长度 diff
static int zipPrevLenByteDiff(unsigned char *p, unsigned int len) {
  unsigned int prevlensize;

  // 取出编码原来的前置节点长度所需的字节数
  // T = O(1)
  ZIP_DECODE_PREVLENSIZE(p, prevlensize);

  // 计算编码 len 所需的字节数,然后进行减法运算
  // T = O(1)
  return zipPrevEncodeLength(NULL, len) - prevlensize;
}

// 解析节点的长度
static unsigned int zipRawEntryLength(unsigned char *p) {
  unsigned int prevlensize, encoding, lensize, len;

  // 取出编码前置节点的长度所需的字节数
  // T = O(1)
  ZIP_DECODE_PREVLENSIZE(p, prevlensize);

  // 取出当前节点值的编码类型,编码节点值长度所需的字节数,以及节点值的长度
  // T = O(1)
  ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);

  // 计算节点占用的字节数总和
  return prevlensize + lensize + len;
}

static unsigned char *ziplistResize(unsigned char *zl, unsigned int len) {

  // 用 zrealloc ,扩展时不改变现有元素
  zl = zrealloc(zl, len);

  // 更新 bytes 属性
  ZIPLIST_BYTES(zl) = intrev32ifbe(len);

  // 重新设置表末端
  zl[len - 1] = ZIP_END;

  return zl;
}

static void zipPrevEncodeLengthForceLarge(unsigned char *p, unsigned int len) {

  if (p == NULL)
    return;

  // 设置 5 字节长度标识
  p[0] = ZIP_BIGLEN;

  // 写入 len
  memcpy(p + 1, &len, sizeof(len));
  memrev32ifbe(p + 1);
}

static unsigned char *__ziplistCascadeUpdate(unsigned char *zl,
                                             unsigned char *p) {
  size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl)), rawlen, rawlensize;
  size_t offset, noffset, extra;
  unsigned char *np;
  zlentry cur, next;

  while (p[0] != ZIP_END) {
    // 将 p 所指向的节点的信息保存到 cur 结构中
    cur = zipEntry(p);
    // 当前节点的长度
    rawlen = cur.headersize + cur.len;
    // 计算编码当前节点的长度所需的字节数
    rawlensize = zipPrevEncodeLength(NULL, rawlen);

    /* Abort if there is no next entry. */
    if (p[rawlen] == ZIP_END)
      break;

    // 取出后续节点的信息,保存到 next 结构中
    next = zipEntry(p + rawlen);

    /* Abort when "prevlen" has not changed. */
    if (next.prevrawlen == rawlen)
      break;

    if (next.prevrawlensize < rawlensize) {

      // 执行到这里,表示 next 空间的大小不足以编码 cur 的长度
      // 所以程序需要对 next 节点的(header 部分)空间进行扩展

      // 记录 p 的偏移量
      offset = p - zl;
      // 计算需要增加的节点数量
      extra = rawlensize - next.prevrawlensize;
      // 扩展 zl 的大小
      zl = ziplistResize(zl, curlen + extra);
      // 还原指针 p
      p = zl + offset;

      // 记录下一节点的偏移量
      np = p + rawlen;
      noffset = np - zl;

      // 当 next 节点不是表尾节点时,更新列表到表尾节点的偏移量
      //
      // 不用更新的情况(next 为表尾节点):
      //
      // |     | next |      ==>    |     | new next          |
      //       ^                          ^
      //       |                          |
      //     tail                        tail
      //
      // 需要更新的情况(next 不是表尾节点):
      //
      // | next |     |   ==>     | new next          |     |
      //        ^                        ^
      //        |                        |
      //    old tail                 old tail
      //
      // 更新之后:
      //
      // | new next          |     |
      //                     ^
      //                     |
      //                  new tail
      if ((zl + intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + extra);
      }

      /* Move the tail to the back. */
      // 向后移动 cur 节点之后的数据,为 cur 的新 header 腾出空间
      //
      // 示例:
      //
      // | header | value |  ==>  | header |    | value |  ==>  | header      |
      // value |
      //                                   |<-->|
      //                            为新 header 腾出的空间
      // T = O(N)
      memmove(np + rawlensize, np + next.prevrawlensize,
              curlen - noffset - next.prevrawlensize - 1);
      // 将新的前一节点长度值编码进新的 next 节点的 header
      zipPrevEncodeLength(np, rawlen);

      /* Advance the cursor */
      // 移动指针,继续处理下个节点
      p += rawlen;
      curlen += extra;
    } else {
      if (next.prevrawlensize > rawlensize) {
        // 执行到这里,说明 next 节点编码前置节点的 header 空间有 5 字节
        // 而编码 rawlen 只需要 1 字节
        // 但是程序不会对 next 进行缩小,
        // 所以这里只将 rawlen 写入 5 字节的 header 中就算了。
        zipPrevEncodeLengthForceLarge(p + rawlen, rawlen);
      } else {
        // 说明 cur 节点的长度正好可以编码到 next 节点的 header 中
        zipPrevEncodeLength(p + rawlen, rawlen);
      }

      /* Stop here, as the raw length of "next" has not changed. */
      break;
    }
  }

  return zl;
}

#define ZIPLIST_INCR_LENGTH(zl, incr)                                          \
  {                                                                            \
    if (ZIPLIST_LENGTH(zl) < UINT16_MAX)                                       \
      ZIPLIST_LENGTH(zl) =                                                     \
          intrev16ifbe(intrev16ifbe(ZIPLIST_LENGTH(zl)) + incr);               \
  }

static void zipSaveInteger(unsigned char *p, int64_t value,
                           unsigned char encoding) {
  int16_t i16;
  int32_t i32;
  int64_t i64;

  if (encoding == ZIP_INT_8B) {
    ((int8_t *)p)[0] = (int8_t)value;
  } else if (encoding == ZIP_INT_16B) {
    i16 = value;
    memcpy(p, &i16, sizeof(i16));
    memrev16ifbe(p);
  } else if (encoding == ZIP_INT_24B) {
    i32 = value << 8;
    memrev32ifbe(&i32);
    memcpy(p, ((uint8_t *)&i32) + 1, sizeof(i32) - sizeof(uint8_t));
  } else if (encoding == ZIP_INT_32B) {
    i32 = value;
    memcpy(p, &i32, sizeof(i32));
    memrev32ifbe(p);
  } else if (encoding == ZIP_INT_64B) {
    i64 = value;
    memcpy(p, &i64, sizeof(i64));
    memrev64ifbe(p);
  } else if (encoding >= ZIP_INT_IMM_MIN && encoding <= ZIP_INT_IMM_MAX) {
    /* Nothing to do, the value is stored in the encoding itself. */
  } else {
    assert(NULL);
  }
}
// 在位置 p 处插入数据 s
static unsigned char *__ziplistInsert(unsigned char *zl, unsigned char *p,
                                      unsigned char *s, unsigned int slen) {
  // 记录当前 ziplist 的长度
  size_t curlen = intrev32ifbe(ZIPLIST_BYTES(zl));
  // 存储 s 需要占用的内存数
  size_t reqlen;
  size_t prevlen;
  size_t offset;
  int nextdiff = 0;
  unsigned char encoding = 0;
  long long value = 123456789; /* initialized to avoid warning. Using a value
                                  that is easy to see if for some reason
                                  we use it uninitialized. */
  zlentry entry, tail;

  /* Find out prevlen for the entry that is inserted. */
  if (p[0] != ZIP_END) {
    // 如果 p[0] 不指向列表末端,说明列表非空,并且 p 正指向列表的其中一个节点
    // decode 该节点, 这个节点在插入结束后会成为新插入节点的后继节点
    entry = zipEntry(p);
    prevlen = entry.prevrawlen;
  } else {
    // 如果 p 指向表尾末端,那么程序需要检查列表是否为:
    // 1)如果 ptail 也指向 ZIP_END ,那么列表为空; prevlen = 0
    // 2)如果列表不为空,那么 ptail 将指向列表的最后一个节点,
    //   需要获取尾节点的长度作为 prevlen 
    unsigned char *ptail = ZIPLIST_ENTRY_TAIL(zl);
    if (ptail[0] != ZIP_END) {
      prevlen = zipRawEntryLength(ptail);
    }
  }

  // ziplist 对外提供的接口,在插入的时候传入的都是一个字符串
  // 但是如果这个字符串编码成一个整数之后可以节省空间,ziplist
  // 会选择将其编码成一个整数插入到 ziplist,这个对调用者是透明的
  // 调用者并不知道他调用接口时候插入的 ”123“ 在 ziplist 中是如何存储的
  if (zipTryEncoding(s, slen, &value, &encoding)) {
    /* 'encoding' is set to the appropriate integer encoding */
    reqlen = zipIntSize(encoding);
  } else {
    // 仍然使用字符串编码
    reqlen = slen;
  }

  /*
            +--------------------------+--------------------------+----------------+
component   | prevlen encoding data    | entrylen encoding data   |underlaying data|
            +--------------------------+--------------------------+----------------+
  计算占用内存,加上 header 占用的长度
  */
  reqlen += zipPrevEncodeLength(NULL, prevlen); // prevlen encoding
  reqlen += zipEncodeLength(NULL, encoding, slen); // entry encoding

  // 如果插入的位置不是在链表尾部,那么我们新插入的节点就会有后继节点
  // 后继节点保存的 prevlen 信息就需要更新,而且因为长度变化,可能存在
  // 之前 prevlen < 254 所以只占用了 1 byte,我们新插入的 entry 占用
  // 超过 254 字节,占用 5 byte(反过来5 byte -> 1 byte 也一样)
  // 在这里我们计算一下新旧 prevlen 的 diff
  nextdiff = (p[0] != ZIP_END) ? zipPrevLenByteDiff(p, reqlen) : 0;

  // 因为 resize 后 zl 地址可能改变,到时候 p-zl 的结果会变为非法,所以
  // 先计算 offset,用于 resize 之后重新索引 p
  offset = p - zl;
  zl = ziplistResize(zl, curlen + reqlen + nextdiff);
  p = zl + offset;

  /* Apply memory move when necessary and update tail offset. */
  if (p[0] != ZIP_END) {
    // 最开始已经提过了,ziplist 其实就像一个数组,要在数组中间插入数据,需要
    // 搬运数据
    /*
area        |<--------------offset------------->|<------curl-offset-1+nextdiff----->|
            +---------+--------+-------+--------+--------+--------+--------+--------+------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | new empty buffer|zlend |
            +---------+--------+-------+--------+--------+--------+--------+--------+------+
                                                ^-newentry-^                
address                                         |          |        
                                                p          p+reqlen
    我们需要将 p 之后的数据搬运到 p+reqlen 处, 需要注意的是我们要给从 p-nextdiff 处开始搬运
    要不然可能导致内存越界(大家可以按照 nextdiff == 4 计算一下)
    */
    memmove(p + reqlen, p - nextdiff, curlen - offset - 1 + nextdiff);

    // 更新后继节点的 prevlen
    zipPrevEncodeLength(p + reqlen, reqlen);

    // 因为是在中间插入,所以尾节点 offset 需要加上 reqlen
    ZIPLIST_TAIL_OFFSET(zl) =
        intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + reqlen);

    // 这里有个 corner case,那就是如果我们的后继节点不是是尾节点
    // 那么因为后继节点的长度也可能发生了改变,我们需要将 nextdiff
    // 也加到尾节点 offset 中
    tail = zipEntry(p + reqlen);
    if (p[reqlen + tail.headersize + tail.len] != ZIP_END) {
      ZIPLIST_TAIL_OFFSET(zl) =
          intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + nextdiff);
    }
  } else {
    // 插入的是尾节点,更新 tail offset
    ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe(p - zl);
  }

  // 当 nextdiff != 0 时,新节点的后继节点的(header 部分)长度已经修改,
  // 所以需要级联地更新后续的节点存储的 prevlen
  if (nextdiff != 0) {
    offset = p - zl;
    zl = __ziplistCascadeUpdate(zl, p + reqlen);
    p = zl + offset;
  }

  // 现在只需要将将新 zlentry 编码即可
  p += zipPrevEncodeLength(p, prevlen);
  p += zipEncodeLength(p, encoding, slen);
  if (ZIP_IS_STR(encoding)) {
    // 写入原始字符串
    memcpy(p, s, slen);
  } else {
    // 作为整数编码
    zipSaveInteger(p, value, encoding);
  }

  // 更新列表的节点数量计数器
  ZIPLIST_INCR_LENGTH(zl, 1);
  return zl;
}

上面的插入例程逻辑较为复杂,主要是因为其有大量编、解码逻辑,而且后继节点存储
了前驱节点的信息(prevlen)所以导致可能牵一发而动全身的情况,所以有级联更新的
逻辑,而且在极端的情况下,级联更新的时间复杂度可以达到 O(n^2) !
ziplist 支持在头、尾插入元素,其实现都是委托 __ziplistInsert 实现的:

unsigned char *ziplistPush(unsigned char *zl, unsigned char *s,
                           unsigned int slen, int where) {

  // 根据 where 参数的值,决定将值推入到表头还是表尾
  unsigned char *p;
  p = (where == ZIPLIST_HEAD) ? ZIPLIST_ENTRY_HEAD(zl) : ZIPLIST_ENTRY_END(zl);

  // 返回添加新值后的 ziplist
  return __ziplistInsert(zl, p, s, slen);
}

delete

下面再来看一下 delete 的实现, 了解了 insert 的逻辑,想必其逆操作难不倒我们:

// 从 p 开始,连续删除 num 个节点
static unsigned char *__ziplistDelete(unsigned char *zl, unsigned char *p,
                                      unsigned int num) {
  unsigned int i, totlen, deleted = 0;
  size_t offset;
  int nextdiff = 0;
  zlentry first, tail;

  // 计算被删除节点总共占用的内存字节数
  // 以及被删除节点的总个数
  first = zipEntry(p);
  for (i = 0; p[0] != ZIP_END && i < num; i++) {
    p += zipRawEntryLength(p);
    deleted++;
  }

  // totlen 是所有被删除节点总共占用的内存字节数
  totlen = p - first.p;
  if (totlen > 0) {
    if (p[0] != ZIP_END) {
      // 执行这里,表示被删除节点之后仍然有节点存在
      // 与插入逻辑一样,因为 p 的前驱节点发生了改变,所以我们要
      // 看看是不是会导致其占用内存发生变化
      nextdiff = zipPrevLenByteDiff(p, first.prevrawlen);
      // 如果有需要的话,将指针 p 后退 nextdiff 字节,为新 header 空出空间
      p -= nextdiff;
      // 删除后 p 的前驱节点变为 first 的前驱,所以将 p 的 prevlen 设为 firstl.prevrawlen
      zipPrevEncodeLength(p, first.prevrawlen);

      // 更新 ziplist tail offset 的逻辑与插入时一致,不再赘述
      ZIPLIST_TAIL_OFFSET(zl) =
          intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) - totlen);

      tail = zipEntry(p);
      if (p[tail.headersize + tail.len] != ZIP_END) {
        ZIPLIST_TAIL_OFFSET(zl) =
            intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl)) + nextdiff);
      }

      // 从表尾向表头移动数据,覆盖被删除节点的数据
      memmove(first.p, p, intrev32ifbe(ZIPLIST_BYTES(zl)) - (p - zl) - 1);
    } else {

      // 执行这里,表示被删除节点之后已经没有其他节点了

      ZIPLIST_TAIL_OFFSET(zl) = intrev32ifbe((first.p - zl) - first.prevrawlen);
    }

    // 缩小并更新 ziplist 的长度
    offset = first.p - zl;
    zl = ziplistResize(zl, intrev32ifbe(ZIPLIST_BYTES(zl)) - totlen + nextdiff);
    ZIPLIST_INCR_LENGTH(zl, -deleted);
    p = zl + offset;

    // 如果 p 所指向的节点的大小已经变更,那么进行级联更新
    // 检查 p 之后的所有节点是否符合 ziplist 的编码要求
    if (nextdiff != 0)
      zl = __ziplistCascadeUpdate(zl, p);
  }

  return zl;
}

读接口(们)

现在我们来看一下读相关接口。既然 ziplist 是一个双向链表,那么我们先看一下如何通过 p
查找其前驱和后继节点

// 给定 p 返回其后继节点
unsigned char *ziplistNext(unsigned char *zl, unsigned char *p) {
  ((void)zl);
  // p 已经指向列表末端
  if (p[0] == ZIP_END) {
    return NULL;
  }
  // 指向后一节点
  p += zipRawEntryLength(p);
  if (p[0] == ZIP_END) {
    // p 已经是表尾节点,没有后置节点
    return NULL;
  }
  return p;
}

// 给定 p 返回其前驱节点
unsigned char *ziplistPrev(unsigned char *zl, unsigned char *p) {
  zlentry entry;

  if (p[0] == ZIP_END) {
    // 如果 p 指向列表末端(列表为空,或者刚开始从表尾向表头迭代)
    // 那么尝试取出列表尾端节点
    p = ZIPLIST_ENTRY_TAIL(zl);
    // 尾端节点也指向列表末端,那么列表为空
    return (p[0] == ZIP_END) ? NULL : p;

  } else if (p == ZIPLIST_ENTRY_HEAD(zl)) {
    // 如果 p 指向列表头 没有前驱节点
    return NULL;
  } else {
    // 既不是表头也不是表尾,从表尾向表头移动指针
    // 计算前一个节点的节点数
    entry = zipEntry(p);
    assert(entry.prevrawlen > 0);
    // 移动指针,指向前一个节点
    return p - entry.prevrawlen;
  }
}

查询一个节点的值,注意,虽然我们的 push 方法并不提供一个 push 整数的接口
但是在 get 的时候,却会根据在 ziplist 中的 encoding 模式,返回字符串或者
整数:

unsigned int ziplistGet(unsigned char *p, unsigned char **sstr,
                        unsigned int *slen, long long *sval) {

  zlentry entry;
  if (p == NULL || p[0] == ZIP_END)
    return 0;
  if (sstr)
    *sstr = NULL;

  // 取出 p 所指向的节点的各项信息,并保存到结构 entry 中
  entry = zipEntry(p);

  if (ZIP_IS_STR(entry.encoding)) {
    // 节点的值为字符串,将字符串长度保存到 *slen ,字符串保存到 *sstr
    if (sstr) {
      *slen = entry.len;
      *sstr = p + entry.headersize;
    }

  } else {
    // 节点的值为整数,解码值,并将值保存到 *sval
    if (sval) {
      *sval = zipLoadInteger(p + entry.headersize, entry.encoding);
    }
  }

  return 1;
}

ziplist 还提供了 compare 接口将 entry 与字符串进行对比:

unsigned int ziplistCompare(unsigned char *p, unsigned char *sstr,
                            unsigned int slen) {
  zlentry entry;
  unsigned char sencoding;
  long long zval, sval;
  if (p[0] == ZIP_END)
    return 0;

  // 取出节点
  entry = zipEntry(p);
  if (ZIP_IS_STR(entry.encoding)) {

    // 节点值为字符串,进行字符串对比, 小窍门先检查长度是否相等
    // 对于不想等的可以避免整个字符串的比较
    if (entry.len == slen) {
      return memcmp(p + entry.headersize, sstr, slen) == 0;
    } else {
      return 0;
    }
  } else {
    // 节点值为整数,尝试将 sstr 也编码成整数
    if (zipTryEncoding(sstr, slen, &sval, &sencoding)) {
      zval = zipLoadInteger(p + entry.headersize, entry.encoding);
      return zval == sval;
    }
  }

  return 0;
}

最后是一个略微有点奇怪的 Find 函数,它可以设置一个 skip 参数来让每次比较
中间隔 skip 个节点:

unsigned char *ziplistFind(unsigned char *p, unsigned char *vstr,
                           unsigned int vlen, unsigned int skip) {
  int skipcnt = 0;
  unsigned char vencoding = 0;
  long long vll = 0;

  // 只要未到达列表末端,就一直迭代
  while (p[0] != ZIP_END) {
    unsigned int prevlensize, encoding, lensize, len;
    unsigned char *q;

    ZIP_DECODE_PREVLENSIZE(p, prevlensize);
    ZIP_DECODE_LENGTH(p + prevlensize, encoding, lensize, len);
    q = p + prevlensize + lensize;

    if (skipcnt == 0) {

      /* Compare current entry with specified entry */
      if (ZIP_IS_STR(encoding)) {
        if (len == vlen && memcmp(q, vstr, vlen) == 0) {
          return p;
        }
      } else {
        // 对 tryEncoding 的结果进行缓存
        // 1.encoding 失败,vencoding=UCHAR_MAC
        // 2.encoding 成果,encoding 结果已经保存在 vll,有 vencoding!=UCHAR_MAX
        if (vencoding == 0) {
          if (!zipTryEncoding(vstr, vlen, &vll, &vencoding)) {
            vencoding = UCHAR_MAX;
          }
          assert(vencoding);
        }
        if (vencoding != UCHAR_MAX) {
          long long ll = zipLoadInteger(q, encoding);
          if (ll == vll) {
            return p;
          }
        }
      }

      skipcnt = skip;
    } else {
      skipcnt--;
    }

    // 前进到下一个节点
    p = q + len;
  }

  // 没有找到指定的节点
  return NULL;
}

至此 ziplist 的相关代码我们就学习完成了。ziplist 在存储一些形如:“123412” 的字符串表示的整数时候,可以节省内存。有其特定的试用场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值