集合对象的底层实现有两种, 分别是intset和dict. 分别对应编码宏中的INTSET和HT. 显然当使用intset作为底层实现的数据结构时, 集合中存储的只能是数值数据, 且必须是整数. 而当使用dict作为集合对象的底层实现时, 是将数据全部存储于dict的键中, 值字段闲置不用.
集合对象的内存布局如下图所示:
集合对象的API接口如下:
分类 API名 功能创建接口 robj *createSetObject(void) 创建一个空集合对象.
底层编码使用HT, 即底层使用dict
-- robj *createIntsetObject(void) 创建一个空集合对象.
底层编码使用INTSET, 即底层使用intset
-- robj *setTypeCreate(sds value) 创建一个空集合对象.
注意入参虽然携带了一个数据, 但这个数据并不会存储在集合中
这个数据只起到决定编码方式的作用, 若这个数据是数值的字符串表达, 则底层编码则为INTSET, 否则为HT
释放接口 void freeSetObject(robj *o) 释放集合对象.
若集合对象底层使用的是dict, 则调用dictRelease释放这个dict
若集合对象底层使用的是intset, 则直接释放这个intset占用的连续内存
编码转换接口 void setTypeConvert(robj *setobj, int enc) 转换集合对象的内部编码
虽然接口设计的好你可以在底层编码之间互相转换, 但实际上这个接口的实现, 目前仅支持从INTSET转换为HT
读写接口 int setTypeAdd(robj *subject, sds value) 向集合对象中写入一个数据
-- int setTypeRemove(robj *setobj, sds value) 删除集合对象中的一个数据
-- int setTypeIsMember(robj *subject, sds value) 判断指定数据是否在集合对象中
-- int setTypeRandomElement(robj *setobj, sds *sdsele, int64_t *llele) 从集合对象中, 随机选出一个数据, 将其数据通过出参返回.
若数据是数值类型, 则从*llele返回, 否则, 从*sdsele返回.
注意该接口若取得二进制数据, 则*sdsele是直接引用集合内的数据, 而不是拷贝一份
-- unsigned long setTypeSize(const robj *subject) 返回集合中数据的个数
迭代器接口 setTypeIterator *setTypeInitIterator(robj *subject) 创建一个集合对象迭代器
-- void setTypeReleaseIterator(setTypeIterator *si) 释放集合对象迭代器
-- int setTypeNext( setTypeIterator *si, sds *sdsele, int64_t *llele) 让集合迭代器步进一步, 并从出参中返回步进前迭代器所指向的数据.
若数据是数值类型, 则从*llele返回, 否则, 从*sdsele返回
注意该接口若取得二进制数据, 则*sdsele是直接引用集合内的数据, 而不是拷贝一份
-- sds setTypeNextObject(setTypeIterator *si) 让集合迭代器步进一步, 并把步进前所指向的数据, 拷贝一份, 构造成一个新的SDS, 作为返回值返回
3.5 有序集合对象
有序集合的底层实现依然有两种, 一种是使用ziplist作为底层实现, 另外一种比较特殊, 底层使用了两种数据结构: dict与skiplist. 前者对应的编码值宏为ZIPLIST, 后者对应的编码值宏为SKIPLIST
使用ziplist来实现在序集合很容易理解, 只需要在ziplist这个数据结构的基础上做好排序与去重就可以了. 使用zskiplist来实现有序集合也很容易理解, Redis中实现的这个跳跃表似乎天然就是为了实现有序集合对象而实现的, 那么为什么还要辅助一个dict实例呢? 我们先看来有序集合对象在这两种编码方式下的内存布局, 然后再做解释:
首先是编码为ZIPLIST时, 有序集合的内存布局如下: