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;
// ...
} 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 列出的常量的其中一个。
* * *
表 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 中的五种不同类型的对象, 说明这些对象底层所使用的编码方式, 列出对象从一种编码转换成另一种编码所需的条件, 以及同一个命令在多种不同编码上的实现方法。
- 介绍
- 前言
- 致谢
- 简介
- 第一部分:数据结构与对象
- 简单动态字符串
- SDS 的定义
- SDS 与 C 字符串的区别
- SDS API
- 重点回顾
- 参考资料
- 链表
- 链表和链表节点的实现
- 链表和链表节点的 API
- 重点回顾
- 字典
- 字典的实现
- 哈希算法
- 解决键冲突
- rehash
- 渐进式 rehash
- 字典 API
- 重点回顾
- 跳跃表
- 跳跃表的实现
- 跳跃表 API
- 重点回顾
- 整数集合
- 整数集合的实现
- 升级
- 升级的好处
- 降级
- 整数集合 API
- 重点回顾
- 压缩列表
- 压缩列表的构成
- 压缩列表节点的构成
- 连锁更新
- 压缩列表 API
- 重点回顾
- 对象
- 对象的类型与编码
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
- 类型检查与命令多态
- 内存回收
- 对象共享
- 对象的空转时长
- 重点回顾
- 第二部分:单机数据库的实现
- 数据库
- 数据库键空间
- 重点回顾
- RDB 持久化
- RDB 文件结构
- 重点回顾
- AOF 持久化
- AOF 持久化的实现
- 重点回顾
- 事件
- 文件事件
- 重点回顾
- 参考资料
- 客户端
- 客户端属性
- 重点回顾
- 服务器
- 命令请求的执行过程
- 重点回顾
- 第三部分:多机数据库的实现
- 复制
- 旧版复制功能的实现
- 重点回顾
- Sentinel
- 启动并初始化 Sentinel
- 重点回顾
- 参考资料
- 集群
- 节点
- 重点回顾
- 第四部分:独立功能的实现
- 发布与订阅
- 频道的订阅与退订
- 重点回顾
- 参考资料
- 事务
- 事务的实现
- 重点回顾
- Lua 脚本
- 创建并修改 Lua 环境
- 重点回顾
- 排序
- SORT <key> 命令的实现
- 重点回顾
- 二进制位数组
- GETBIT 命令的实现
- 重点回顾
- 慢查询日志
- 慢查询记录的保存
- 慢查询日志的阅览和删除
- 添加新日志
- 重点回顾
- 监视器
- 成为监视器
- 向监视器发送命令信息
- 重点回顾
- 源码、相关资源和勘误