zoukankan      html  css  js  c++  java
  • Redis源码剖析(八)对象系统

    对象的类型与编码

    在 Redis 中新创建一个键值对时, 我们至少会创建两个对象, 一个对象用作键值对的键(键对象), 另一个对象用作键值对的值(值对象)。Redis 中的每个对象都由一个 redisObject 结构表示:

    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;

     

    对象类型

    对象的type属性记录了对象的类型,type属性的值有以下几种:

    类型 

    对象名称

    REDIS_STRING       

    字符串对象        

    REDIS_LIST

    列表对象

    REDIS_HASH

    哈希对象

    REDIS_SET

    集合对象

    REDIS_ZSET

    有序集合对象

    编码与底层实现

    对象的encoding属性记录了对象使用的编码,即底层使用的数据结构,encoding的值有以下几种:

    编码常量

    底层数据结构

     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

     跳跃表

     


    字符串对象

    值对象编码

    字符串对象的编码可以是 int 、 raw 或者 embstr

    如果字符串对象保存的是整数值, 那么这个整数值保存在 ptr属性(将 void* 转换成 long ) ,对象编码为int

    如果字符串对象保存的是字符串值, 并且字符串长度大于 39 字节, 那么该字符串值使用简单动态字符串(SDS)来保存, 对象编码为 raw 。

    如果字符串对象保存的字符串值, 并且字符串长度小于等于 39 字节, 那么字符串对象将使用 embstr 编码来保存字符串值。

    字符串对象编码的部分代码如下:

    robj *tryObjectEncoding(robj *o) {
        long value;
        sds s = o->ptr;
        size_t len;
    
        // 对字符串进行检查
        // 只对长度小于或等于 21 字节,并且可以被解释为整数的字符串进行编码
        len = sdslen(s);
        if (len <= 21 && string2l(s,len,&value)) {
        ......
    o->ptr = (void*) value;
    .......
    }
    // 尝试将 RAW 编码的字符串编码为 EMBSTR 编码,#define REDIS_ENCODING_EMBSTR_SIZE_LIMIT 39 if (len <= REDIS_ENCODING_EMBSTR_SIZE_LIMIT) {     ...... } // 这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间 if (o->encoding == REDIS_ENCODING_RAW && sdsavail(s) > len/10) { o->ptr = sdsRemoveFreeSpace(o->ptr); } return o; }

    EMBSTR编码时,直接申请一段连续的空间,空间中依次包含 redisObject 和 sdshdr 两个结构:

    robj *createEmbeddedStringObject(char *ptr, size_t len) {
        robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
        ......
        return o;
    }

    编码的转换

     当我们对 embstr 编码的字符串对象执行任何修改命令时, 程序会先将对象的编码从 embstr 转换成 raw , 然后再执行修改命令。下面给出APPEND命令部分代码:

    void appendCommand(redisClient *c) {
        size_t totlen;
        robj *o, *append;
    
        // 取出键相应的值对象
        o = lookupKeyWrite(c->db,c->argv[1]);
    
        // 检查追加操作之后,字符串的长度是否符合 Redis 的限制
        append = c->argv[2];
        totlen = stringObjectLen(o)+sdslen(append->ptr);
        if (checkStringLength(c,totlen) != REDIS_OK)
                return;
    
        // 执行追加操作,此处会进行编码的转换
        o = dbUnshareStringValue(c->db,c->argv[1],o);
        o->ptr = sdscatlen(o->ptr,append->ptr,sdslen(append->ptr));
        totlen = sdslen(o->ptr);
    }

    dbUnshareStringValue函数实现:

    robj *dbUnshareStringValue(redisDb *db, robj *key, robj *o) {
        redisAssert(o->type == REDIS_STRING);
        if (o->refcount != 1 || o->encoding != REDIS_ENCODING_RAW) {
            robj *decoded = getDecodedObject(o);
            //创建raw编码对象
            o = createRawStringObject(decoded->ptr, sdslen(decoded->ptr));
            decrRefCount(decoded);
            dbOverwrite(db,key,o);
        }
        return o;
    }

     列表对象

    编码转换

    列表对象的编码可以是 ziplist 或者 linkedlist

    列表对象使用 ziplist 编码:

    1. 列表对象保存的所有字符串元素的长度都小于 64 字节
    2. 列表对象保存的元素数量小于 512 个

    不能满足这两个条件的列表对象需要使用 linkedlist 编码。以上两个条件的上限值可以通过配置文件中的list-max-ziplist-value 和 list-max-ziplist-entries 选项进行修改。

    ziplist编码的列表对象:

    linkedlist编码的列表对象:

    编码转换的部分代码如下:

    void listTypeTryConversion(robj *subject, robj *value) {if (sdsEncodedObject(value) &&
            // 看字符串是否过长
            sdslen(value->ptr) > server.list_max_ziplist_value)
                // 将编码转换为双端链表
                listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
    }
    
    // PUSH操作
    void listTypePush(robj *subject, robj *value, int where) {
    
        // 是否需要转换编码?
        listTypeTryConversion(subject,value);
    
        if (subject->encoding == REDIS_ENCODING_ZIPLIST &&
            ziplistLen(subject->ptr) >= server.list_max_ziplist_entries)
                listTypeConvert(subject,REDIS_ENCODING_LINKEDLIST);
    
        // ZIPLIST
        if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
            int pos = (where == REDIS_HEAD) ? ZIPLIST_HEAD : ZIPLIST_TAIL;
            // 取出对象的值,因为 ZIPLIST 只能保存字符串或整数
            value = getDecodedObject(value);
            subject->ptr = ziplistPush(subject->ptr,value->ptr,sdslen(value->ptr),pos);
            decrRefCount(value);
    
        // 双端链表
        } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
            if (where == REDIS_HEAD) {
                listAddNodeHead(subject->ptr,value);
            } else {
                listAddNodeTail(subject->ptr,value);
            }
            incrRefCount(value);
    
        // 未知编码
        } else {
            redisPanic("Unknown list encoding");
        }
    }

    阻塞行为

    待填坑


    哈希对象

    哈希对象的编码可以是 ziplist 或者 hashtable 。

    当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码:

    1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节
    2. 哈希对象保存的键值对数量小于 512 个

    不能满足这两个条件的哈希对象需要使用 hashtable 编码。以上两个条件的上限值可以通过配置文件中的 hash-max-ziplist-value 和  hash-max-ziplist-entries 选项进行修改。

    ziplist编码的哈希对象:

    另一方面, hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:

    • 字典的每个都是一个字符串对象
    • 字典的每个都是一个字符串对象

     hashtable编码的哈希对象:


    集合对象

     集合对象的编码可以是 intset 或者 hashtable 

    当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码:

    1. 集合对象保存的所有元素都是整数值
    2. 集合对象保存的元素数量不超过 512 个

    不能满足这两个条件的集合对象需要使用 hashtable 编码。元素数量的上限值是可以通过配置文件 set-max-intset-entries 选项进行修改。

    intset编码的集合对象:

     

     hashtable编码的集合对象


    有序集合对象

    有序集合的编码可以是 ziplist 或者 skiplist 

    当使用ziplist编码作为有序集合对象底层实现时,每个集合元素使用两个压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序。

    ziplist编码的有序集合对象如下:

    skiplist 编码的有序集合对象使用 zset 结构作为底层实现:

    typedef struct zset {
    
        zskiplist *zsl;
    
        dict *dict;
    
    } zset;

    zset 结构中的 zsl 跳跃表按分值从小到大保存了所有集合元素: 跳跃表节点的 object 属性保存了元素的成员,  score 属性则保存了元素的分值ZRANK、 ZRANGE 等命令就是基于跳跃表 API 来实现的。

    除此之外, zset 结构中的 dict 字典为有序集合创建了一个从成员到分值的映射: 字典的保存了元素的成员, 而字典的则保存了元素的分值。 通过字典, 可以用 O(1) 复杂度查找给定成员的分值

    skiplist编码的有序集合对象如下:

    这两种数据结构都会通过指针来共享相同元素的成员和分值,不会因此而浪费额外的内存。


    内存回收

     Redis 使用引用计数技术实现的内存回收机制。引用计数信息由 redisObject 结构的 refcount 属性记录。

    以list的PUSH操作为例:

    void listTypePush(robj *subject, robj *value, int where) {
        ......
        // ZIPLIST
        if (subject->encoding == REDIS_ENCODING_ZIPLIST) {
            ......
            decrRefCount(value);
    
        // 双端链表
        } else if (subject->encoding == REDIS_ENCODING_LINKEDLIST) {
            ......
            incrRefCount(value);
        }
    }
    incrRefCount的实现,当引用计数为0时会释放对象内存:
    void decrRefCount(robj *o) {
    
        if (o->refcount <= 0) redisPanic("decrRefCount against refcount <= 0");
    
        // 释放对象
        if (o->refcount == 1) {
            switch(o->type) {
            case REDIS_STRING: freeStringObject(o); break;
            case REDIS_LIST: freeListObject(o); break;
            case REDIS_SET: freeSetObject(o); break;
            case REDIS_ZSET: freeZsetObject(o); break;
            case REDIS_HASH: freeHashObject(o); break;
            default: redisPanic("Unknown object type"); break;
            }
            zfree(o);
    
        // 减少计数
        } else {
            o->refcount--;
        }
    }

    对象的空转时长

    redisObject 结构中的 lru 属性记录了对象最后一次被命令程序访问的时间。可以通过OBJECT IDLETIME 命令可以打印出给定键的空转时长。

    除了可以被 OBJECT IDLETIME 命令打印出来之外, 键的空转时长还有另外一项作用: 如果服务器打开了 maxmemory 选项, 并且服务器用于回收内存的算法为 volatile-lru 或者 allkeys-lru , 那么当服务器占用的内存数超过了 maxmemory 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。


  • 相关阅读:
    86. Partition List
    328. Odd Even Linked List
    19. Remove Nth Node From End of List(移除倒数第N的结点, 快慢指针)
    24. Swap Nodes in Pairs
    2. Add Two Numbers(2个链表相加)
    92. Reverse Linked List II(链表部分反转)
    109. Convert Sorted List to Binary Search Tree
    138. Copy List with Random Pointer
    为Unity的新版ugui的Prefab生成预览图
    ArcEngine生成矩形缓冲区
  • 原文地址:https://www.cnblogs.com/lizhimin123/p/10167867.html
Copyright © 2011-2022 走看看