Redis设计与实现7 对象的类型与编码

2018-06-05  本文已影响10人  one_zheng

 Redis 使用对象来表示数据库中的键和值, 每次当我们在 Redis 的数据库中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。

举个例子, 以下 SET 命令在数据库中创建了一个新的键值对, 其中键值对的键是一个包含了字符串值 "msg" 的对象, 而键值对的值则是一个包含了字符串值 "hello world" 的对象:

redis> SET msg "hello world"
OK

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

typedef struct redisObject {

    // 类型
    unsigned type:4;

    // 编码
    unsigned encoding:4;

    // 指向底层实现数据结构的指针
    void *ptr;

    // 引用计数
    int refcount;

    // 对象最后一次被命令程序访问的时间

    unsigned lru:22;
    // ...

} robj;

类型

对象的 type 属性记录了对象的类型, 这个属性的值可以是表 8-1 列出的常量的其中一个。

表 8-1 对象的类型

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

对于 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

表 8-2 列出了 TYPE 命令在面对不同类型的值对象时所产生的输出。

表 8-2 不同类型值对象的 TYPE 命令输出

对象 对象 type 属性的值 TYPE 命令的输出
字符串对象 REDIS_STRING "string"
列表对象 REDIS_LIST "list"
哈希对象 REDIS_HASH "hash"
集合对象 REDIS_SET "set"
有序集合对象 REDIS_ZSET "zset"

编码和底层实现

对象的 ptr 指针指向对象的底层实现数据结构, 而这些数据结构由对象的 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 跳跃表和字典

表 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 跳跃表和字典

每种类型的对象都至少使用了两种不同的编码, 表 8-4 列出了每种类型的对象可以使用的编码。

表 8-4 不同类型和编码的对象

类型 编码 对象
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 命令可以查看一个数据库键的值对象的编码:

redis> SET msg "hello wrold"
OK

redis> OBJECT ENCODING msg
"embstr"

redis> SET story "long long long long long long ago ..."
OK

redis> OBJECT ENCODING story
"raw"

redis> SADD numbers 1 3 5
(integer) 3

redis> OBJECT ENCODING numbers
"intset"

redis> SADD numbers "seven"
(integer) 1

redis> OBJECT ENCODING numbers
"hashtable"

表 8-5 列出了不同编码的对象所对应的 OBJECT ENCODING 命令输出。

表 8-5 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"

通过 encoding 属性来设定对象所使用的编码, 而不是为特定类型的对象关联一种固定的编码, 极大地提升了 Redis 的灵活性和效率, 因为 Redis 可以根据不同的使用场景来为一个对象设置不同的编码, 从而优化对象在某一场景下的效率。

举个例子, 在列表对象包含的元素比较少时, Redis 使用压缩列表作为列表对象的底层实现:

在接下来的内容中, 我们将分别介绍 Redis 中的五种不同类型的对象, 说明这些对象底层所使用的编码方式, 列出对象从一种编码转换成另一种编码所需的条件, 以及同一个命令在多种不同编码上的实现方法。

类型检查与命令多态

Redis 中用于操作键的命令基本上可以分为两种类型。

其中一种命令可以对任何类型的键执行, 比如说 DEL 命令、 EXPIRE 命令、 RENAME 命令、 TYPE 命令、 OBJECT 命令, 等等。

举个例子, 以下代码就展示了使用 DEL 命令来删除三种不同类型的键:

# 字符串键
redis> SET msg "hello"
OK

# 列表键
redis> RPUSH numbers 1 2 3
(integer) 3

# 集合键
redis> SADD fruits apple banana cherry
(integer) 3

redis> DEL msg
(integer) 1

redis> DEL numbers
(integer) 1

redis> DEL fruits
(integer) 1

而另一种命令只能对特定类型的键执行, 比如说:

SET 、 GET 、 APPEND 、 STRLEN 等命令只能对字符串键执行;
HDEL 、 HSET 、 HGET 、 HLEN 等命令只能对哈希键执行;
RPUSH 、 LPOP 、 LINSERT 、 LLEN 等命令只能对列表键执行;
SADD 、 SPOP 、 SINTER 、 SCARD 等命令只能对集合键执行;
ZADD 、 ZCARD 、 ZRANK 、 ZSCORE 等命令只能对有序集合键执行;
诸如此类。

举个例子, 我们可以用 SET 命令创建一个字符串键, 然后用 GET 命令和 APPEND 命令操作这个键, 但如果我们试图对这个字符串键执行只有列表键才能执行的 LLEN 命令, 那么 Redis 将向我们返回一个类型错误:

redis> SET msg "hello world"
OK

redis> GET msg
"hello world"

redis> APPEND msg " again!"
(integer) 18

redis> GET msg
"hello world again!"

redis> LLEN msg
(error) WRONGTYPE Operation against a key holding the wrong kind of value

内存回收

因为 C 语言并不具备自动的内存回收功能, 所以 Redis 在自己的对象系统中构建了一个引用计数(reference counting)技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。

每个对象的引用计数信息由 redisObject 结构的 refcount 属性记录:

typedef struct redisObject {

    // ...

    // 引用计数
    int refcount;

    // ...

} robj;

对象的引用计数信息会随着对象的使用状态而不断变化:

在创建一个新对象时, 引用计数的值会被初始化为 1 ;
当对象被一个新程序使用时, 它的引用计数值会被增一;
当对象不再被一个程序使用时, 它的引用计数值会被减一;
当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。
表 8-12 列出了修改对象引用计数的 API , 这些 API 分别用于增加、减少、重置对象的引用计数。

表 8-12 修改对象引用计数的 API

函数 作用
incrRefCount 将对象的引用计数值增一。
decrRefCount 将对象的引用计数值减一, 当对象的引用计数值等于 0 时, 释放对象。
resetRefCount 将对象的引用计数值设置为 0 , 但并不释放对象, 这个函数通常在需要重新设置对象的引用计数值时使用。

对象的整个生命周期可以划分为创建对象、操作对象、释放对象三个阶段。

作为例子, 以下代码展示了一个字符串对象从创建到释放的整个过程:

// 创建一个字符串对象 s ,对象的引用计数为 1
robj *s = createStringObject(...)

// 对象 s 执行各种操作 ...

// 将对象 s 的引用计数减一,使得对象的引用计数变为 0
// 导致对象 s 被释放
decrRefCount(s)

其他不同类型的对象也会经历类似的过程。

重点回顾

上一篇下一篇

猜你喜欢

热点阅读