Redis对象系统——五种对象、底层实现和常用命令

注:本文章总结自黄建宏前辈的《Redis设计与实现》,仅当做笔记和备忘录,并非本人原创。

第八章 对象

前面介绍的都是基本数据结构,Redis并没有直接使用这些基本数据结构,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型。
Redis通过引用计数实现了内存回收,同时还实现了对象共享机制。

ps: redis用引用计数法的技术实现了内存回收。但是引用计数法的一个致命弱点就是,如果遇到循环引用,那么循环引用的对象将永不被回收。因此造成内存泄露。所以JVM摒弃这个内存回收技术,采用根路径可达算法。那么Redis是怎么解决引用计数的循环引用问题的呢?

Redis为什么要使用对象而不是基本数据结构?

  • Redis可以在执行命令之前,根据对象的类型来判断一个对象是否可以执行给定的命令。
  • 可以针对不同的使用场景,为对象设置不同的数据结构实现,从而优化对象在不同场景下的使用效率。
  • 对象带有访问时间记录信息,该信息可以用于计算数据库键的空转时间。空转时间是服务器优化内存,删除无用键的依据。
对象类型与编码

Redis中的键和值都是对象。Redis中的每个对象都由一个redisObject结构表示,该结构中和保存数据有关的3个属性分别是type属性、encoding属性和ptr属性:

typedef struct redisObject {

    // 类型,':'表示占用的bit数
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 对象最后一次被访问的时间
    unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */

    // 引用计数
    int refcount;

    // 指向实际值的指针
    void *ptr;

} robj;

type属性记录了对象的类型,它可以是下表中的任何一个

类型常量对象名称
REDIS_STRING字符串对象
REDIS_LIST列表对象
REDIS_HASH哈希对象
REDIS_SET集合对象
REDIS_ZSET有序集合对象

对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是上面五种的任意一种。TYPE命令返回的是值的类型,而不是键的类型(因为键都是REDIS_STRING类型)。

ptr指针指向对象的底层实现数据结构。encoding属性记录了对象所使用的编码,也即是说这个对象使用了什么数据结构作为对象的底层实现。
通过encoding属性来设定对象所使用的编码,而不是为特定类型的对象关联一种固定的编码,极大地提升了Redis的灵活性和效率,因为Redis可以根据不同的使用场景来为一个对象设置不同的编码,从而优化对象在某一场景下的效率。

字符串对象

字符串对象可以选择的编码是int、raw或者embstr。他们的使用情况如下:

  • int:一个字符串对象保存的是整数,并且这个整数可以用long类型表示,那么就将这个整数保存在ptr中(将void*转换成long),并将字符串对象编码为int。
  • raw:保存的是一个字符串值,并且这个字符串值的长度大于39字节,那么将用一个SDS来保存这个字符串,并将对象编码设置为raw。
  • embstr:保存的是一个字符串值,但长度小于等于39字节,那么字符串对象将使用embstr编码方式。

raw和embstr的区别?
rmbstr编码是专门用于保存短字符串的一种优化编码方式,它只进行一次内存分配和回收,它的StringObject和SDS紧挨。而raw编码单独分配StringObject和SDS,需要两次内存分配和回收。

什么时候发生编码转化?
在int和embstr的条件不满足时,会自动转化为raw编码。此外,对embstr执行任何修改命令时,程序会将对象编码转换成raw,因为embstr没有任何操作函数,它是只读的。

