由于 set、hash 用的底层数据结构已经在之前介绍了,而这些数据结构又不如 quicklist 复杂,所以将会在此一口气全部介绍完。

Overview

image

确定一个命令调用了什么函数

博客链接

set

set 包含了三种数据结构:listpack, intset, 哈希表。看得出转换就是看当前编码类型和 size_hint、max_listpack_entries、set_max_intset_entries 之间的大小关系。

值得提的有:

  1. set 只能够从 listpack 或 intset 转换到哈希表,没有哈希表转换回去的代码,也没有 listpack 和 intset 之间的转换。
void setTypeMaybeConvert(robj *set, size_t size_hint) {
    if ((set->encoding == OBJ_ENCODING_LISTPACK && size_hint > server.set_max_listpack_entries)
        || (set->encoding == OBJ_ENCODING_INTSET && size_hint > server.set_max_intset_entries))
    {
        setTypeConvertAndExpand(set, OBJ_ENCODING_HT, size_hint, 1);
    }
}
  1. 初始创建 set 的时候会查看第一个元素是否表示为整数,如果能,根据元素数量(这里传递给了 size_hint),决定是否创建一个 intset。

  2. 哈希表编码的 set 对存储有优化,见我的博客的特点3: 内存存储优化

  3. 迭代器混合了三种数据结构的迭代器。

  4. 交集运算是先按照集合大小从小到大排序,边遍历最小的那个集合的元素,边对所有其他集合取交集

for each element in smallest_set:
  for each set in other_set:
    if element not in set:
      remove element from smallest_set
...
  1. 并集、差集运算实现共用一个函数

并集操作必须遍历所有集合的所有元素,但是差集操作就不一定了,所以和 zdiff 一样,它有两种时间复杂度不同的算法。

差集操作 A - B 就是找到 A 里面有的而 B 里面没有的元素,也可以说从 A 中移除所有在 B 中出现的元素。

算法1:

这是一个 O(最小集合中的元素数量*集合数量) 的算法

for each element in smallest_set:
  for each set in other_set:
    if element in set:
      remove element from smallest_set
...

算法2:

这是一个 O(所有集合中的元素数量) 的算法

for each element in other_set:
  if element in smallest_set:
    remove element from smallest_set
  1. intset 的实现,就是排序的数组。查找时用二分,插入时要复制插入位置以后的内容。
static uint8_t intsetSearch(intset *is, int64_t value, uint32_t *pos) {
    int min = 0, max = intrev32ifbe(is->length)-1, mid = -1;
    int64_t cur = -1;
...
    while(max >= min) {
        mid = ((unsigned int)min + (unsigned int)max) >> 1;
        cur = _intsetGet(is,mid);
        if (value > cur) {
            min = mid+1;
        } else if (value < cur) {
            max = mid-1;
        } else {
            break;
        }
    }
...
}

扩容时也不是 c++ vector 那样的成倍扩容,而是简单的 size + 1

intset *intsetAdd(intset *is, int64_t value, uint8_t *success) {
...
        is = intsetResize(is,intrev32ifbe(is->length)+1);
        if (pos < intrev32ifbe(is->length)) intsetMoveTail(is,pos,pos+1);
...
}

hash

hash 就是所谓的 hashmap,包括了键值对的那种。它利用了 listpack 或 listpack extended 或哈希表实现。

listpack 转换为哈希表的条件是:插入的字符串长度大于 server.hash_max_listpack_value,或新插入的元素数量大于 server.hash_max_listpack_entries,或 listpack 已经无法新增元素了。

这么说,哪怕 listpack 占用空间超过了 1G 都可能还是在用 listpack?其实这不会发生(除非设置的hash_max_listpack_entries 和 hash_max_listpack_value 比较离谱)。在 hashTypeAdd 中有元素插入后的检查,此时可能会转换为哈希表。

    size_t new_fields = (end - start + 1) / 2;
    if (new_fields > server.hash_max_listpack_entries) {
        hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
        dictExpand(o->ptr, new_fields);
        return;
    }

    for (i = start; i <= end; i++) {
        if (!sdsEncodedObject(argv[i]))
            continue;
        size_t len = sdslen(argv[i]->ptr);
        if (len > server.hash_max_listpack_value) {
            hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);
            return;
        }
        sum += len;
    }
    if (!lpSafeToAdd(hashTypeListpackGetLp(o), sum))
        hashTypeConvert(o, OBJ_ENCODING_HT, &db->hexpires);

除此之外值得提的有:

  1. hash 没有集合操作。曾有人提出加入 hdiff 这一类集合操作,但是他最后发现自己没有想明白什么场景下应该这么做。可以见该 issue https://github.com/redis/redis/issues/12879

  2. hash 没有 set 那么多的内存优化

  3. 由于 hash 支持设置过期时间,所以它需要一个获取当前时间的办法。获取时间用的是系统调用的 gettimeofday,但是有 cache,cache 怎么用的,怎么更新的,未来再看吧。