Redis 对象系统:类型、编码与 LRU 计数器详解

好的,没问题!

各位观众老爷们,今天咱们来聊聊Redis的“对象系统”,听起来是不是有点高大上?其实一点都不难,说白了,就是Redis怎么管理它里面的数据,以及怎么省着点用内存。

Redis作为一个内存数据库,寸土寸金,每一块内存都要精打细算。所以,它搞了一套“对象系统”,就像一个精明的管家,把各种数据都打理得井井有条。

对象系统是什么?

简单来说,Redis里存储的任何东西,都不是直接存的,而是封装成了一个redisObject结构体。这个结构体就像一个“百宝箱”,里面装着真实的数据,还记录着数据的类型、编码方式,以及一些其他的元数据。

typedef struct redisObject {
    unsigned type:4;       // 对象类型 (5种)
    unsigned encoding:4;   // 对象编码 (8种)
    unsigned lru:LRU_BITS;  // LRU时间 (用于淘汰)
    int refcount;          // 引用计数
    void *ptr;             // 指向底层数据结构的指针
} robj;

看到没?这个redisObject就是核心。咱们一点点来解剖它:

  • type (对象类型): 告诉我们这个“百宝箱”里装的是什么类型的宝贝,比如字符串、列表、哈希、集合、有序集合。

  • encoding (对象编码): 告诉我们这个宝贝是怎么存放的,也就是底层的数据结构。同一个类型的宝贝,可以有不同的存放方式,这样可以根据实际情况选择最省内存、最快的方案。

  • lru (LRU时间): 这个是用来做内存淘汰的。当Redis内存不够用的时候,会把最久没用过的宝贝给“扔掉”,腾出空间给新来的。这个lru记录的就是这个宝贝上次被使用的时间,帮助Redis判断谁该“滚蛋”。

  • refcount (引用计数): 这个很重要,用来做内存共享的。如果多个地方都用到了同一个宝贝,Redis不会傻乎乎地复制多份,而是让它们都指向同一个宝贝,然后把refcount加1。当一个地方不用这个宝贝了,就把refcount减1。当refcount变成0的时候,说明这个宝贝彻底没用了,Redis就可以把它回收了。

  • ptr (指针): 指向实际存储数据的内存地址。

对象类型(Type):五大金刚

Redis支持五种基本数据类型,也就是type字段的取值:

类型常量 类型名称 描述
REDIS_STRING 字符串 (string) 可以是字符串、整数、浮点数,但最终都以字符串形式存储。
REDIS_LIST 列表 (list) 有序的字符串列表。可以从头部或尾部添加、删除元素。
REDIS_HASH 哈希 (hash) 键值对的集合,类似于字典。
REDIS_SET 集合 (set) 无序、唯一的字符串集合。
REDIS_ZSET 有序集合 (zset) 有序的字符串集合,每个元素都有一个分数(score),用于排序。

对象编码(Encoding):八面玲珑

每种数据类型,根据数据的大小、复杂度等因素,可以选择不同的编码方式,也就是encoding字段的取值。不同的编码方式,底层使用的数据结构也不同。

1. 字符串(string):

  • REDIS_ENCODING_INT: 如果字符串可以被解释成整数,就用这种编码。直接把整数值存在ptr里,省空间。
  • REDIS_ENCODING_RAW: 如果字符串比较长,或者不能被解释成整数,就用这种编码。底层用简单动态字符串(SDS)来存储。
  • REDIS_ENCODING_EMBSTR: 如果字符串比较短,就用这种编码。把redisObject和SDS放在一块连续的内存里,减少内存碎片。

代码示例:

// 创建一个整数类型的字符串对象
robj *createStringObjectFromLongLong(long long value) {
    robj *o = zmalloc(sizeof(robj));
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_INT;
    o->ptr = (void*)((long)value); // 直接把整数值存到ptr里
    o->refcount = 1;
    return o;
}