命令作用
SET设置值
GET获取值
APPEND将给定字符串追加到本字符串末尾
INCRBYFLOAT将字符串转化为long double然后加上给定的数字
INCRBY对整数值进行加法计算,只能用于int编码
DECRBY对整数值进行减法计算,只能用于int编码
STRLEN返回字符串的长度
SETRANGE将字符串特定索引上的值设置为给定的字符
GETRANGE返回字符串指定索引上的字符
redis 127.0.0.1:6379> SET name "zhoucheng"
OK
redis 127.0.0.1:6379> SET age 20
OK
redis 127.0.0.1:6379> GET name
"zhoucheng"
redis 127.0.0.1:6379> APPEND name "sir"
(integer) 12
redis 127.0.0.1:6379> GET name
"zhouchengsir"
redis 127.0.0.1:6379> INCRBY age 1
(integer) 21
redis 127.0.0.1:6379> DECRBY age 1
(integer) 20
redis 127.0.0.1:6379> STRLEN name
(integer) 12
redis 127.0.0.1:6379> STRLEN age
(integer) 2
redis 127.0.0.1:6379> SETRANGE name 0 "A"
(integer) 12
redis 127.0.0.1:6379> GET name
"Ahouchengsir"
redis 127.0.0.1:6379> GET age
"20"
列表对象

列表对象可以是ziplist或者linkedlist。
Ziplist 是为了尽可能地节约内存而设计的特殊编码双端链表。它的所有元素在内存中紧挨在一起,却又不能通过下标访问。ziplist结构参见上面“第七章 压缩列表”。
LinkedList是以链表的形式,每个节点是一个字符串对象。字符串对象是Redis五种类型的对象中唯一一种会被其他四种对象嵌套的对象。

编码转换
当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:

  • 列表对象保存的所有字符串元素的长度都小于64字节;
  • 列表对象保存的元素数量小于512个;

不能同时满足这两个条件的,就需要使用linkelist编码。上面的两个值都是可以通过配置文件修改的。

命令作用
LPUSH插入表头
RPUSH插入表尾
LPOP从表头返回并删除
RPOP从表尾返回并删除
LINDEX返回指定节点保存的元素
LLEN返回表的长度
LINSERT将值插入到指定位置
LREM删除包含了给定元素的节点
LTRIM删除表中所有不在指定索引范围内的节点
LSET将指定索引的值更新为指定的值
redis 127.0.0.1:6379> LPUSH list 1
(integer) 1
redis 127.0.0.1:6379> LPUSH list 2
(integer) 2
redis 127.0.0.1:6379> LPUSH list 3
(integer) 3
redis 127.0.0.1:6379> LPUSH list "name"
(integer) 4
redis 127.0.0.1:6379> LPUSH list "name"
(integer) 5
redis 127.0.0.1:6379> RPUSH list "zhou"
(integer) 6
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "name"
2) "name"
3) "3"
4) "2"
5) "1"
6) "zhou"
redis 127.0.0.1:6379> LPOP list
"name"
redis 127.0.0.1:6379> RPOP list
"zhou"
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "name"
2) "3"
3) "2"
4) "1"
redis 127.0.0.1:6379> LINDEX list 1
"3"
redis 127.0.0.1:6379> LLEN list
(integer) 4
redis 127.0.0.1:6379> LINSERT list BEFORE 1 "zhou"
(integer) 5
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "name"
2) "3"
3) "2"
4) "zhou"
5) "1"
redis 127.0.0.1:6379> LINSERT list AFTER 1 "cheng"
(integer) 6
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "name"
2) "3"
3) "2"
4) "zhou"
5) "1"
6) "cheng"
redis 127.0.0.1:6379> LREM list 1 "name"
(integer) 1
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "3"
2) "2"
3) "zhou"
4) "1"
5) "cheng"
redis 127.0.0.1:6379> LTRIM list 0 2
OK
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "3"
2) "2"
3) "zhou"
redis 127.0.0.1:6379> LSET list 1 4
OK
redis 127.0.0.1:6379> LRANGE list 0 -1
1) "3"
2) "4"
3) "zhou"
哈希对象

哈希对象可用的编码是ziplist或者hashtable。

ziplist方式的哈希对象使用ziplist作为底层数据结构,键和值节点紧挨在一起,插入到压缩列表末尾。
hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:

  • 字典的每个键都是一个字符串对象,对象中保存了键值对的键;
  • 字典的每个值都是一个字符串对象,对象中保存了键值对的值。

