Set 结构存储值与结构读写能力:

  包含字符串的无序收集器(unordered collection), 且数据不重复.

  添加,获取,移除单个元素; 检查一个元素是否存在于集合中; 计算交集,并集,差集; 从集合里面随机获取元素. 存储不可以重复的数据

 

  ZSet(有序集合) 结构存储值与结构读写能力:

  字符串成员(member)与浮点数分值(score)之间的有序映射, 元素的排列顺序由分值的大小决定.

  添加,获取,删除单个元素; 根据分值范围(range)或者成员来获取元素. 存储有序的集合数据(set), Redis的有序集合就和散列一样, 都用于存储键值对: 有序集合的键被称为成员(member), 每个成员都是独一无二的; 而有序集合的值则被称为分值(score), 分值必须为浮点数. 有序集合是Redis里面唯一一个既可以根据成员(这一点和散列一样)访问元素, 又可以根据分值及分值的排列顺序来访问元素的结构.

c redis 有序集合 redis有序set原理_数据

 

 

  与 Java 中的 HashSet 一样,无序且存储元素不重复。其底层有两种方式实现,当 value是整数值时,且数据量是不大时使用 inset 来存储,其他情况都是用字典 dict(即hash) 来存储。

 

  inset

  Redis 中 inset 的结构定义如下所示:

typedf struct inset{
    uint32_t encoding;//编码方式 有三种 默认 INSET_ENC_INT16
    uint32_t length;//集合元素个数
    int8_t contents[];//实际存储元素的数组 
                      //元素类型并不一定是ini8_t类型,柔性数组不占intset结构体大小,并且数组中的元
                      //素从小到大排列
}inset;
#define INTSET_ENC_INT16 (sizeof(int16_t))   //16位,2个字节,表示范围-32,768~32,767
#define INTSET_ENC_INT32 (sizeof(int32_t))   //32位,4个字节,表示范
                                             //围-2,147,483,648~2,147,483,647
#define INTSET_ENC_INT64 (sizeof(int64_t))   //64位,8个字节,表示范
//围-9,223,372,036,854,775,808~9,223,372,036,854,775,807

  

  编码格式 encoding:共有三种,INTSET_ENC_INT16、INSET_ENC_INT32 和 INSET_ENC_INT64 三种,分别对应不用的范围。Redis 为了尽可能地节省内存,会根据插入数据的大小选择不一样的类型来进行存储。

  元素数量 length:记录了保存数据的数组 contents 中共有多少个元素,这样获取多个数的时间复杂度就是 O(1)。

  数组 contents:真正存储数据的地方,数组时按照从小到大有序排列的,并且不包含任何重复项。

 

  intset 的示意图如下所示:

c redis 有序集合 redis有序set原理_数据_02

 

 

  intset 中整数的升级过程

  1. 了解旧的存储格式,计算出目前已有元素占用内存大小,计算规则是 length * encoding,如4 * 16 = 64。

  2. 确定新的编码格式,当原有的编码格式不能存储下新增的数据时,此时就要选择新的合适的编码格式;

  3. 根据新的编码格式计算出需要新增的内存大小,然后从尾部将数据插入。

  4. 根据新的编码格式重置之前的值,此时 contents 存在两种编码格式设置的值,就需要进行统一,从插入新数据的起始位置开始,从后向前将之前的数据按照新的编码格式进行移动和设置。从后往前是为了防止数据被覆盖。

  优点:根据存入的数据大小选择合适的编码方式,且只在必要的时候进行升级操作,节省内存。

  缺点:升级过程耗费系统资源,还有就是不支持降级,一旦升级就不可降级。

c redis 有序集合 redis有序set原理_List_03

 

 

 

 


 

  zset 是 Redis 提供的一个非常特殊的数据结构,常用作排行榜等功能,以用户 Id 为 value,关注时间或者分数作为 score 进行排序。与其他数据结构相似,zset也有两种不同的实现,分别是 zipList 和 skipList。

  zipList:满足下面两个条件

  1. {score, value} 键值对少于 128个

  2. 每个元素的长度小于 64 字节

 

  skipList:不满足以上两个条件时使用跳表、hash 和 skipList

  1. hash 用来存储 value 到 score 的映射,这样就可以在 O(1) 时间内找到 value 对应的分数

  2. skipList 按照从小到大的顺序存储分数。

  3. skipList 每个元素的值都是 {score, value} 对。

 

  只用 zipList 的示意图如下所示:

c redis 有序集合 redis有序set原理_c redis 有序集合_04

 

 

  使用跳表时的示意图:

 

c redis 有序集合 redis有序set原理_c redis 有序集合_05

 

  跳表 skipList

  跳表 skipList 在 Redis 中的运用场景只有一个,那就是作为有序列表 zset 的底层实现。跳表可以保证增删改查等操作时的时间复杂度为 O(logN),这个性能可以与平衡树相媲美,但实现方式上却更加简单,唯一美中不足的就是跳表占用的空间比较大,其实就是一种空间换时间的思想。跳表的结构如下所示:

c redis 有序集合 redis有序set原理_List_06

 

  Redis 中的跳表一个节点最高可以达到 64层,一个跳表中最多可以存储 2^64个元素。跳表中,每个节点都是一个 skipListNode,每个跳表的节点也都维护者一个 score 值,这个值在跳表中是按照从小到大的顺序排列好的。

typedf struct zskiplist{
    	//头节点
    	struct zskiplistNode *header;
    	//尾节点
    	struct zskiplistNode *tail;
    	// 跳表中元素个数
    	unsigned long length;
    	//目前表内节点的最大层数
   	 int level;
}zskiplist;

  

 

  header:指向跳表的头节点,通过这个指针可以直接找到表头,时间复杂度为 O(1);

  tail:指向跳表的尾节点,通过这个指针可以找到表尾,时间复杂度为O(1);

  length:记录跳表的长度,即不包括头节点,整个跳表中有多少个元素;

  level:记录当前跳表内,所有节点中层数最大的 level;

 

  zskipListNode 的结构定义如下:

typedf struct zskiplistNode{
    	sds ele;// 具体的数据
    	double score;// 分数
    	struct zskiplistNode *backward;//后退指针
    	struct zskiplistLevel{  
        struct zskiplistNode *forward;//前进指针forward
        unsigned int span;//跨度span
    }level[];//层级数组 最大32
}zskiplistNode;

  

 

  ele:真正的数据,每个节点的数据都是唯一的,但节点的分数 score可以是一样的。两个相同分数 score 的节点是按照元素的字典序进行排序的;

  score:各个节点中的数字是节点所保留的分数 score,在条表中,节点按照各自保存的分数从小到大排列;

  backward:用于从表尾向表头便利,每个节点只有一个后退指针,即每次只能后退一步;

  层级数组:这个数组中的每个节点都有两个属性,forward 指向下一个节点,span 跨度用来计算当前节点在跳表中的一个排名,这就为 zset 提供了一个查看排名的方法。数组中的每个节点中用1、2、3等字样标记节点的各个层,L1 代表第一层,L2代表第二层,L3代表第三次,以此类推;

c redis 有序集合 redis有序set原理_数据_07

 

 

c redis 有序集合 redis有序set原理_数据_08