Redis实现之对象(三)
集合对象
集合对象的编码可以是intset或者hashtable,intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。举个栗子,以下代码将创建一个图1-12所示的intset编码集合对象:
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"

图1-12 inset编码的numbers集合对象
另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL,以下的示例,将创建一个如图1-13所示的hashtable编码集合对象:
127.0.0.1:6379> SADD fruits "apple" "banana" "cherry"
(integer) 3
127.0.0.1:6379> OBJECT ENCODING fruits
"hashtable"

图1-13 hashtable编码的fruits集合对象
编码的转换
当集合对象可以同时满足以下两个条件时,对象使用intset编码:
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量不超过512个
不能满足以上两个条件对的集合对象需要使用hashtable编码,注意,第一个条件是无法修改的,但第二个条件的上限值可以修改,具体请看配置文件中关于set-max-intset-entries选项的说明
对于使用intset编码的集合对象来说,当使用intset编码所需的两个条件的任意一个不能被满足时,就会执行对象的编码转换操作,原本保存在整数集合中的所有元素都会被转移并保存到字典里面,并且对象的编码也会从intset变为hashtable
举个栗子,以下代码创建一个只包含整数元素的集合对象,该对象原来的编码为intset,但我们只要添加一个字符串元素,集合对象的编码转移操作就会被执行
127.0.0.1:6379> SADD numbers 1 3 5
(integer) 3
127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
127.0.0.1:6379> SADD numbers "seven"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"
除此之外,如果我们创建一个包含512个整数元素的集合对象,那么对象的编码应该是intset。但是,只要我们再往集合添加一个整数元素,使得这个集合的元素变为513,那么对象的编码转换操作就会被执行:
127.0.0.1:6379> EVAL "for i=1, 512 do redis.call('SADD', KEYS[1], i) end" 1 integers
(nil)
127.0.0.1:6379> SCARD integers
(integer) 512
127.0.0.1:6379> OBJECT ENCODING integers
"intset"
127.0.0.1:6379> SADD integers 10086
(integer) 1
127.0.0.1:6379> SCARD integers
(integer) 513
127.0.0.1:6379> OBJECT ENCODING integers
"hashtable"
集合命令的实现
因为集合键的值为集合对象,所以用于集合键的所有命令都是针对集合对象来操作的,表1-10列出了其中一部分集合键的命令,以及这些命令在不同编码的集合对象下的实现方法
| 命令 | intset编码的实现方法 | hashtable编码的实现方法 |
| SADD | 调用intsetAdd函数,将所有新元素添加到整数集合里面 | 调用dictAdd,以新元素为键,NULL为值,将键值对添加到字典里面 |
| SCARD | 调用intsetLen函数,返回整数集合所包含的元素数量,这个数量就是集合对象所包含的元素数量 | 调用dictSize函数,返回字典所包含的键值对数量,这个数量就是集合对象所包含的元素数量 |
| SISMEMBER | 调用intsetFind函数,在整数集合中查找给定的元素,如果找到了说明元素存在于集合,没找到则说明元素不存在于集合 | 调用dictFind 函数,在字典的键中查找给定的元素,如果找到了说明元素存在于集合,没找到则说明元素不存在于集合 |
| SMEMBERS | 遍历整个整数集合,使用intsetGet函数返回集合元素 | 遍历整个字典,使用dictGetKey函数返回字典的键作为集合元素 |
| SRANDMEMBER | 调用intsetRandom函数,从整数集合中随机返回一个元素 | 调用dictGetRandomKey函数,从字典中随机返回一个字典键 |
| SPOP | 调用intsetRandom函数,从整数集合中随机取出一个元素,在将这个随机元素返回给客户端之后,调用intsetRemove函数, 将随机元素从整数集合中删除掉 | 调用dictGetRandomKey函数,从字典中随机取出一个字典键,在将这个随机字典键的值返回给客户端之后,调用 dictDelete函数,从字典中删除随机字典键所对应的键值对 |
| SREM | 调用intsetRemove函数,从整数集合中删除所有给定的元素 | 调用dictDelete函数,从字典中删除所有键为给定元素的键值对 |
有序集合对象
有序集合的编码可以是ziplist或者skiplist,ziplist编码的压缩列表中,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向
举个栗子,如果我们执行以下ZADD命令,那么服务器将创建一个有序集合对象作为price键的值:
127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry
(integer) 3
127.0.0.1:6379> OBJECT ENCODING price
"ziplist"
price这个值对象如图1-14所示,而对象所使用的的压缩列表如图1-15所示

图1-14 ziplist编码的有序集合对象

