除了用于实现引用计数内存回收机制之外, 对象的引用计数属性还带有对象共享的作用。
举个例子, 假设键 A 创建了一个包含整数值 `100` 的字符串对象作为值对象, 如图 8-20 所示。
![](https://box.kancloud.cn/2015-09-13_55f5219baa7cc.png)
如果这时键 B 也要创建一个同样保存了整数值 `100` 的字符串对象作为值对象, 那么服务器有以下两种做法:
1. 为键 B 新创建一个包含整数值 `100` 的字符串对象;
2. 让键 A 和键 B 共享同一个字符串对象;
以上两种方法很明显是第二种方法更节约内存。
在 Redis 中, 让多个键共享同一个值对象需要执行以下两个步骤:
1. 将数据库键的值指针指向一个现有的值对象;
2. 将被共享的值对象的引用计数增一。
举个例子, 图 8-21 就展示了包含整数值 `100` 的字符串对象同时被键 A 和键 B 共享之后的样子, 可以看到, 除了对象的引用计数从之前的 `1` 变成了 `2` 之外, 其他属性都没有变化。
![](https://box.kancloud.cn/2015-09-13_55f5219db9ef5.png)
共享对象机制对于节约内存非常有帮助, 数据库中保存的相同值对象越多, 对象共享机制就能节约越多的内存。
比如说, 假设数据库中保存了整数值 `100` 的键不只有键 A 和键 B 两个, 而是有一百个, 那么服务器只需要用一个字符串对象的内存就可以保存原本需要使用一百个字符串对象的内存才能保存的数据。
目前来说, Redis 会在初始化服务器时, 创建一万个字符串对象, 这些对象包含了从 `0` 到 `9999` 的所有整数值, 当服务器需要用到值为 `0`到 `9999` 的字符串对象时, 服务器就会使用这些共享对象, 而不是新创建对象。
注意
创建共享字符串对象的数量可以通过修改 `redis.h/REDIS_SHARED_INTEGERS` 常量来修改。
举个例子, 如果我们创建一个值为 `100` 的键 `A` , 并使用 OBJECT REFCOUNT 命令查看键 `A` 的值对象的引用计数, 我们会发现值对象的引用计数为 `2` :
~~~
redis> SET A 100
OK
redis> OBJECT REFCOUNT A
(integer) 2
~~~
引用这个值对象的两个程序分别是持有这个值对象的服务器程序, 以及共享这个值对象的键 `A` , 如图 8-22 所示。
![](https://box.kancloud.cn/2015-09-13_55f5219ef35e0.png)
如果这时我们再创建一个值为 `100` 的键 `B` , 那么键 `B` 也会指向包含整数值 `100` 的共享对象, 使得共享对象的引用计数值变为 `3` :
~~~
redis> SET B 100
OK
redis> OBJECT REFCOUNT A
(integer) 3
redis> OBJECT REFCOUNT B
(integer) 3
~~~
图 8-23 展示了共享值对象的三个程序。
![](https://box.kancloud.cn/2015-09-13_55f521a0ec055.png)
另外, 这些共享对象不单单只有字符串键可以使用, 那些在数据结构中嵌套了字符串对象的对象(`linkedlist` 编码的列表对象、 `hashtable` 编码的哈希对象、 `hashtable` 编码的集合对象、以及 `zset` 编码的有序集合对象)都可以使用这些共享对象。
为什么 Redis 不共享包含字符串的对象?
当服务器考虑将一个共享对象设置为键的值对象时, 程序需要先检查给定的共享对象和键想创建的目标对象是否完全相同, 只有在共享对象和目标对象完全相同的情况下, 程序才会将共享对象用作键的值对象, 而一个共享对象保存的值越复杂, 验证共享对象和目标对象是否相同所需的复杂度就会越高, 消耗的 CPU 时间也会越多:
* 如果共享对象是保存整数值的字符串对象, 那么验证操作的复杂度为 ![O(1)](https://box.kancloud.cn/2015-09-13_55f521a2227eb.png) ;
* 如果共享对象是保存字符串值的字符串对象, 那么验证操作的复杂度为 ![O(N)](https://box.kancloud.cn/2015-09-13_55f521a314bb1.png) ;
* 如果共享对象是包含了多个值(或者对象的)对象, 比如列表对象或者哈希对象, 那么验证操作的复杂度将会是 ![O(N^2)](https://box.kancloud.cn/2015-09-13_55f521a3ee960.png) 。
因此, 尽管共享更复杂的对象可以节约更多的内存, 但受到 CPU 时间的限制, Redis 只对包含整数值的字符串对象进行共享。
- 介绍
- 前言
- 致谢
- 简介
- 第一部分:数据结构与对象
- 简单动态字符串
- SDS 的定义
- SDS 与 C 字符串的区别
- SDS API
- 重点回顾
- 参考资料
- 链表
- 链表和链表节点的实现
- 链表和链表节点的 API
- 重点回顾
- 字典
- 字典的实现
- 哈希算法
- 解决键冲突
- rehash
- 渐进式 rehash
- 字典 API
- 重点回顾
- 跳跃表
- 跳跃表的实现
- 跳跃表 API
- 重点回顾
- 整数集合
- 整数集合的实现
- 升级
- 升级的好处
- 降级
- 整数集合 API
- 重点回顾
- 压缩列表
- 压缩列表的构成
- 压缩列表节点的构成
- 连锁更新
- 压缩列表 API
- 重点回顾
- 对象
- 对象的类型与编码
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
- 类型检查与命令多态
- 内存回收
- 对象共享
- 对象的空转时长
- 重点回顾
- 第二部分:单机数据库的实现
- 数据库
- 数据库键空间
- 重点回顾
- RDB 持久化
- RDB 文件结构
- 重点回顾
- AOF 持久化
- AOF 持久化的实现
- 重点回顾
- 事件
- 文件事件
- 重点回顾
- 参考资料
- 客户端
- 客户端属性
- 重点回顾
- 服务器
- 命令请求的执行过程
- 重点回顾
- 第三部分:多机数据库的实现
- 复制
- 旧版复制功能的实现
- 重点回顾
- Sentinel
- 启动并初始化 Sentinel
- 重点回顾
- 参考资料
- 集群
- 节点
- 重点回顾
- 第四部分:独立功能的实现
- 发布与订阅
- 频道的订阅与退订
- 重点回顾
- 参考资料
- 事务
- 事务的实现
- 重点回顾
- Lua 脚本
- 创建并修改 Lua 环境
- 重点回顾
- 排序
- SORT <key> 命令的实现
- 重点回顾
- 二进制位数组
- GETBIT 命令的实现
- 重点回顾
- 慢查询日志
- 慢查询记录的保存
- 慢查询日志的阅览和删除
- 添加新日志
- 重点回顾
- 监视器
- 成为监视器
- 向监视器发送命令信息
- 重点回顾
- 源码、相关资源和勘误