// 创建一个SDS类型的字符串对象
robj *createStringObject(const char *s, size_t len) {
    robj *o = zmalloc(sizeof(robj));
    o->type = REDIS_STRING;
    o->encoding = REDIS_ENCODING_RAW;
    o->ptr = sdsnewlen(s, len); // 创建一个SDS
    o->refcount = 1;
    return o;
}

2. 列表(list):

  • REDIS_ENCODING_ZIPLIST: 如果列表的元素比较少,而且每个元素都比较小,就用这种编码。底层用压缩列表(ziplist)来存储。ziplist是一种非常紧凑的数据结构,可以节省大量的内存。
  • REDIS_ENCODING_LINKEDLIST: 如果列表的元素比较多,或者某个元素比较大,就用这种编码。底层用双向链表(linkedlist)来存储。双向链表插入、删除元素比较快,但是占用空间比较大。
  • REDIS_ENCODING_QUICKLIST: Redis 3.2 引入的新编码,结合了ziplist和linkedlist的优点。它是一个ziplist的链表,每个ziplist可以存储多个元素。这样既可以节省内存,又可以提高性能。

3. 哈希(hash):

  • REDIS_ENCODING_ZIPLIST: 如果哈希的键值对比较少,而且每个键值对都比较小,就用这种编码。底层用压缩列表(ziplist)来存储。
  • REDIS_ENCODING_HT: 如果哈希的键值对比较多,或者某个键值对比较大,就用这种编码。底层用字典(dict)来存储。字典是一种哈希表,可以快速地查找键值对。

4. 集合(set):

  • REDIS_ENCODING_INTSET: 如果集合的所有元素都是整数,就用这种编码。底层用整数集合(intset)来存储。intset是一种有序、唯一的整数集合,可以节省内存。
  • REDIS_ENCODING_HT: 如果集合的元素不是整数,或者整数的范围超出了intset的范围,就用这种编码。底层用字典(dict)来存储。字典的键存储集合的元素,值都设置为NULL。

5. 有序集合(zset):

  • REDIS_ENCODING_ZIPLIST: 如果有序集合的元素比较少,而且每个元素都比较小,就用这种编码。底层用压缩列表(ziplist)来存储。ziplist的元素按照score从小到大排序。
  • REDIS_ENCODING_SKIPLIST: 如果有序集合的元素比较多,或者某个元素比较大,就用这种编码。底层用跳跃表(skiplist)和字典(dict)来存储。skiplist用于按照score排序,dict用于根据元素查找score。

总结一下各种类型的encoding:

类型 编码方式 底层数据结构 适用场景
string REDIS_ENCODING_INT int 可以被解释成整数的字符串
string REDIS_ENCODING_RAW SDS 较长的字符串,或者不能被解释成整数的字符串
string REDIS_ENCODING_EMBSTR SDS (连续内存) 较短的字符串
list REDIS_ENCODING_ZIPLIST ziplist 元素少且小的列表
list REDIS_ENCODING_LINKEDLIST linkedlist 元素多或元素大的列表
list REDIS_ENCODING_QUICKLIST ziplist链表 结合了ziplist和linkedlist的优点
hash REDIS_ENCODING_ZIPLIST ziplist 键值对少且小的哈希
hash REDIS_ENCODING_HT dict 键值对多或键值对大的哈希
set REDIS_ENCODING_INTSET intset 所有元素都是整数的集合
set REDIS_ENCODING_HT dict 元素不是整数,或者整数范围超出intset范围的集合
zset REDIS_ENCODING_ZIPLIST ziplist 元素少且小的有序集合
zset REDIS_ENCODING_SKIPLIST skiplist + dict 元素多或元素大的有序集合

编码转换:见机行事

Redis并不是一开始就确定一个对象的编码方式,而是会根据数据的变化,动态地进行编码转换。

比如,一个字符串一开始是用REDIS_ENCODING_INT编码的,但如果后来往里面追加了一个非数字字符,Redis就会把它转换成REDIS_ENCODING_RAW编码。

类似地,如果一个列表一开始是用REDIS_ENCODING_ZIPLIST编码的,但如果后来元素变得太多,或者某个元素变得太大,Redis就会把它转换成REDIS_ENCODING_LINKEDLIST编码。