图1-15 有序集合元素在压缩列表中按分值从小到大排列
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表
redis.h
typedef struct zset {
dict *dict;
zskiplist *zsl;
} zset;
typedef struct zskiplistNode {
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zset结构中的的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的obj属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的
除此之外,zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过这个字典,程序可以在O(1)的时间复杂度内查找给定成员的分值,ZSCORE命令就是根据这一特性实现的,而很多其他有序集合命令都在实现的内部用到了这一特性
有序集合中每个元素的成员都是一个字符串对象,而每个元素的分值都是一个double类型的浮点数。值得一提的是,虽然zset结构同时使用跳跃表和字典来保存有序集合元素,但这两种数据结构都会通过指针来共享相同元素的成员和分值,所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或分值,也不会因为浪费额外的内存
为什么有序集合需要同时使用跳跃表和字典来实现?在理论上,有序集合可以单独使用字典或者跳跃表其中一种数据结构来实现,但无论使用字典还是跳跃表,在性能上比起同时使用字典和跳跃表都会有所降低。举个例子,如果我们只是用字典来实现有序集合,那么虽然可以在O(1)的时间复杂度内查找成员对应的分值,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比如:ZRANK、ZRANGE等命令时,程序都需要对字典的所有元素进行排序,完成这种排序至少需要O(N logN)的时间复杂度,以及额外的O(N)内存空间(因为要创建一个数组来保存排序后的元素)
另一方面如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作时的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的时间复杂度将从O(1)上升至O(logN)。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合
举个栗子,如果前面的price键创建的不是ziplist编码的有序集合对象,而是skiplist编码的有序集合对象,那么这个有序集合对象将会是图1-16所示的样子,而对象所使用的zset结构将会是图8-17所示的样子

图1-16 skiplist编码的有序集合对象

图1-17 有序集合元素同时被保存在字典和跳跃表中
编码的转换
当有序集合对象可以同时满足以下条件时,对象使用ziplist编码:
- 有序集合保存的元素数量小于128个
- 有序集合保存的所有元素的长度小于64字节
不能满足以上两个条件的有序集合对象将使用skiplist编码
# 对象包含了 128 个元素
127.0.0.1:6379> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers
(nil)
127.0.0.1:6379> ZCARD numbers
(integer) 128
127.0.0.1:6379> OBJECT ENCODING numbers
"ziplist"
# 再添加一个新元素
127.0.0.1:6379> ZADD numbers 3.14 pi
(integer) 1
# 对象包含的元素数量变为 129 个
127.0.0.1:6379> ZCARD numbers
(integer) 129
# 编码已改变
127.0.0.1:6379> OBJECT ENCODING numbers
"skiplist"
以下代码则展示了有序集合对象因为元素的成员过长而引发编码转换的情况:
# 向有序集合添加一个成员只有三字节长的元素
127.0.0.1:6379> ZADD blah 1.0 www
(integer) 1
127.0.0.1:6379> OBJECT ENCODING blah
"ziplist"
# 向有序集合添加一个成员为 66 字节长的元素
127.0.0.1:6379> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
(integer) 1
# 编码已改变
127.0.0.1:6379> OBJECT ENCODING blah
"skiplist"
有序集合命令的实现
因为有序集合键的值为哈希值,所以用于有序集合键的所有命令都是针对哈希对象来构建的,表1-11列出了其中一部分有序集合键命令,以及这些命令在不同编码的哈希对象下的实现方法
| 命令 | ziplist编码的实现方法 | zset编码的实现方法 |
| ZADD | 调用ziplistInsert函数, 将成员和分值作为两个节点分别插入到压缩列表 | 先调用zslInsert函数,将新元素添加到跳跃表,然后调用dictAdd 函数,将新元素关联到字典 |
| ZCARD | 调用ziplistLen函数,获得压缩列表包含节点的数量,将这个数量除以2得出集合元素的数量 | 访问跳跃表数据结构的length属性, 直接返回集合元素的数量 |
| ZCOUNT | 遍历压缩列表,统计分值在给定范围内的节点的数量 | 遍历跳跃表,统计分值在给定范围内的节点的数量 |
| ZRANGE | 从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素 | 从表头向表尾遍历跳跃表,返回给定索引范围内的所有元素 |
| ZREVRANGE | 从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素 | 从表尾向表头遍历跳跃表,返回给定索引范围内的所有元素 |
| ZRANK | 从表头向表尾遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名 | 从表头向表尾遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名 |
| ZREVRANK | 从表尾向表头遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名 | 从表尾向表头遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名 |
| ZREM | 遍历压缩列表,删除所有包含给定成员的节点,以及被删除成员节点旁边的分值节点 | 遍历跳跃表,删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联 |
| ZSCORE | 遍历压缩列表,查找包含了给定成员的节点,然后取出成员节点旁边的分值节点保存的元素分值 | 直接从字典中取出给定成员的分值 |
Redis实现之对象(三)的更多相关文章
- Redis数据结构和对象三
1.Redis 对象系统 Redis用到的所有主要数据结构,简单动态字符串(SDS).双端链表.字典.压缩列表.整数集合.跳跃表. Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些 ...
- redis底层设计(三)——redis数据类型
今天我们来看一下redis的数据类型.既然redis的键值对可以保存不同类型的值,那么很自然就需要对键值对的类型进行检查以及多态处理.下面我们将对redis所使用的对象系统进行了解,并分别观察字符串. ...
- redis 存储java对象 两种方式
根据redis的存储原理,Redis的key和value都支持二进制安全的字符串 1.利用序列化和反序列化的方式存储java对象我们可以通过对象的序列化与反序列化完成存储于取出,这样就可以使用redi ...
- Redis源代码剖析--对象object
前面一系列的博客分析了Redis的基本数据结构,有动态字符串sds.双端链表sdlist.字典dict.跳跃表skiplist.整数集合intset和压缩列表ziplist等,这些数据结构对于用户来说 ...
- Redis实战之征服 Redis + Jedis + Spring (三)
一开始以为Spring下操作哈希表,列表,真就是那么土.恍惚间发现“stringRedisTemplate.opsForList()”的强大,抓紧时间恶补下. 通过spring-data-redis完 ...
- 征服 Redis + Jedis + Spring (三)—— 列表操作【转】
一开始以为Spring下操作哈希表,列表,真就是那么土.恍惚间发现“stringRedisTemplate.opsForList()”的强大,抓紧时间恶补下. 相关链接: 征服 Redis 征服 Re ...
- Redis 小白指南(三)- 事务、过期、消息通知、管道和优化内存空间
Redis 小白指南(三)- 事务.过期.消息通知.管道和优化内存空间 简介 <Redis 小白指南(一)- 简介.安装.GUI 和 C# 驱动介绍> 讲的是 Redis 的介绍,以及如何 ...
- redis 序列化存入对象
redis 序列化存入对象 //序列化 public static byte [] serialize(Object obj){ ObjectOutputStream obi=null; ByteAr ...
- Redis如何存储对象与集合示例详解
前言 大家都知道在项目中,缓存以及mq消息队列可以说是不可或缺的2个重要技术.前者主要是为了减轻数据库压力,大幅度提升性能.后者主要是为了提高用户的体验度,我理解的是再后端做的一个ajax请求(异 ...
- 关于Spring Data redis几种对象序列化的比较
redis虽然提供了对list set hash等数据类型的支持,但是没有提供对POJO对象的支持,底层都是把对象序列化后再以字符串的方式存储的.因此,Spring data提供了若干个Seriali ...
随机推荐
- git&github学习【尚硅谷】
2019/01/17 18:22 集中式版本工具会有单点故障的问题 分布式版本工具能够避免单点故障 git在本地的结构: 团队内部协作: pull push add commit 等等 关于g ...
- Counting blessings can actually increase happiness and health by reminding us of the good things in life.
Counting blessings can actually increase happiness and health by reminding us of the good things in ...
- hibernate笔记4--qbc查询
Criteria:是一种完全面向对象的查询方式,Criteria查询也叫做qbc查询(query by Criteria). 查询全部,分页查询,统计查询,条件查询,排序查询,离线查询 ...
- NC57访问报错:java.sql.SQLException: Io 异常: Got minus one from a read call
一.报错信息 1. 前端登录界面 2. 后台应用日志 报错信息一致为: $$callid= $$thread=[Service Monitor and Runtime Enroment] $$ho ...
- 定制Banner
1.修改Banner (1)在SpringBoot启动的时候会有一个默认启动的图案 (2)在src/main/resources下新建一个banner.txt (3)通过http://patorjk. ...
- <转载>为什么VR不可能成功?
这是一个来自Quora的回答,我把要点总结翻译了下,供大家参考批判. How big and issue the nausea problem for Virtual Reality products ...
- MVC下c#对接微信公众平台开发者模式
在ashx文件中进行HttpContext的处理: using System; using System.Collections.Generic; using System.Linq; using S ...
- 项目移动后报error LNK1123
VS20101.解决方案窗口 项目|项目属性|配置属性|清单工具|输入和输出|嵌入清单 “是”改为“否”:2.项目|项目属性|配置属性|连接器|清单文件|嵌入清单 “是”改为“否”:3.对于64位的操 ...
- 8. String to Integer
Implement atoi to convert a string to an integer. Hint: Carefully consider all possible input cases. ...
- js 中//<![CDATA[ 意义
CDATA内部所有东西都会被解析器忽略,加入文本中包含了大量< 和 $符号,就像编程中经常出现的情况一样,那么这个元素就可以被定义为一个CDATA部分 ,CDATA 区段开始于 "&l ...