redis-对象
对象的类型与编码
-
edis 中的每个对象都由一个
redisObject
结构表示, 该结构中和保存数据有关的三个属性分别是type
属性、encoding
属性和ptr
属性:typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 指向底层实现数据结构的指针 void *ptr; // ... } robj;
1、类型(type)
-
对于 Redis 数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种, 因此:
- 当我们称呼一个数据库键为“字符串键”时, 我们指的是“这个数据库键所对应的值为字符串对象”;
- 当我们称呼一个键为“列表键”时, 我们指的是“这个数据库键所对应的值为列表对象”,
-
TYPE 命令的实现方式也与此类似, 当我们对一个数据库键执行 TYPE 命令时, 命令返回的结果为数据库键对应的值对象的类型, 而不是键对象的类型:
# 键为字符串对象,值为字符串对象 redis> SET msg "hello world" OK redis> TYPE msg string # 键为字符串对象,值为列表对象 redis> RPUSH numbers 1 3 5 (integer) 6 redis> TYPE numbers list # 键为字符串对象,值为哈希对象 redis> HMSET profile name Tome age 25 career Programmer OK redis> TYPE profile hash # 键为字符串对象,值为集合对象 redis> SADD fruits apple banana cherry (integer) 3 redis> TYPE fruits set # 键为字符串对象,值为有序集合对象 redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry (integer) 3 redis> TYPE price zset
-
不同类型值对象的 TYPE 命令输出
对象 对象 type
属性的值TYPE 命令的输出 字符串对象 REDIS_STRING
"string"
列表对象 REDIS_LIST
"list"
哈希对象 REDIS_HASH
"hash"
集合对象 REDIS_SET
"set"
有序集合对象 REDIS_ZSET
"zset"
-
2、编码(encoding)
-
encoding
属性记录了对象所使用的编码, 也即是说这个对象使用了什么数据结构作为对象的底层实现, 这个属性的值可以是表 8-3 列出的常量的其中一个。对象的编码
编码常量 编码所对应的底层数据结构 REDIS_ENCODING_INT
long
类型的整数REDIS_ENCODING_EMBSTR
embstr
编码的简单动态字符串REDIS_ENCODING_RAW
简单动态字符串 REDIS_ENCODING_HT
字典 REDIS_ENCODING_LINKEDLIST
双端链表 REDIS_ENCODING_ZIPLIST
压缩列表 REDIS_ENCODING_INTSET
整数集合 REDIS_ENCODING_SKIPLIST
跳跃表和字典 不同类型和编码的对象
类型 编码 对象 REDIS_STRING
REDIS_ENCODING_INT
使用整数值实现的字符串对象。
REDIS_STRING
REDIS_ENCODING_EMBSTR
使用 embstr 编码的简单动态字符串实现的字符串对象。
REDIS_STRING
REDIS_ENCODING_RAW
使用简单动态字符串实现的字符串对象。
REDIS_LIST
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的列表对象。 REDIS_LIST
REDIS_ENCODING_LINKEDLIST
使用双端链表实现的列表对象。 REDIS_HASH
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的哈希对象。 REDIS_HASH
REDIS_ENCODING_HT
使用字典实现的哈希对象。 REDIS_SET
REDIS_ENCODING_INTSET
使用整数集合实现的集合对象。 REDIS_SET
REDIS_ENCODING_HT
使用字典实现的集合对象。 REDIS_ZSET
REDIS_ENCODING_ZIPLIST
使用压缩列表实现的有序集合对象。 REDIS_ZSET
REDIS_ENCODING_SKIPLIST
使用跳跃表和字典实现的有序集合对象。 使用 OBJECT ENCODING 命令可以查看一个数据库键的值对象的编码:
对象所使用的底层数据结构 编码常量 OBJECT ENCODING 命令输出 整数 REDIS_ENCODING_INT
"int"
embstr
编码的简单动态字符串(SDS)REDIS_ENCODING_EMBSTR
"embstr"
简单动态字符串 REDIS_ENCODING_RAW
"raw"
字典 REDIS_ENCODING_HT
"hashtable"
双端链表 REDIS_ENCODING_LINKEDLIST
"linkedlist"
压缩列表 REDIS_ENCODING_ZIPLIST
"ziplist"
整数集合 REDIS_ENCODING_INTSET
"intset"
跳跃表和字典 REDIS_ENCODING_SKIPLIST
"skiplist"
3、底层实现数据结构的指针 (*ptr)
-
字符串对象
-
字符串对象的编码可以是
int
、raw
或者embstr
-
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度大于
39
字节, 那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值, 并将对象的编码设置为raw
。 -
如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于
39
字节, 那么字符串对象将使用embstr
编码的方式来保存这个字符串值。使用
embstr
编码的字符串对象来保存短字符串值有以下好处:embstr
编码将创建字符串对象所需的内存分配次数从raw
编码的两次降低为一次。- 释放
embstr
编码的字符串对象只需要调用一次内存释放函数, 而释放raw
编码的字符串对象需要调用两次内存释放函数。 - 因为
embstr
编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起raw
编码的字符串对象能够更好地利用缓存带来的优势。
-
-
列表对象
-
列表对象的编码可以是
ziplist
或者linkedlist
。-
ziplist
编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素。 -
linkedlist
编码的列表对象使用双端链表作为底层实现, 每个双端链表节点(node)都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。
-
-
当列表对象可以同时满足以下两个条件时, 列表对象使用
ziplist
编码,不能满足这两个条件的列表对象需要使用linkedlist
编码:- 列表对象保存的所有字符串元素的长度都小于
64
字节; - 列表对象保存的元素数量小于
512
个;
-
列表对象因为保存了长度太大的元素而进行编码转换的情况:
# 所有元素的长度都小于 64 字节 redis> RPUSH blah "hello" "world" "again" (integer) 3 redis> OBJECT ENCODING blah "ziplist" # 将一个 65 字节长的元素推入列表对象中 redis> RPUSH blah "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww" (integer) 4 # 编码已改变 redis> OBJECT ENCODING blah "linkedlist"
-
列表对象因为保存的元素数量过多而进行编码转换的情况:
# 列表对象包含 512 个元素 redis> EVAL "for i=1,512 do redis.call('RPUSH', KEYS[1], i) end" 1 "integers" (nil) redis> LLEN integers (integer) 512 redis> OBJECT ENCODING integers "ziplist" # 再向列表对象推入一个新元素,使得对象保存的元素数量达到 513 个 redis> RPUSH integers 513 (integer) 513 # 编码已改变 redis> OBJECT ENCODING integers "linkedlist"
- 列表对象保存的所有字符串元素的长度都小于
-
列表命令的实现
命令 ziplist
编码的实现方法linkedlist
编码的实现方法LPUSH 调用 ziplistPush
函数, 将新元素推入到压缩列表的表头。调用 listAddNodeHead
函数, 将新元素推入到双端链表的表头。RPUSH 调用 ziplistPush
函数, 将新元素推入到压缩列表的表尾。调用 listAddNodeTail
函数, 将新元素推入到双端链表的表尾。LPOP 调用 ziplistIndex
函数定位压缩列表的表头节点, 在向用户返回节点所保存的元素之后, 调用ziplistDelete
函数删除表头节点。调用 listFirst
函数定位双端链表的表头节点, 在向用户返回节点所保存的元素之后, 调用listDelNode
函数删除表头节点。RPOP 调用 ziplistIndex
函数定位压缩列表的表尾节点, 在向用户返回节点所保存的元素之后, 调用ziplistDelete
函数删除表尾节点。调用 listLast
函数定位双端链表的表尾节点, 在向用户返回节点所保存的元素之后, 调用listDelNode
函数删除表尾节点。LINDEX 调用 ziplistIndex
函数定位压缩列表中的指定节点, 然后返回节点所保存的元素。调用 listIndex
函数定位双端链表中的指定节点, 然后返回节点所保存的元素。LLEN 调用 ziplistLen
函数返回压缩列表的长度。调用 listLength
函数返回双端链表的长度。LINSERT 插入新节点到压缩列表的表头或者表尾时, 使用 ziplistPush
函数; 插入新节点到压缩列表的其他位置时, 使用ziplistInsert
函数。调用 listInsertNode
函数, 将新节点插入到双端链表的指定位置。LREM 遍历压缩列表节点, 并调用 ziplistDelete
函数删除包含了给定元素的节点。遍历双端链表节点, 并调用 listDelNode
函数删除包含了给定元素的节点。LTRIM 调用 ziplistDeleteRange
函数, 删除压缩列表中所有不在指定索引范围内的节点。遍历双端链表节点, 并调用 listDelNode
函数删除链表中所有不在指定索引范围内的节点。LSET 调用 ziplistDelete
函数, 先删除压缩列表指定索引上的现有节点, 然后调用ziplistInsert
函数, 将一个包含给定元素的新节点插入到相同索引上面。调用 listIndex
函数, 定位到双端链表指定索引上的节点, 然后通过赋值操作更新节点的值。
-
-
哈希对象
-
哈希对象的编码可以是
ziplist
或者hashtable
。ziplist
编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:- 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
hashtable
编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:- 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
- 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。
-
编码转换
当哈希对象可以同时满足以下两个条件时, 哈希对象使用
ziplist
编码,不能满足这两个条件的哈希对象需要使用hashtable
编码:- 哈希对象保存的所有键值对的键和值的字符串长度都小于
64
字节; - 哈希对象保存的键值对数量小于
512
个;
-
哈希对象因为键值对的键长度太大而引起编码转换的情况:
# 哈希对象只包含一个键和值都不超过 64 个字节的键值对 redis> HSET book name "Mastering C++ in 21 days" (integer) 1 redis> OBJECT ENCODING book "ziplist" # 向哈希对象添加一个新的键值对,键的长度为 66 字节 redis> HSET book long_long_long_long_long_long_long_long_long_long_long_description "content" (integer) 1 # 编码已改变 redis> OBJECT ENCODING book "hashtable"
-
值的长度太大也引起编码转换
# 哈希对象只包含一个键和值都不超过 64 个字节的键值对 redis> HSET blah greeting "hello world" (integer) 1 redis> OBJECT ENCODING blah "ziplist" # 向哈希对象添加一个新的键值对,值的长度为 68 字节 redis> HSET blah story "many string ... many string ... many string ... many string ... many" (integer) 1 # 编码已改变 redis> OBJECT ENCODING blah "hashtable"
-
哈希对象因为包含的键值对数量过多而引起编码转换的情况
# 创建一个包含 512 个键值对的哈希对象 redis> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i) end" 1 "numbers" (nil) redis> HLEN numbers (integer) 512 redis> OBJECT ENCODING numbers "ziplist" # 再向哈希对象添加一个新的键值对,使得键值对的数量变成 513 个 redis> HMSET numbers "key" "value" OK redis> HLEN numbers (integer) 513 # 编码改变 redis> OBJECT ENCODING numbers "hashtable"
- 哈希对象保存的所有键值对的键和值的字符串长度都小于
-
哈希命令的实现
命令 ziplist
编码实现方法hashtable
编码的实现方法HSET 首先调用 ziplistPush
函数, 将键推入到压缩列表的表尾, 然后再次调用ziplistPush
函数, 将值推入到压缩列表的表尾。调用 dictAdd
函数, 将新节点添加到字典里面。HGET 首先调用 ziplistFind
函数, 在压缩列表中查找指定键所对应的节点, 然后调用ziplistNext
函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。调用 dictFind
函数, 在字典中查找给定键, 然后调用dictGetVal
函数, 返回该键所对应的值。HEXISTS 调用 ziplistFind
函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。调用 dictFind
函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。HDEL 调用 ziplistFind
函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。调用 dictDelete
函数, 将指定键所对应的键值对从字典中删除掉。HLEN 调用 ziplistLen
函数, 取得压缩列表包含节点的总数量, 将这个数量除以2
, 得出的结果就是压缩列表保存的键值对的数量。调用 dictSize
函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。HGETALL 遍历整个压缩列表, 用 ziplistGet
函数返回所有键和值(都是节点)。遍历整个字典, 用 dictGetKey
函数返回字典的键, 用dictGetVal
函数返回字典的值。
-
-
集合对象
-
集合对象的编码可以是
intset
或者hashtable
。intset
编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面。
hashtable
编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为NULL
。
-
编码的转换
- 当集合对象可以同时满足以下两个条件时, 对象使用
intset
编码,不能满足这两个条件的集合对象需要使用hashtable
编码:- 集合对象保存的所有元素都是整数值;
- 集合对象保存的元素数量不超过
512
个;
- 当集合对象可以同时满足以下两个条件时, 对象使用
-
集合命令的实现
命令 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)。压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。
skiplist
编码的有序集合对象使用zset
结构作为底层实现, 一个zset
结构同时包含一个字典和一个跳跃表:
typedef struct zset { zskiplist *zsl; dict *dict; } zset;
-
编码的转换
-
当有序集合对象可以同时满足以下两个条件时, 对象使用
ziplist
编码,不能满足以上两个条件的有序集合对象将使用skiplist
编码:- 有序集合保存的元素数量小于
128
个; - 有序集合保存的所有元素成员的长度都小于
64
字节;
- 有序集合对象因为包含了过多元素而引发编码转换的情况:
# 对象包含了 128 个元素 redis> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers (nil) redis> ZCARD numbers (integer) 128 redis> OBJECT ENCODING numbers "ziplist" # 再添加一个新元素 redis> ZADD numbers 3.14 pi (integer) 1 # 对象包含的元素数量变为 129 个 redis> ZCARD numbers (integer) 129 # 编码已改变 redis> OBJECT ENCODING numbers "skiplist"
- 有序集合对象因为元素的成员过长而引发编码转换的情况:
# 向有序集合添加一个成员只有三字节长的元素 redis> ZADD blah 1.0 www (integer) 1 redis> OBJECT ENCODING blah "ziplist" # 向有序集合添加一个成员为 66 字节长的元素 redis> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo (integer) 1 # 编码已改变 redis> OBJECT ENCODING blah "skiplist"
- 有序集合保存的元素数量小于
-
-
有序集合命令的实现方法
命令 ziplist
编码的实现方法zset
编码的实现方法ZADD 调用 ziplistInsert
函数, 将成员和分值作为两个节点分别插入到压缩列表。先调用 zslInsert
函数, 将新元素添加到跳跃表, 然后调用dictAdd
函数, 将新元素关联到字典。ZCARD 调用 ziplistLen
函数, 获得压缩列表包含节点的数量, 将这个数量除以2
得出集合元素的数量。访问跳跃表数据结构的 length
属性, 直接返回集合元素的数量。ZCOUNT 遍历压缩列表, 统计分值在给定范围内的节点的数量。 遍历跳跃表, 统计分值在给定范围内的节点的数量。 ZRANGE 从表头向表尾遍历压缩列表, 返回给定索引范围内的所有元素。 从表头向表尾遍历跳跃表, 返回给定索引范围内的所有元素。 ZREVRANGE 从表尾向表头遍历压缩列表, 返回给定索引范围内的所有元素。 从表尾向表头遍历跳跃表, 返回给定索引范围内的所有元素。 ZRANK 从表头向表尾遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 从表头向表尾遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 ZREVRANK 从表尾向表头遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 从表尾向表头遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 ZREM 遍历压缩列表, 删除所有包含给定成员的节点, 以及被删除成员节点旁边的分值节点。 遍历跳跃表, 删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。 ZSCORE 遍历压缩列表, 查找包含了给定成员的节点, 然后取出成员节点旁边的分值节点保存的元素分值。 直接从字典中取出给定成员的分值。
-
内存回收与对象共享
-
Redis 在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。
typedef struct redisObject { // ... // 引用计数 int refcount; // ... } robj;
-
对象的引用计数信息会随着对象的使用状态而不断变化:
- 在创建一个新对象时, 引用计数的值会被初始化为
1
; - 当对象被一个新程序使用时, 它的引用计数值会被增一;
- 当对象不再被一个程序使用时, 它的引用计数值会被减一;
- 当对象的引用计数值变为
0
时, 对象所占用的内存会被释放。
- 在创建一个新对象时, 引用计数的值会被初始化为
-
在 Redis 中, 让多个键共享同一个值对象需要执行以下两个步骤:
- 将数据库键的值指针指向一个现有的值对象;
- 将被共享的值对象的引用计数增一。
对象的空转时长
-
OBJECT IDLETIME 命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的
lru
时间计算得出的:redis> SET msg "hello world" OK # 等待一小段时间 redis> OBJECT IDLETIME msg (integer) 20 # 等待一阵子 redis> OBJECT IDLETIME msg (integer) 180 # 访问 msg 键的值 redis> GET msg "hello world" # 键处于活跃状态,空转时长为 0 redis> OBJECT IDLETIME msg (integer) 0