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列出了其中一部分集合键的命令,以及这些命令在不同编码的集合对象下的实现方法

表8-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列出了其中一部分有序集合键命令,以及这些命令在不同编码的哈希对象下的实现方法

表   8-11有序集合命令的实现方法
命令 ziplist编码的实现方法 zset编码的实现方法
ZADD 调用ziplistInsert函数, 将成员和分值作为两个节点分别插入到压缩列表 先调用zslInsert函数,将新元素添加到跳跃表,然后调用dictAdd 函数,将新元素关联到字典
ZCARD 调用ziplistLen函数,获得压缩列表包含节点的数量,将这个数量除以2得出集合元素的数量 访问跳跃表数据结构的length属性, 直接返回集合元素的数量
ZCOUNT 遍历压缩列表,统计分值在给定范围内的节点的数量 遍历跳跃表,统计分值在给定范围内的节点的数量
ZRANGE 从表头向表尾遍历压缩列表,返回给定索引范围内的所有元素 从表头向表尾遍历跳跃表,返回给定索引范围内的所有元素
ZREVRANGE 从表尾向表头遍历压缩列表,返回给定索引范围内的所有元素 从表尾向表头遍历跳跃表,返回给定索引范围内的所有元素
ZRANK 从表头向表尾遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名 从表头向表尾遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后,途经节点的数量就是该成员所对应元素的排名
ZREVRANK 从表尾向表头遍历压缩列表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名 从表尾向表头遍历跳跃表,查找给定的成员,沿途记录经过节点的数量,当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名
ZREM 遍历压缩列表,删除所有包含给定成员的节点,以及被删除成员节点旁边的分值节点 遍历跳跃表,删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联
ZSCORE 遍历压缩列表,查找包含了给定成员的节点,然后取出成员节点旁边的分值节点保存的元素分值 直接从字典中取出给定成员的分值

原文地址:https://www.cnblogs.com/beiluowuzheng/p/9737243.html

时间: 2024-10-28 01:27:45

Redis实现之对象(三)的相关文章

ServiceStack.Redis之IRedisClient<第三篇>

事实上,IRedisClient里面的很多方法,其实就是Redis的命令名.只要对Redis的命令熟悉一点就能够非常快速地理解和掌握这些方法,趁着现在对Redis不是特别了解,我也对着命令来了解一下这些方法. 一.属性 IRedisClient的属性如下: 属性 说明 ConnectTimeout  连接超时 Db 当前数据库的ID或下标 DbSize  当前数据库的 key 的数量 HadExceptions    Hashes  存储复杂对象,一个value中有几个field  Host 

Redis笔记整理(三):进阶操作与高级部分

[TOC] Redis笔记整理(三):进阶操作与高级部分 Redis发布订阅 Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息. Redis客户端可以订阅任意数量的频道. 下图展示了频道channel1,以及订阅这个频道的三个客户端--client1,client2,client5之间的关系. 当有新消息通过PUBLISH命令发送给频道channel1时,这个消息就会被发送给订阅它的三个客户端: 相关操作命令如下: 命令 描述 PSUBS

Redis 小白指南(三)- 事务、过期、消息通知、管道和优化内存空间

Redis 小白指南(三)- 事务.过期.消息通知.管道和优化内存空间 简介 <Redis 小白指南(一)- 简介.安装.GUI 和 C# 驱动介绍> 讲的是 Redis 的介绍,以及如何在 Windows 上安装并使用,一些 GUI 工具和自己简单封装的 RedisHelper. <Redis 小白指南(二)- 聊聊五大类型:字符串.散列.列表.集合和有序集合>讲的是 Redis 中最核心的内容,最常用的就是和数据类型打交道. 目录 事务 过期时间 消息通知 管道 优化内存空间

[转] Redis 存储List对象

如果需要用到Redis存储List对象,而list又不需要进行操作,可以按照MC的方式进行存储,不过Jedis之类的客户端没有提供API,可以有两种思路实现: 1.      分别序列化 elements ,然后 set 存储 2.    序列化List对象,set存储 这两种方法都类似MC的 Object方法存储,运用这种方式意味着放弃Redis对List提供的操作方法. import net.spy.memcached.compat.CloseUtil; import net.spy.mem

hibernate对象三种状态

hibernate里对象有三种状态: 1,Transient 瞬时 :对象刚new出来,还没设id,设了其他值. 2,Persistent 持久:调用了save().saveOrUpdate(),就变成Persistent,有id 3,Detached  脱管 : 当session  close()完之后,变成Detached. 例子程序: Teacher类: 1 package com.oracle.hibernate.id; 2 3 import javax.persistence.Enti

征服 Redis + Jedis + Spring (三)—— 列表操作【转】

一开始以为Spring下操作哈希表,列表,真就是那么土.恍惚间发现“stringRedisTemplate.opsForList()”的强大,抓紧时间恶补下. 相关链接: 征服 Redis 征服 Redis + Jedis 征服 Redis + Jedis + Spring (一)—— 配置&常规操作(GET SET DEL) 征服 Redis + Jedis + Spring (二)—— 哈希表操作(HMGET HMSET) 征服 Redis + Jedis + Spring (三)—— 列表

Redis 小白指南(三)- 事务、Watch 命令、过期、消息通知、管道、优化内存空间

Redis 小白指南(三)- 事务.Watch 命令.过期.消息通知.管道.优化内存空间 简介 目录 事务 Watch 命令 过期时间 排序 消息通知 管道 优化内存空间 事务 事务是一组命令的集合,事务和命令一样都是 Redis 的最小执行单位.即一个事务中的命令,要么都执行,要么都不执行.可以思考关系型数据库中的事务特性 ACID: (1)原子性(Atomicity):在事务结束时,其中包含的更新处理要么全部执行,要么完全不执行. (2)一致性(Consistency):事务中包含的处理,要

Redis实战之征服 Redis + Jedis + Spring (三)

一开始以为Spring下操作哈希表,列表,真就是那么土.恍惚间发现“stringRedisTemplate.opsForList()”的强大,抓紧时间恶补下. 通过spring-data-redis完成LINDEX, LLEN, LPOP, LPUSH, LRANGE, LREM, LSET, LTRIM, RPOP, RPUSH命令.其实还有一些命令,当前版本不支持.不过,这些List的操作方法可以实现队列,堆栈的正常操作,足够用了. 相关链接: Redis实战 Redis实战之Redis +

Redis如何存储对象与集合示例详解

前言 大家都知道在项目中,缓存以及mq消息队列可以说是不可或缺的2个重要技术.前者主要是为了减轻数据库压力,大幅度提升性能.后者主要是为了提高用户的体验度,我理解的是再后端做的一个ajax请求(异步),并且像ribbmitmq等消息队列有重试机制等功能. 这里主要讲redis/303688.html">redis如何把对象,集合存入,并且取出.下面话不多说了,来一起看看详细的介绍吧. 1.在启动类上加入如下代码 private Jedis jedis;private JedisPoolCo

前端之JavaScript:JS之DOM对象三

js之DOM对象三 一.JS中for循环遍历测试 for循环遍历有两种 第一种:是有条件的那种,例如    for(var i = 0;i<ele.length;i++){} 第二种:for (var i in li ){} 现在我们来说一下测试一下第二种(数组和obj的) 1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8">