对象
对象这一章节可以理解为前面七个章节的总结篇,仿佛是我在前七章中获取了七个拼图,而这一章中,我们将使用七章拼图来进行实际的拼接操作.
Redis 并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创造了一个对象系统,每种对象都用到了至少一种我们前面所介绍的数据结构.
8.1 对象的类型和编码
Redis 使用对象来表示数据库中的键和值,每一个键值对都是两个对象,键是一个对象,值是一个对象,如以下命令
SET msg “hello world”
就会创造出两个对象来分别存储, msg和"hello world"
对象数据结构如下
typedef struct redisObject {
// 类型
unsigned type:4;
// 编码
unsigned encoding:4;
// 对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 指向实际值的指针
void *ptr;
} robj;
8.1.1 类型
类型常量 | 对象的名称 | 类型 | 命令 |
REDIS_STRING | 字符串的独享 | string | SET msg “hello world” |
REDIS_LIST | 列表对象 | list | RPUSH numbers 1 3 5 |
REDIS_HASH | 哈希对象 | hash | HMSET profile name Tom age 25 career Programmer |
REDIS_SET | 集合对象 | set | SADD fruits apple banana cherry |
REDIS_ZSET | 有序集合对象 | zset | ZADD price 8.5 apple 5.0 banana 6.0 cherry |
5种类型的实现并不是唯一的,都由多种实现构成,没有银弹,没有一种数据结构是万能的,在不同的条件和场景下各种数据结构都有自己的优势,类似于 stl 用各种细分来压榨性能的思路来编写数据库,且在这种设计下,用一层中间桥接代码来切分应用层和底层实现,更易于扩展.
8.1.2
上述说道,每种类型都有多种编码方式,下面将进行编码方式的指代,每种编码方式对应着前七章的介绍的六种数据结构,除开天然的 int 编码常量
编码常量 | 编码对应的底层数据结构 |
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 | 跳跃表和字典 |
类型和编码之间是可以交叉的,类型可以由多种编码构成
类型 | 编码 | 对象 |
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 xxx
可以查看 xxx 值的编码方式
对应输出如下
编码常量 | 编码对应的底层数据结构 | OBJECT ENCODING命令输出 |
REDIS_ENCODING_INT | long类型的整数 | “int” |
REDIS_ENCODING_EMBSTR | 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” |
通过上述内容可以直接看到, Redis 根据不同的使用场景来对一个对象设置不同的编码方式,从而优化对象在某一场景下的效率.
下面,将逐一的介绍每种对象的编码实现以及在编码之间的转换.
8.2 字符串对象
字符串对象的编码可以是 int, raw 或 embstr.
int:
当存储的字符串可以转换为数字且小于21字节时,void* ptr将转换成long类类型,
如下:
SET numbers 10086
raw:
当存储的字符串的字节长度大于39字节时,将使用 raw 模式进行存储,
如下:
SET story "hello world, hello world, hello world, hello world, hello world, hello world, hello world, hello world"
embstr:
当存储的字符串的字节长度小于39字节时,将使用 embstr 进行存储,
如下:
SET msg "hello world"
int 无需赘述,
主要谈论一下 raw 和 embstr 的区别
raw 实现方式为之前讨论的 Redis 自己设计的动态字符串,没什么大花头,ptr直接指向 sdshdr 数据结构.
embstr 编码方式就比较特殊,是专门用于保存段字符串的一种优化编码方式,编码和raw一样,都使用 redisObject结构和 sdsshdr 结构来表示字符串对象,但是 raw 编码会调用两次内存分配, 一次调用获取 redisObject ,一次 调用获取 sdshdr , 而 embstr 编码则通过调用一次内存分配函数来分配一块连续的空间,空间一次包含 redisObject 和 sdshdr 两个结构
redisObject | sdshdr | |||||
type | encodin | ptr | … | free | len | buff |
所以简单总结一下 embstr 和 raw 的优势:
1): 分配时,将内存分配从两次减为一次.
2): 释放时,将内存释放从两次减为一次.
3): embstr 的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势.
关于浮点数,其实浮点数的存储也是用字符串来进行存储的
SET PI 3.14
如该条指令,存储后使用以下命令分别查看
type PI
OBJECT ENCODING PI
可以看到,类型为string,编码方式为 embstr ,当浮点数进行加法时,也是将字符串转换成浮点数,得出结果后,再转换为 string 进行保存
上述逻辑由以下代码实现
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
/* Make sure this is a string object, the only type we encode
* in this function. Other types use encoded memory efficient
* representations but are handled by the commands implementing
* the type. */
//确定编码为字符串类型,只对字符串进行编码转换,否则触发断言
redisAssertWithInfo(NULL,o,o->type == REDIS_STRING);
/* We try some specialized encoding only for objects that are
* RAW or EMBSTR encoded, in other words objects that are still
* in represented by an actually array of chars. */
// 只在字符串的编码为 RAW 或者 EMBSTR 时尝试进行编码
if (!sdsEncodedObject(o)) return o;
/* It's not safe to encode shared objects: shared objects can be shared
* everywhere in the "object space" of Redis and may end in places where
* they are not handled. We handle them only as values in the keyspace. */
// 不对共享对象进行编码
if (o->refcount > 1) return o;
/* Check if we can represent this string as a long integer.
* Note that we are sure that a string larger than 21 chars is not
* representable as a 32 nor 64 bit integer. */
// 对字符串进行检查
// 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
len = sdslen(s);
if (len <= 21 && string2l(s,len,&value)) {
/* This object is encodable as a long. Try to use a shared object.
* Note that we avoid using shared integers when maxmemory is used
* because every object needs to have a private LRU field for the LRU
* algorithm to work well. */
if (server.maxmemory == 0 &&
value >= 0 &&
value < REDIS_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {
if (o->encoding == REDIS_ENCODING_RAW) sdsfree(o->ptr);
o->encoding = REDIS_ENCODING_INT;
o->ptr = (void*) value;
return o;
}
}
/* If the string is small and is still RAW encoded,
* try the EMBSTR encoding which is more efficient.
* In this representation the object and the SDS string are allocated
* in the same chunk of memory to save space and cache misses. */
// 尝试将 RAW 编码的字符串编码为 EMBSTR 编码
if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {
robj *emb;
if (o->encoding == REDIS_ENCODING_EMBSTR) return o;
emb = createEmbeddedStringObject(s,sdslen(s));
decrRefCount(o);
return emb;
}
/* We can't encode the object...
*
* Do the last try, and at least optimize the SDS string inside
* the string object to require little space, in case there
* is more than 10% of free space at the end of the SDS string.
*
* We do that only for relatively large strings as this branch
* is only entered if the length of the string is greater than
* REDIS_ENCODING_EMBSTR_SIZE_LIMIT. */
// 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间
if (o->encoding == REDIS_ENCODING_RAW &&
sdsavail(s) > len/10)
{
o->ptr = sdsRemoveFreeSpace(o->ptr);
}
/* Return the original object. */
return o;
}
字符串对象保存各类型值的编码方式
值 | 编码 |
可以用 long 类型保存的整数 | int |
可以用long double 类型保存的浮点数 | embstr 或者 raw |
字符串值,或者因为长度太大而没办法用 long 类型表示的整数, 又或者因为长度太大而没办法用 long double 类型表示的浮点数 | embstr 或者 raw |
8.2.1 编码的转换
当一个对象编码方式是 int 时,当任何非数学范畴的字符添加时,如在尾部加上一个字符串,都会改变其编码方式,
SET number 10000
APPEND number "is a number"
number对象的编码将从 int 转换为 raw
而 embstr 编码也是类似,任何的修改操作都将会被转换成 raw,
即使书中没有提到这点,我们在看到 embstr 的设计方式时,也应该想到这一点,一次性分配 redisObject 和 embstr ,将两个对象同时绑定在同一块内存中时,就注定了 embstr 这种编码方式只能作为只读方式存在,当内存大小固定后再进行修改,还保持 embstr 这种模式,那么其优势也将不复存在,所以 Redis 的处理方法为,当 embstr 编码对象遇到读取之外的需求时,会改变其编码方式,转换为 raw 模式.
SET msg "hello"
APPEND msg "world"
msg 将变成 embstr
8.2.2 字符串命令的实现
见到这段内容,我还是有点诧异的,在我构思中,命令应该属于应用层的操作,无论编码方式如何,其底层都应该尽量支持每一种,但是命令这一动作却针对每种不同的对象类型,分化出了各种专属类型命令(应该可以这么理解),当然, Redis 作为一款成功的数据库,这么做肯定是有很强的道理的, 我的能力,见识,思考深度还是处于一个初级阶段,这也是我学习所希望能够改善的.希望在完成整体的学习后,能深刻认识如此实现的原因.
命令 | int 编码的实现方式 | embstr 编码的实现方式 | raw 编码的实现方式 |
SET | 保存成int | 保存成 embstr | 保存成 raw |
GET | 转换成字符串返回 | 直接返回字符串 | 直接返回字符串 |
APPEND | 转成成raw编码存储 | 转换成raw编码存储 | 调用 sdshdf 的字符串追加函数 sdscatlen |
INCRBYFLOAT | 取出int转换成浮点数,计算完毕后,再转换为字符串存回 | 尝试转为浮点数,成功则修改后存回,否则返回错误 | 取出int转换成浮点数,计算完毕后,再转换为字符串存回 |
INCRBY | 对整数值进行加法,再存回 | 错误 | 错误 |
DECRBY | 对整数值进行减法,在存回 | 错误 | 错误 |
STRLEN | 将整数值值拷贝转换成字符串,返回长度 | 返回字符串长度 | 返回字符串长度 |
SETRANGE | 转换成 raw 编码,然后使用 raw 的方式执行 | 转换成 raw 编码,然后使用 raw 的方式执行 | 将字符串特定索引上的值设置为给定的字符 |
GETRANGE | 将整数值值拷贝转换成字符串,返回字符串指定索引上的字符 | 直接返回字符串指定索引上的字符 | 直接返回字符串指定索引上的字符 |
8.3 列表对象
列表对象的编码可以是 ziplist 或者 linkedlist
ptr分别为指向双向链表linkedlist或者ziplist对象来进行实现
需要注意的是,linkedlist 没有原生能支持字符串的节点配置,所以链表节点的指针实际是指向一个 StringObject , 当然实际上 Redis 是没有这种数据结构的,实际上是编码为 raw 或者 embstr 的robj,代码如下
/*
* 创建一个新 robj 对象
*/
robj *createObject(int type, void *ptr) {
robj *o = zmalloc(sizeof(*o));
o->type = type;
o->encoding = REDIS_ENCODING_RAW;
o->ptr = ptr;
o->refcount = 1;
/* Set the LRU to the current lruclock (minutes resolution). */
o->lru = LRU_CLOCK();
return o;
}
8.3.1 编码转换
ziplist 和 linkedlist 之间当然也是可以互相转换的,两者之间在不同环境下各有优势
当同时满足以下条件时,列表对象使用ziplist编码:
1): 列表对象保存 所有字符串元素的长度都小于64字节.
2): 列表对象保存的元素数量小于512个.
不满足上述条件的都使用linkedlist来进行编码.
那该如何理解该条规则呢?
我是这样理解的
观察 ziplist 和 linkedlist 的设计思路,很容易将其退化成 vector 和 list 的对比,也就是动态数组和列表,
动态数组能快速的拿去元素,但是当进行扩容时,由于是本质是一整块内存,所以要进行一个整体迁移,其代价是需要跟list的读取效率进行取舍的,根据 redis 的判断,上述两个条件应该就是 ziplist 效率高于 linkedlist 的转折点,但如何得出该结论那就不在本次阅读的范围内了.
8.3.2 列表命令的实现
命令 | zpilist 编码的实现方式 | linkedlist 编码的实现方式 |
LPUSH | 调用 ziplistPush,将元素推入表头 | 调用 listAddNodeHead,将元素推入表头 |
RPUSH | 调用 ziplistPush,将元素推入表尾 | 调用 listAddNodetail,将元素推入表尾 |
LPOP | 调用 ziplistIndex 定位到表头,在返回元素后,删除表头 | 调用 listFirst 定位表头,返回元素后,再调用 listDelNode进行删除 |
RPOP | 调用 ziplistIndex 定位到表头,在返回元素后,删除表头 | 调用 listLast 定位表头,返回元素后,再调用 listDelNode进行删除 |
LINDEX | 调用 ziplistIndex 进行元素定位并返回 | 调用 listIndex 进行定位,并返回 |
LLen | 调用 ziplistLen 返回长度 | 调用 listLength 返回长度 |
LINSERT | 插入表头或表尾时使用ziplistPush,其他情况使用 ziplistInsert | 调用 listInsertNode 函数进行插入 |
LREM | 遍历节点,使用 ziplistDelete 进行对应节点删除 | 使用 listDelNode 删除对应节点 |
LTRIM | 调用ziplistDeleteRange 删除不在范围内的所有节点 | 遍历双端链表节点,并调用 listDelNode 删除链表中不在范围内的元素 |
LSET | 调用 ziplistDelete 删除指定指定索引的现有节点,然后调用ziplistInsert进行元素插入 | 调用 listIndex 找到指定节点,进行复制修改 |
8.4 哈希对象
哈希对象类型可以用 ziplist 或 hashtable 进行实现
当使用 ziplist 进行实现实现,会将键值对两个对象连续推入压缩列表尾部,也就是哈希对象键值对是连续存放的,键在前值在后.
HSET profile name "yxy"
HSET profile age "27"
HSET profile sex "man"
zlbytes | zltail | zlelen | “name” | “yxy” | “age” | 27 | “sex” | “man” | zlend |
ziplist 在内存中将用该种结构进行哈希对象的存储
当使用 hashtable 实现时, 所有的键值对都将使用 哈希节点实现 进行实现,键值对分别为 StrObject 对象
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
8.4.1 编码转换
当哈希对象同时满足一下两个条件时,哈希对象使用 ziplist 进行编码:
1): 哈希对象保存的所有键值对的键和值的字符串长度都小小于 64字节;
2): 哈希对象保存的键值对数量小于512个;
不能满足这两个条件的哈希对象需要使用 hashtable 编码
8.4.2 哈希命令的实现
命令 | ziplist 编码实现方法 | hashtable 编码的实现方法 |
HSET | 调用 ziplistpush 推入键,再调用 ziplistPush 推入值 | 调用 dictAdd函数,将新节点添加到字典里 |
HGET | 调用 ziplistFind 函数,找出对应节点,再调用 ziplistNext 函数,返回值节点 | 调用 dictFind 函数,再通过 dictGetVal 找出值节点 |
HEXISTS | 调用 ziplistFind 来进行节点查找 | 调用 dictFind 来进行查找 |
HDel | 调用 ziplistFind 函数,找到节点,并将当前节点和下一个节点都进行删除 | 调用 dictDelete函数,将指定键值对删除 |
HLen | 调用 ziplistLen 函数,得出当前 ziplist 的长度, 由于是键值对存放,所以返回一半的数量 | 调用 dictSize,并返回 |
HGETALL | 遍历整个列表,用 ziplistGet 函数返回所有节点 | 遍历整个字典,用 dictGetKey 函数 返回键,用 dictGetVal 函数返回值 |
8.5 集合对象
集合对象的编码跨越式 intset 或者是 hashtable
inset 模式存放的所有元素都为 整数时方可启用
SADD number 1 3 5
当元素不一定是整数时,使用 hashtable 进行实现,当然,key 为一个 StringObject, val 为 null
SADD fruits "apple" "banana" "cherry"
8.5.1 编码转换
当集合对象同时满足以下条条件时,对象使用 intset 编码:
1): 集合对象保存的所有元素都是整数值;
2): 集合对象保存的元素数量不超过512个;
不能同时满足这两个条件的集合对象需要使用 hashtable 编码
类似于字符串对象, 当 intset 编码模式中,插入一个字符串时,编码实现也会转换成 hashtable 模式.
SADD number "two"
number 将变成 hashtable 模式.
8.5.2 集合命令的实现
命令 | intset 编码的实现方法 | hashtable 编码的实现方法 |
SADD | 调用 intsetAdd 将所有元素添加到整数集合里面 | 调用 dictAdd ,以新元素为键,null为值,将键值对添加到字典里 |
SCARD | 调用 intsetLen ,返回集合元素数量 | 调用 dictsize 返回字典键值对数量 |
SISMEMBER | 调用 intsetFind ,查找元素是否存在集合中 | 调用 dictFind 进行查找 |
SMEMBERS | 遍历整个整数集合, 使用 intsetGet 返回所有集合 | 遍历整个字典,使用 dictGetKey 返回所有的键 |
SRANDMEMBER | 调用 intsetRandom 从整数集合中随机返回一个元素 | 调用 dictGetRankdomKey,从字典中返回一个字典键 |
SPOP | 调用 insetRandom 从整数集合中返回一个元素后,删除该元素 | 调用 dictGetRankdomKey,从字典中返回一个字典键后,删除该元素 |
SREM | 调用 insetRemove ,删除指定元素 | 调用 dictDelete函数, 从字典中删除所有键相同的元素 |
8.6 有序集合对象
有序集合对象实现可以是 ziplist 或者 skiplist
ziplist 方式和哈希对象实现类似,元素成员和分值为连续存放在两个邻近的节点中.
ZADD price 8.5 apple 5.0 banana 6.0 cherry
skiplist 并不是单纯的使用 skiplist 来进行有序集合对象的实现
/*
* 有序集合
*/
typedef struct zset {
// 字典,键为成员,值为分值
// 用于支持 O(1) 复杂度的按成员取分值操作
dict *dict;
// 跳跃表,按分值排序成员
// 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
// 以及范围操作
zskiplist *zsl;
} zset;
通过代码可以发现, skiplist 编码是通过 zset 的组合结构来进行有序集合的实现
dict 用来实现,成员到分值的映射,以O(1)的效率来进行成员分值的查询操作.
而 skiplist 可以用来范围查找.查找效率接近于红黑树为 O(logN)
而且实际上 skiplist 节点中的独享是和 dict 键值对共享的,所以并不会出现内存浪费的情况
8.6.1 编码的转换
当有序集合对象同时满足以下两个条件时,对象使用 ziplsit 编码:
1): 有序结合保存的元素数量小于128个;
2): 有序集合保存的所有元素成员的长度都小于64字节;
否则都将使用 skiplist 进行实现
8.6.2 有序集合命令的实现
命令 | ziplist 编码的实现方式 | zet编码的实现方式 |
ZADD | 调用 ziplistInert 函数,将成员和分值作为两个节点分别插入 | 调用 zslInsert 插入跳跃表,再通过 dictAdd 添加到字典 |
ZCARD | 调用 ziplistlen 函数,除以2,再返回 | 直接读取跳跃表的 length 属性 |
ZCOUNT | 遍历压缩列表,来进行统计 | 遍历跳跃表,统计范围内分值的数量 |
ZRANGE | 遍历压缩列表,来进行统计 | 遍历跳跃表,统计范围内分值的数量 |
ZRANK | 通过遍历节点数量,找到对应元素后,遍历的节点数量除以2就是排名 | 遍历节点,找到元素后,经过的节点就是成员排名 |
ZREVRANK | 从表尾开始遍历,经过的节点就是排名 | 遍历跳跃表节点,找到元素后,经过的节点就是成员排名 |
ZREM | 遍历压缩表,删除节点以及下一个节点 | 遍历跳跃表,删除给点的跳跃表节点,并在字典中也进行同时删除 |
ZSCORE | 遍历压缩表,找到元素后,返回下一个节点的具体数值 | 通过字典找到元素进行值返回 |
8.7 类型检查与命令多态
命令大体上分为两类,一种是任何键都可以执行,如 DEL 命令, EXPIRE 命令, RENAME 命令, TYPE 命令, OBJECT 命令
但是有些类型的操作是需要特殊的命令,如:
SET,GET,APPEND,STRLEN等命令只能对字符串键执行;
HDEL,HSET,HGET,HLEN等命令只能对哈希键执行;
RPUSH,LPOP,LINSERT,LLEN等命令只能对列表键执行;
SADD,SPOP,SINTER,SCARD等命令智能化对集合键执行;
ZADD,ZCARD,ZRANK,ZSCORE等命令只能对有序集合键执行;
如果用不相容的命令去执行其他类型的键时, Redis 会返回一个错误
(error) WRONGTYPE operation against a key holding the wrong kind of value
8.7.1 类型检查的实现
当客户端发来一个命令时,服务器会用 redisObject 中的 type 属性进行初步检查,通过后再调用具体实现函数,否则返回一个错误
8.7.2 多态命令的实现
多态命令类似于面向对象中虚函数的概念,当然这里我们使用的是 c ,所以我们只是使用了相同的概念,LLEN命令是用来对列表键进行操作的,但是链表键的实现可能由 ziplist 或 linkedlist 来实现,但是最终他们都进行 LLEN 对应函数的调用, 所以我们可以任务 LLEN 是一个多态命令, 无论 列表键的实现方式是如何,最终都能找到正确的对应调用函数,并返回给我们正确的结果
8.8 内存回收
内存回收的思路类似于 C++ 中的智能指针 shard_ptr , 通过特定的函数进行申请和释放,且维护一个共享的计数器,当计数器为0时候进行内存释放.在C++中通过构造函数和析构函数来完成计数的维护,也被称为RAII技术,构造时就完成释放.
计数器的原则如下:
1): 在创建一个新对象时,引用计数的值会被初始化为1;
2): 当对象被一个新程序使用时,引用计数值会被增加1;
3): 当对象不再被一个程序使用时,它的引用计数值会被减1;
4): 当对象的引用计数值为0时,对象所占用的内存会被释放;
这个不就是智能指针吗…
8.9 对象共享
基于内存回收机制(智能指针),当一个对象可以被共享时,我们可以更放心的将对象开放出去,比如 有序集合中的 zset 结构,通过 skiplist 和 dict 的组合完成的数据结构,他们的节点就可以用同一个对象,用内存回收思路进行维护.
虽然会增加一定的复杂度,但是好处也是不言而喻的:
原方法为当100个对象同时进行一个对象的存储时,需要进行100个对象的内存申请,但是他们的内容都是100这个整数值,而对象进行共享后,100个对象迅速缩减为1个.100个地方进行共享,所要改变的仅仅是计数器值而已
但是 Redis 并不支持字符串的共享, 理由为 整数值的验证操作复杂度为 O(1), 而共享字符串的验证操作复杂度为 O(N),谁让字符串是数组呢.不仅仅限制于字符串,每当共享对象的复杂度上升时,验证操作复杂度更高,如列表键中的stringObject 其验证复杂度就位 O(N^2),这个是无法接受的,所以当遇到复杂对象时,还是使用空间换时间的思路,当然能节省我们还是节省,对于只包含整数的字符串对象就是这么做的.
8.10 对象的空转时长
在 redisObjet 中还有一个 lru 属性,用来记录对象最后一次被访问的时间
通过 OBJECT IDLETIME 就可以看到 robj 的空转时间
用当前时间减去 lru 属性来得出.
当然 OBJECT IDLETIME 去访问的时,不会去修改 lru 属性.
总结
还是我之前的观点,没有银弹,没有一个数据结构是万能的, Redis 所做的就是更细分所有的存储结构,在不同的环境下做出不同的选择,来达到提升效率的目的.
在文章中我曾提出一个问题,为什么不同的类型需要用不同的命令来进行操作,说实话,我还是不明白这么做的原因,或者说通用的命令也太少了,据我观察,至少 size 这个命令是可以被所有数据结构通用的, 但却有 LLEN HLen CARD 等多种不同的版本.
对象的总结:
每个键值对都是2个对象,键为一个,值为一个
Redis 共有字符串,列表,哈希,集合,有序集合五种类型的对象,每种类型都有2种或以上的实现方式
服务器在执行命令之前 ,会先建厂给定键的类型是否能执行
对象带有类似于 C++ shard_ptr 的内存回收机制
会共享 0 ~ 9999 的字符串对象,受限于验证操作复杂度,只支持这么多,当然数据库大量的数据都是 小整数,虽然范围小,但是重复率超高.
对象会记录最后一次访问时间,可以用来计算空转时间