上一章我们讲了 Redis 的底层数据结构, 不了解的人可能会有疑问: 这个和平时用的五大对象有啥关系呢? 这一章我们就主要解释他们所建立的联系.
看这个文章之前, 如果对 ziplist,skiplist,intset 等数据结构不熟悉的话, 建议先回顾一下上一章节: 面试官: 你看过 Redis 数据结构底层实现吗?
0. 五类对象分别是什么
五类对象就是我们常用的 string,list,set,zset,hash
1. 为什么要有对象
我们平时主要是通过操作对象的 API 来操作 Redis, 而不是通过它的调用它底层数据结构来完成(外观模式). 但我们还需要了解其底层, 只有这样才能写最优化高效的代码.
跟 java 一样, 对象使开发更方便简洁, 降低开发门槛. 开发者不需要了解其复杂的底层 API, 直接调用高层接口即可实现开发.
Redis 根据对象类型来判断命令是否违法, 如果你 set key value1 value2 就报错.
对象下可以包含多种数据结构, 使数据存储更加多态化.(下面主讲)
Reids 基于对象做了垃圾回收(引用计数法).
对象带有更丰富的属性, 来帮助 Redis 实现更高级的功能.(比如对象的闲置时间).
2. Redis 对象 (RedisObject) 源码分析
- typedef struct redisObject {
- // 类型
- unsigned type:4;
- // 编码
- unsigned encoding:4;
- // 指向底层实现数据结构的指针
- void *ptr;
- // ...
- } robj;
type 字段
记录对象类型.
我们平时用的命令 type <key>, 其实就是返回这个字段的属性.
- 127.0.0.1:6379> set hello world
- OK
- 127.0.0.1:6379> type hello
- string
- 127.0.0.1:6379> rpush list 1 2 3
- (integer) 3
- 127.0.0.1:6379> type list
- list
- ...
那 type 有多少中类型呢? 看下面这个表:
encoding 字段
记录对象使用的编码(数据结构),Reids 中称数据结构为 encoding.
我们可以这样查看我们 Redis 对象中的 encoding:
- 127.0.0.1:6379> object encoding hello
- "embstr"
- 127.0.0.1:6379> object encoding list
- "quicklist"
- ...
既然它是标明该 redisObject 是使用的什么数据结构, 那肯定也有个对应的表:
我们可以看到, Redis 对对象的底层 encoding 分的很细, String 类型就有三个, 其它四个对象都分别有两种不同的底层数据结构的实现. 他们有一规律, 就是用 ziplist,intset,embstr 来实现少量的数据, 数据量一旦庞大, 就会升级到 skiplist,raw,linkedlist,ht 来实现, 后面我会仔细讲解.
3. 分别分析各个对象的底层编码实现(数据结构)
3.1 字符串(string)
字符串编码有三个: int,raw,embstr.
3.1.1 int
当 string 对象的值全部是数字, 就会使用 int 编码.
- 127.0.0.1:6379> set number 123455
- OK
- 127.0.0.1:6379> object encoding number
- "int"
- 3.1.2 embstr
字符串或浮点数长度小于等于 39 字节, 就会使用 embstr 编码方式来存储, embstr 存储内存一般很小, 所以 Redis 一次性分配且内存连续(效率高).
- 127.0.0.1:6379> set shortStr "suwe suwe suwe"
- OK
- 127.0.0.1:6379> object encoding shortStr
- "embstr"
- 3.1.2 raw
当一个字符串或浮点数长度大于 39 字节, 就使用 SDS 来保存, 编码为 raw, 由于不确定值的字节大小, 所以键和值各分配各的, 所以就分配两次内存(回收也是两次), 同理它一定不是内存连续的.
- 127.0.0.1:6379> set longStr "hello everyone, we dont need to sleep around to go aheard! do you think?"
- OK
- 127.0.0.1:6379> object encoding longStr
- "raw"
3.1.3 编码转换
前面说过, Redis 会自动对编码进行转换来适应和优化数据的存储.
int->raw
条件: 数字对象进行 append 字母, 就会发生转换.
- 127.0.0.1:6379> object encoding number
- "int"
- 127.0.0.1:6379> append number "is a lucky number"
- (integer) 24
- 127.0.0.1:6379> object encoding number
- "raw"
- embstr->raw
条件: 对 embstr 进行修改, Redis 会先将其转换成 raw, 然后才进行修改. 所以 embstr 实际上是只读性质的.
- 127.0.0.1:6379> object encoding shortStr
- "embstr"
- 127.0.0.1:6379> append shortStr "(hhh"
- (integer) 18
- 127.0.0.1:6379> object encoding shortStr
- "raw"
3.2 列表(list)
列表对象编码可以是: ziplist 或 linkedlist.
ziplist 压缩列表不知道大家还记得不, 就是 zlbytes zltail zllen entry1 entry2 ..end 结构, entry 节点里有 pre-length,encoding,content 属性, 忘记的可以返回去看下.
linkedlist, 类似双向链表, 也是上一章的知识.
3.2.1 编码转换
ziplist->linkedlist
条件: 列表对象的所有字符串元素的长度大于等于 64 字节 & 列表元素数大于等于 512. 反之, 小于 64 和小于 512 会使用 ziplist 而不是用 linkedlist.
这个阈值是可以修改的, 修改选项: list-max-ziplist-value 和 list-max-ziplist-entriess
3.3 哈希(hash)
哈希对象的编码有: ziplist 和 hashtable
3.3.1 编码转换
ziplist->hashtable
条件: 哈希对象所有键和值字符串长度大于等于 64 字节 & 键值对数量大于等于 512
这个阈值也是可以修改的, 修改选项: hash-max-ziplist-value 和 hash-max-ziplist-entriess
3.4. 集合(set)
集合对象的编码有: intset 和 hashtable
3.4.1 intset
集合对象所有元素都是整数
集合对象元素数不超过 512 个
3.4.2 编码转换
intset->hashtable
条件: 元素不都是整数 & 元素数大于等于 512
3.5. 有序集合(zset)
有序集合用到的编码: ziplist 和 skiplist
大家可能很好奇阿, ziplist 的 entry 中只有属性 content 可以存放数据, 集合也是 key-value 形式, 那怎么存储呢?
第一个节点保存 key, 第二个节点保存 value 以此类推...
3.5.1 为什么要用这两个编码
如果只用 ziplist 来实现, 无法做到元素的排序, 不支持范围查找, 能做到元素的快速查找.
如果只用 skiplist 来实现, 无法做到快速查找, 但能做到元素排序, 范围操作.
3.5.2 编码转换
ziplist->skiplist
条件: 有序集合元素数>= 128 & 含有元素的长度>= 64
这个阈值也是可以修改的, 修改选项: zset-max-ziplist-value 和 zset-max-ziplist-entriess
4. 垃圾回收
为什么要说内存回收呢, 因为 redisObject 有一个字段:
- typedef struct redisObject {
- // ...
- // 引用计数
- int refcount;
- // ...
- } robj;
Redis 的垃圾回收采用引用计数法(和 jvm 一样), 底层采用一个变量对对象的使用行为进行计数.
初始化为 1
对象被引用,+1
对象引用消除,-1
计数器 ==0, 回收对象
5. 对象共享
5.1 对象共享的体现
Redis 中, 值是整数值且相等的两个对象, Redis 会将该对象进行共享, 且引用计数 + 1
Redis 启动会自动生成 0-9999 的整数值放到内存中来共享.
5.2 为什么要对象共享
节约内存
5.3 为什么不对字符串进行共享
成本太高.
验证整数相等只需要 O(1)的时间复杂度, 而验证字符串要 O(n).
6. 对象的空闲时长
最后, redisObject 还有一个字段, 记录了对象最后一次被访问的时间:
- typedef struct redisObject {
- // ...
- unsigned lru:22;
- // ...
- } robj;
因为这个字段记录对象最后一次被访问的时间, 所以它可以用来查看该对象多久未使用, 即: 用当前时间 - lru
- 127.0.0.1:6379> object idletime hello
- (integer) 5110
它还关系到 Redis 的热点数据实现, 如果我们选择 lr 算法, 当内存超出阈值后会对空闲时长较高的对象进行释放, 回收内存.
参考文献:
《Redis 设计与实现》黄健宏著
http://redisbook.com/index.html
来源: https://www.cnblogs.com/javazhiyin/p/11095363.html