由于 set、hash 用的底层数据结构已经在之前介绍了,而这些数据结构又不如 quicklist 复杂,所以将会在此一口气全部介绍完。
Overview
确定一个命令调用了什么函数
见博客链接
set
set 包含了三种数据结构:listpack, intset, 哈希表。看得出转换就是看当前编码类型和 size_hint、max_listpack_entries、set_max_intset_entries 之间的大小关系。
值得提的有:
- 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);
}
}
-
初始创建 set 的时候会查看第一个元素是否表示为整数,如果能,根据元素数量(这里传递给了 size_hint),决定是否创建一个 intset。
-
哈希表编码的 set 对存储有优化,见我的博客的特点3: 内存存储优化
-
迭代器混合了三种数据结构的迭代器。
-
交集运算是先按照集合大小从小到大排序,边遍历最小的那个集合的元素,边对所有其他集合取交集
for each element in smallest_set:
for each set in other_set:
if element not in set:
remove element from smallest_set
...
- 并集、差集运算实现共用一个函数
并集操作必须遍历所有集合的所有元素,但是差集操作就不一定了,所以和 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
- 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);
除此之外值得提的有:
-
hash 没有集合操作。曾有人提出加入 hdiff 这一类集合操作,但是他最后发现自己没有想明白什么场景下应该这么做。可以见该 issue https://github.com/redis/redis/issues/12879
-
hash 没有 set 那么多的内存优化
-
由于 hash 支持设置过期时间,所以它需要一个获取当前时间的办法。获取时间用的是系统调用的 gettimeofday,但是有 cache,cache 怎么用的,怎么更新的,未来再看吧。