编码转换
当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:

  • 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;
  • 哈希对象保存的键值对数量小于512个;

不能满足这两个条件的哈希对象需要使用hashtable编码。上面两个上限值可以通过配置文件修改。

问题:Redis中哈希对象有两种编码方式,分别是ziplist、hashtable方式。哈希对象,总得体现哈希算法,使得基本操作达到O(1)的效率。hashtable编码方式使用字典,也即是Java中hashMap的方式,这个我可以理解。但是,ziplist方式所有元素都是紧挨的,它是怎么实现hash,并使得查询等操作有O(1)的时间效率的呢?
已解答:https://blog.csdn.net/zhoucheng05_13/article/details/79864568

命令作用
HSET插入新的键值对
HGET查找给定键,并返回它的值
HEXISTS查找给定元素是否存在
HDEL删除指定键所在的键值对
HLEN返回键值对的数目
HGETALL返回所有的键和值
redis 127.0.0.1:6379> HSET hobj "name" "cheng"
(integer) 1
redis 127.0.0.1:6379> HSET hobj "age" 12
(integer) 1
redis 127.0.0.1:6379> HSET hobj "set" "male"
(integer) 1
redis 127.0.0.1:6379> HGETALL hobj
1) "name"
2) "cheng"
3) "age"
4) "12"
5) "set"
6) "male"
redis 127.0.0.1:6379> HGET hobj age
"12"
redis 127.0.0.1:6379> HEXISTS hobj name
(integer) 1
redis 127.0.0.1:6379> HEXISTS hobj hobby
(integer) 0
redis 127.0.0.1:6379> HGETALL hobj
1) "name"
2) "cheng"
3) "age"
4) "12"
5) "set"
6) "male"
redis 127.0.0.1:6379> HDEL hobj set
(integer) 1
redis 127.0.0.1:6379> HGETALL hobj
1) "name"
2) "cheng"
3) "age"
4) "12"
redis 127.0.0.1:6379> HLEN hobj
(integer) 2
集合对象

集合对象的编码可以是intset或者hashtable。
intset编码使用整数集合作为底层实现,整数集合类似于一个数组,详细见上面介绍。
hashtable编码使用字典(类似于Java中的HashMap)作为底层实现。

当下面两个条件都被满足时,使用intset编码,否则,使用hashtable编码

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不超过512

第二个条件的上限可以通过配置文件修改。当两个条件中任一个不满足时,就会自动执行编码转换操作:

redis 127.0.0.1:6379> SADD numbers 1 3 4 5 6
(integer) 5
redis 127.0.0.1:6379> OBJECT ENCODING numbers
"intset"
redis 127.0.0.1:6379> SADD numbers "seven"
(integer) 1
redis 127.0.0.1:6379> OBJECT ENCODING numbers
"hashtable"
命令作用
SADD添加新元素
SCARD返回元素数量
SISMEMBER判断给定的元素是否在集合中
SMEMBERS返回所有元素
SRANDMEMBER随机返回集合中的一个元素
SPOP从列表中随机删除一个值
SREM删除所有给定的元素
redis 127.0.0.1:6379> SADD setest "name" "age"
(integer) 2
redis 127.0.0.1:6379> SADD setest "sex"
(integer) 1
redis 127.0.0.1:6379> SMEMBERS setest
1) "sex"
2) "age"
3) "name"
redis 127.0.0.1:6379> SCARD setest
(integer) 3
redis 127.0.0.1:6379> SISMEMBER setest "name"
(integer) 1
redis 127.0.0.1:6379> SISMEMBER setest "hobby"
(integer) 0
redis 127.0.0.1:6379> SRANDMEMBER setest
"sex"
redis 127.0.0.1:6379> SRANDMEMBER setest
"sex"
redis 127.0.0.1:6379> SRANDMEMBER setest
"sex"
redis 127.0.0.1:6379> SRANDMEMBER setest
"name"
redis 127.0.0.1:6379> SPOP setest
"sex"
redis 127.0.0.1:6379> SMEMBERS setest
1) "age"
2) "name"
redis 127.0.0.1:6379> SREM setest "age"
(integer) 1
redis 127.0.0.1:6379> SMEMBERS setest
1) "name"
有序集合对象

