对象

对象这一章节可以理解为前面七个章节的总结篇,仿佛是我在前七章中获取了七个拼图,而这一章中,我们将使用七章拼图来进行实际的拼接操作.

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 的字符串对象,受限于验证操作复杂度,只支持这么多,当然数据库大量的数据都是 小整数,虽然范围小,但是重复率超高.
对象会记录最后一次访问时间,可以用来计算空转时间