这种动态编码转换的机制,使得Redis可以根据实际情况,选择最合适的编码方式,从而提高性能和节省内存。

LRU 计数器:谁最不重要?

前面说了,lru字段记录的是对象上次被访问的时间,用于内存淘汰。Redis使用LRU(Least Recently Used,最近最少使用)算法来淘汰内存。

但是,Redis并没有使用标准的LRU算法,而是使用了一种近似的LRU算法。为什么呢?因为标准的LRU算法需要维护一个全局的链表,每次访问一个对象都要更新链表,开销太大。

Redis的近似LRU算法是这样的:

  1. Redis维护一个全局的LRU时钟。
  2. 每个redisObject都有一个lru字段,记录的是对象上次被访问时,全局LRU时钟的值。
  3. 当需要淘汰内存的时候,Redis会随机选择一些对象,然后比较它们的lru值,选择lru值最小的对象进行淘汰。

这种算法虽然不是完全精确,但是开销很小,而且效果也比较好。

LRU_BITS:精度和范围

LRU_BITS定义了lru字段的位数。位数越多,可以表示的时间范围就越大,精度也就越高。但是,位数越多,占用的空间也就越大。

Redis会根据maxmemory-policy配置,选择不同的LRU_BITS

  • 如果maxmemory-policy配置为volatile-lruallkeys-lru等LRU策略,LRU_BITS通常是24位。
  • 如果maxmemory-policy配置为volatile-ttlallkeys-random等非LRU策略,LRU_BITS通常是8位。

引用计数:共享的秘密

refcount字段记录的是对象被引用的次数。当refcount为0的时候,说明对象没有被任何地方引用,就可以被回收了。

引用计数机制可以有效地节省内存。如果多个地方都用到了同一个对象,Redis不会复制多份,而是让它们都指向同一个对象,然后把refcount加1。

代码示例:

// 增加对象的引用计数
void incrRefCount(robj *o) {
    o->refcount++;
}

// 减少对象的引用计数
void decrRefCount(robj *o) {
    if (o->refcount <= 0) return; // 防止出现负数
    if (o->refcount == 1) {
        // 引用计数为1,说明对象可以被回收了
        switch (o->type) {
            case REDIS_STRING: freeStringObject(o); break;
            case REDIS_LIST: freeListObject(o); break;
            case REDIS_HASH: freeHashObject(o); break;
            case REDIS_SET: freeSetObject(o); break;
            case REDIS_ZSET: freeZsetObject(o); break;
            default: break;
        }
        zfree(o);
    } else {
        o->refcount--;
    }
}

对象共享:小数字的智慧

在Redis启动的时候,会预先创建一些常用的对象,比如0到9999的整数。这些对象会被多个地方共享,从而节省内存。

当需要使用这些整数的时候,Redis会直接返回共享的对象,而不是重新创建一个新的对象。

命令与对象:幕后推手

当我们执行一个Redis命令的时候,Redis会先对输入的参数进行类型检查,然后根据参数的类型,创建相应的redisObject

比如,当我们执行SET key value命令的时候,Redis会创建两个redisObject,一个表示key,一个表示value

命令执行完毕后,Redis会根据情况,决定是否需要释放这些redisObject

总结:Redis对象系统的精髓

Redis的对象系统是Redis高效运行的关键之一。它通过以下手段来提高性能和节省内存:

  • 多种数据类型: 支持五种基本数据类型,可以满足不同的应用场景。
  • 多种编码方式: 每种数据类型都有多种编码方式,可以根据实际情况选择最合适的编码方式。
  • 动态编码转换: 可以根据数据的变化,动态地进行编码转换。
  • 近似LRU算法: 使用近似LRU算法来淘汰内存,开销小,效果好。
  • 引用计数机制: 通过引用计数机制来实现内存共享,节省内存。
  • 对象共享: 预先创建一些常用的对象,供多个地方共享。

理解了Redis的对象系统,就能更好地理解Redis的内部机制,从而更好地使用Redis。

好了,今天的讲座就到这里,希望大家有所收获! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注