有序集合的编码可以是ziplist或者skiplist。

ziplist底层使用压缩列表,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),第二个节点保存元素的分值(Score)。压缩列表内按照分值升序排列。

skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:

/*
 * 有序集合
 */
typedef struct zset {

    // 字典,键为成员,值为分值
    // 用于支持 O(1) 复杂度的按成员取分值操作
    dict *dict;

    // 跳跃表,按分值排序成员
    // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
    // 以及范围操作
    zskiplist *zsl;

} zset;

正如源码注释所说,之所以同时采用两种数据结构来实现,是为了互相弥补对方的不足。zskiplist是为了弥补哈希表不能进行范围查询的缺点,而哈希表将单个元素的查询操作从zskiplist的O(logN)优化到了哈希表的O(1)。
虽然同时使用了两种结构,但是所有的元素节点都是共用的,即真正的节点只有一份,但是它被两种方式使用了。因此,同时使用两种数据结构,并没有浪费空间,且同时提高了范围查询和单值查询的效率。

有序集合的每个元素成员都是一个字符串对象,而每个元素的分支都是一个double类型的浮点数。

编码的挑选和转换
当有序集合对象同时满足以下两个条件时,对象使用ziplist编码:

  • 有序集合保存的元素数量小于128个;
  • 有序集合保存的所有元素成员的长度都小于64字节。

不能满足任何一个,则使用skiplist编码。以上两个条件的上限值是可以在配置文件里修改的。当两个条件的任何一个不被满足时,程序会自动执行编码转换操作。

它包含如下常用命令:

命令作用
ZADD插入元素
ZCARD返回集合元素的数量
ZCOUNT统计分值在给定范围内的节点的数量
ZRANGE返回给定索引范围内的所有元素
ZREVRANGE倒序返回给定范围内的元素
ZRANK返回元素的集合中的排名
ZREVRANK返回元素在集合中的倒序排名
ZREM删除所有包含了给定成员的跳跃表节点
ZSCORE返回给定元素的分值
redis 127.0.0.1:6379> ZADD fruits 2.3 pear 1.5 apple 3.2 banana
(integer) 3
redis 127.0.0.1:6379> ZCARD fruits
(integer) 3
redis 127.0.0.1:6379> ZCOUNT fruits 2 5
(integer) 2
redis 127.0.0.1:6379> ZRANGE fruits 2 5
1) "banana"
redis 127.0.0.1:6379> ZRANGE fruits 0 5
1) "apple"
2) "pear"
3) "banana"
redis 127.0.0.1:6379> ZREVRANGE fruits 0 5
1) "banana"
2) "pear"
3) "apple"
redis 127.0.0.1:6379> ZRANK fruits pear
(integer) 1
redis 127.0.0.1:6379> ZRANK fruits apple
(integer) 0
redis 127.0.0.1:6379> ZREVRANK fruits apple
(integer) 2
redis 127.0.0.1:6379> ZREVRANGE fruits 0 5
1) "banana"
2) "pear"
3) "apple"
redis 127.0.0.1:6379> ZRANGE fruits 0 5
1) "apple"
2) "pear"
3) "banana"
redis 127.0.0.1:6379> ZREM fruits apple
(integer) 1
redis 127.0.0.1:6379> ZRANGE fruits 0 5
1) "pear"
2) "banana"
redis 127.0.0.1:6379> ZSCORE fruits banana
"3.2000000000000002"
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值