Redis读书笔记(二)Redis的对象
Redis的对象
Redis的主要数据结构:简单动态字符串、双端列表、字典、整数结合、跳跃表、压缩列表等。
Redis没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构构建了一个对象系统,这个系统包括字符串对象,列表对象,哈希对象,集合对象和有序集合对象这五种类型的对象,每种对象都用到了至少一种前面所介绍的数据结构。
1、对象的结构
Redis使用对象来表示数据库的键和值。每个对象都是一个redisObject结构,是一个按照位段存储的结构,节约内存:
typedef struct redisObject{ //类型 unsigned type :4; //编码 unsigned encoding:4; //指向底层实现数据结构的指针 void *ptr; ... } robj;
type是类型常量,记录对象的类型:
encoding记录对象使用的编码,即对象底层实现使用的具体数据结构:
使用object encoding命令可以查看键的值对象的编码,每个对象都至少使用两种不同编码常量:
对象的ptr指针指向对象的底层实现数据结构,而这些数据结构由对象的encoding属性决定。
2、字符串对象
字符串对象可以是int,raw或embstr。
- 如果字符串对象保存的是整数值,且这个整数值可用long类型表示,底层就会以
REDIS_ENCODING_INT
编码来实现。 - 如果字符串对象保存的是字符串值,且这个字符串值的长度<=39字节,底层编码就是
REDIS_ENCODING_EMBSTR
,使用embstr编码的方式保存字符串。 - 如果字符串对象保存的是字符串值,且这个字符串值的长度>39字节,字符串值将使用一个SDS保存,底层编码为
REDIS_ENCODING_RAW
。
embstr编码
embstr编码是专门用于保存短字符串的一种优化编码方式,与raw的效果相同,都使用redisObject和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数分别创建redisObject和sdshdr结构。而embstr编码则通过调用一次内存分配函数来分配一块连续空间,空间依次包括redisObject和sdshdr俩结构。
使用embstr编码保存短字符串的优点:
- 内存分配次数从raw编码的2次降为1次。
- 释放embstr编码的字符串对象只需调用1次内存释放函数,而释放raw编码的字符串对象需要两次。
- embstr编码的字符串对象的所有数据都保存在一块连续的内存中,能更好地利用缓存带来的优势.
注:浮点数的存储,在Redis底层也会以字符串的形式保存。在有需要时,程序会将字符串对象中的字符串值转为浮点数值执行运算操作,然后再将结果转为字符串值保存。
编码的转换
int->raw
:执行一些命令,使得int编码的字符串对象保存的不再是整数值,而是字符串值时。比如整数追加字符串。
embstr->raw
:Redis没有为embstr编写修改程序,所以是只读的,当embstr编码的字符串修改后,就变成raw编码的字符串对象。
3、列表对象
列表对象的编码是ziplist或linkedlist。
当列表可以同时满足以下两个条件时,列表对象使用ziplist编码,否则使用linkedlist编码。:
- 列表对象保存的所有字符串元素的长度都<64字节
- 列表对象保存的元素数量<512个
使用ziplist编码,执行RPUSH elements 1 "three" 5
,后的数据结构:
使用linkedlist编码,执行RPUSH elements 1 "three" 5
,后的数据结构:
注:SDS对象都以StringObject代替。
4、哈希对象
哈希对象的编码可以是ziplist或hashtable。
ziplist编码的哈希对象使用压缩列表作为底层实现:每当有新的键值对插入哈希对象时,Redis会先将保存键的压缩列表节点推入表尾,再将保存值的压缩列表节点推入表尾。
hashtable编码的哈希对象使用字典作为底层实现:字典的每个键都是一个字符串对象,保存键;字典的每个值都是字符串对象,保存值。
当哈希对象可以同时满足下两个条件时,使用ziplist编码,否则使用hashtable编码:
- 哈希对象保存的所有键值对的值和键都<64字节
- 哈希对象保存的键值对数量<512个
5、集合对象
集合对象的编码可以是intset或hashtable。
intset编码集合对象:
如果以hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合对象,而字典的值则全都是null。
当集合对象同时满足以下两个条件时,使用intset编码,否则使用hashtable编码:
- 集合对象保存的所有元素都是整数值
- 集合对象保存的元素数量<=512个
6、有序集合对象
有序集合的编码可以是ziplist或skiplist。
ziplist编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,而第二个元素则保存元素的分值。
skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:
typedef struct zset{ zskiplist *zsl; dict *dict; } zset;
zset结构中的dict跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,score属性则保存了元素的分值。
zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存元素,字典值保存分值。通过字典,可以用O(1)复杂度查找给定成员的分值。有序集合元素都是字符串对象,分值都是double类型浮点数。zset的跳跃表和字典通过指针来共享相同元素的成员和分值,不会浪费额外内存。
当有序集合对象同时满足以下两条件时,对象使用ziplist编码,否则使用skiplist编码:
- 有序集合保存的元素数量<128个
- 有序集合保存的所有元素成员的长度都<64字节
为什么有序集合需要同时使用跳跃表和字典来实现?
==
7、类型检查与命令多态
Redis中用于操作键的命令可分为两种类型。
一种是可对任何类型的键执行,如del,expire,rename等。
另一种命令只能对特定类型的键执行,如set,get,hdel,hset,rpush等。如果对特定类型使用其他类型的命令,那么就会报错。
类型检查的实现
为了确保只有指定类型的键可以执行某些特定命令,在执行前,Redis会先通过RedisObject的type属性检查输入键的类型是否正确,再决定是否执行给定的命令。
多态命令的实现
Redis除了根据值对象判断键是否能够执行制定命令外,还会根据值对象的编码方式,选择正确的命令实现代码来执行。比如基于编码的多态,列表对象的编码可能是ziplist或linkedlist,所以需要多态命令执行对应编码的API。
基于类型的多态——一个命令可以同时处理多种不同类型的键。
基于编码的多态——一个命令可以同时处理多种不同编码。
8、内存回收
由于C语言没有内存回收机制,所以Redis在对象系统中构建了一个引用计数器技术实现内存回收机制。每个对象的引用计数器信息由redisObject的refcount来记录。当对象的引用计数值为0时,所占用的内存会被释放。
typedef struct redisObject{ //... // 引用计数 int refcount; //... } robj;
9、对象共享
引用计数器还有共享对象的作用。如果两个不同键的值都一样(必须是整数值的字符串对象),则将数据库键的值指针指向一个现有的值对象,然后将被共享对象的引用计数加一。如果不是整数值的对象,则需要耗费大量的时间验证共享对象和目标对象是否相同,复杂度较高,消耗CPU时间,所以Redis不会共享包含字符串的对象。
Redis在初始化服务时,会创建很多字符串对象,包含0~9999的整数(和Integer的常量池有点像),当需要时,就会使用这些共享对象,而不是新创建对象。
10、对象的空转时长
redisObject还包含了lru属性,记录对象最后一个被命令程序访问的时间。object idletime
命令可打印键的空转时长,就是当前时间减去lru时间计算得到的。
typedef struct redisObject{ //... // 引用计数 unsigned lru:22; //... } robj;