redis-对象

redis-对象

对象的类型与编码

  • edis 中的每个对象都由一个 redisObject 结构表示, 该结构中和保存数据有关的三个属性分别是 type 属性、 encoding 属性和 ptr 属性:

    typedef struct redisObject {
    
        // 类型
        unsigned type:4;
    
        // 编码
        unsigned encoding:4;
    
        // 指向底层实现数据结构的指针
        void *ptr;
    
        // ...
    
    } robj;
    
    
1、类型(type)
  • 对于 Redis 数据库保存的键值对来说, 键总是一个字符串对象, 而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种, 因此:

    • 当我们称呼一个数据库键为“字符串键”时, 我们指的是“这个数据库键所对应的值为字符串对象”;
    • 当我们称呼一个键为“列表键”时, 我们指的是“这个数据库键所对应的值为列表对象”,
  • TYPE 命令的实现方式也与此类似, 当我们对一个数据库键执行 TYPE 命令时, 命令返回的结果为数据库键对应的值对象的类型, 而不是键对象的类型:

    # 键为字符串对象,值为字符串对象
    
    redis> SET msg "hello world"
    OK
    
    redis> TYPE msg
    string
    
    # 键为字符串对象,值为列表对象
    
    redis> RPUSH numbers 1 3 5
    (integer) 6
    
    redis> TYPE numbers
    list
    
    # 键为字符串对象,值为哈希对象
    
    redis> HMSET profile name Tome age 25 career Programmer
    OK
    
    redis> TYPE profile
    hash
    
    # 键为字符串对象,值为集合对象
    
    redis> SADD fruits apple banana cherry
    (integer) 3
    
    redis> TYPE fruits
    set
    
    # 键为字符串对象,值为有序集合对象
    
    redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry
    (integer) 3
    
    redis> TYPE price
    zset
    
    • 不同类型值对象的 TYPE 命令输出

      对象对象 type 属性的值TYPE 命令的输出
      字符串对象REDIS_STRING"string"
      列表对象REDIS_LIST"list"
      哈希对象REDIS_HASH"hash"
      集合对象REDIS_SET"set"
      有序集合对象REDIS_ZSET"zset"
2、编码(encoding)
  • encoding 属性记录了对象所使用的编码, 也即是说这个对象使用了什么数据结构作为对象的底层实现, 这个属性的值可以是表 8-3 列出的常量的其中一个。

    对象的编码

    编码常量编码所对应的底层数据结构
    REDIS_ENCODING_INTlong 类型的整数
    REDIS_ENCODING_EMBSTRembstr 编码的简单动态字符串
    REDIS_ENCODING_RAW简单动态字符串
    REDIS_ENCODING_HT字典
    REDIS_ENCODING_LINKEDLIST双端链表
    REDIS_ENCODING_ZIPLIST压缩列表
    REDIS_ENCODING_INTSET整数集合
    REDIS_ENCODING_SKIPLIST跳跃表和字典

    不同类型和编码的对象

    类型编码对象
    REDIS_STRINGREDIS_ENCODING_INT使用整数值实现的字符串对象。
    REDIS_STRINGREDIS_ENCODING_EMBSTR使用 embstr 编码的简单动态字符串实现的字符串对象。
    REDIS_STRINGREDIS_ENCODING_RAW使用简单动态字符串实现的字符串对象。
    REDIS_LISTREDIS_ENCODING_ZIPLIST使用压缩列表实现的列表对象。
    REDIS_LISTREDIS_ENCODING_LINKEDLIST使用双端链表实现的列表对象。
    REDIS_HASHREDIS_ENCODING_ZIPLIST使用压缩列表实现的哈希对象。
    REDIS_HASHREDIS_ENCODING_HT使用字典实现的哈希对象。
    REDIS_SETREDIS_ENCODING_INTSET使用整数集合实现的集合对象。
    REDIS_SETREDIS_ENCODING_HT使用字典实现的集合对象。
    REDIS_ZSETREDIS_ENCODING_ZIPLIST使用压缩列表实现的有序集合对象。
    REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳跃表和字典实现的有序集合对象。

    使用 OBJECT ENCODING 命令可以查看一个数据库键的值对象的编码:

    对象所使用的底层数据结构编码常量OBJECT ENCODING 命令输出
    整数REDIS_ENCODING_INT"int"
    embstr 编码的简单动态字符串(SDS)REDIS_ENCODING_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"
3、底层实现数据结构的指针 (*ptr)
  • 字符串对象

    • 字符串对象的编码可以是 intraw 或者 embstr

      digraph {      label = "\n 图 8-1    int 编码的字符串对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_STRING | encoding \n REDIS_ENCODING_INT |  ptr | ... "];      node [shape = plaintext];      number [label = "10086"]      redisObject:ptr -> number;  }

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

      digraph {      label = "\n 图 8-2    raw 编码的字符串对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_STRING | encoding \n REDIS_ENCODING_RAW |  ptr | ... "];      sdshdr [label = "  sdshdr | free \n 0 | len \n 43 |  buf"];      buf [label = " { 'L' | 'o' | 'n' | 'g' | ... | 'k' | 'i' | 'n' | 'g' | ' ' | '.' | '.' | '.' | '\0' } " ];      //      redisObject:ptr -> sdshdr:head;     sdshdr:buf -> buf;  }

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

      digraph {      label = "\n 图 8-3    embstr 编码创建的内存块结构";      node [shape = record];      embstr [ label = " { redisObject | { type | encoding |  ptr | ... } } |  { sdshdr | { free | len |  buf }} " ];  }

      使用 embstr 编码的字符串对象来保存短字符串值有以下好处:

      • embstr 编码将创建字符串对象所需的内存分配次数从 raw 编码的两次降低为一次。
      • 释放 embstr 编码的字符串对象只需要调用一次内存释放函数, 而释放 raw 编码的字符串对象需要调用两次内存释放函数。
      • 因为 embstr 编码的字符串对象的所有数据都保存在一块连续的内存里面, 所以这种编码的字符串对象比起 raw 编码的字符串对象能够更好地利用缓存带来的优势。
  • 列表对象

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

      • ziplist 编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素。

        digraph {      label = "\n 图 8-5    ziplist 编码的 numbers 列表对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_LIST | encoding \n REDIS_ENCODING_ZIPLIST |  ptr | ... "];      ziplist [label = " { zlbytes | zltail | zllen | 1 | "three" | 5 | zlend } "];      redisObject:ptr -> ziplist;  }

      • linkedlist 编码的列表对象使用双端链表作为底层实现, 每个双端链表节点(node)都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。

        digraph {      label = "\n 图 8-6    linkedlist 编码的 numbers 列表对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_LIST | encoding \n REDIS_ENCODING_LINKEDLIST |  ptr | ... "];      subgraph cluster_linked_list {          label = "链表";          style = dashed;          node1 [label = "StringObject \n 1 "];         node2 [label = "StringObject \n "three""];         node3 [label = "StringObject \n 5 "];          node1 -> node2 -> node3;      }      redisObject:ptr -> node1;  }

    • 当列表对象可以同时满足以下两个条件时, 列表对象使用 ziplist 编码,不能满足这两个条件的列表对象需要使用 linkedlist 编码:

      1. 列表对象保存的所有字符串元素的长度都小于 64 字节;
      2. 列表对象保存的元素数量小于 512 个;
      • 列表对象因为保存了长度太大的元素而进行编码转换的情况:

        # 所有元素的长度都小于 64 字节
        redis> RPUSH blah "hello" "world" "again"
        (integer) 3
        
        redis> OBJECT ENCODING blah
        "ziplist"
        
        # 将一个 65 字节长的元素推入列表对象中
        redis> RPUSH blah "wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww"
        (integer) 4
        
        # 编码已改变
        redis> OBJECT ENCODING blah
        "linkedlist"
        
      • 列表对象因为保存的元素数量过多而进行编码转换的情况:

        # 列表对象包含 512 个元素
        redis> EVAL "for i=1,512 do redis.call('RPUSH', KEYS[1], i) end" 1 "integers"
        (nil)
        
        redis> LLEN integers
        (integer) 512
        
        redis> OBJECT ENCODING integers
        "ziplist"
        
        # 再向列表对象推入一个新元素,使得对象保存的元素数量达到 513 个
        redis> RPUSH integers 513
        (integer) 513
        
        # 编码已改变
        redis> OBJECT ENCODING integers
        "linkedlist"
        
    • 列表命令的实现

      命令ziplist 编码的实现方法linkedlist 编码的实现方法
      LPUSH调用 ziplistPush 函数, 将新元素推入到压缩列表的表头。调用 listAddNodeHead 函数, 将新元素推入到双端链表的表头。
      RPUSH调用 ziplistPush 函数, 将新元素推入到压缩列表的表尾。调用 listAddNodeTail 函数, 将新元素推入到双端链表的表尾。
      LPOP调用 ziplistIndex 函数定位压缩列表的表头节点, 在向用户返回节点所保存的元素之后, 调用 ziplistDelete 函数删除表头节点。调用 listFirst 函数定位双端链表的表头节点, 在向用户返回节点所保存的元素之后, 调用 listDelNode 函数删除表头节点。
      RPOP调用 ziplistIndex 函数定位压缩列表的表尾节点, 在向用户返回节点所保存的元素之后, 调用 ziplistDelete 函数删除表尾节点。调用 listLast 函数定位双端链表的表尾节点, 在向用户返回节点所保存的元素之后, 调用 listDelNode 函数删除表尾节点。
      LINDEX调用 ziplistIndex 函数定位压缩列表中的指定节点, 然后返回节点所保存的元素。调用 listIndex 函数定位双端链表中的指定节点, 然后返回节点所保存的元素。
      LLEN调用 ziplistLen 函数返回压缩列表的长度。调用 listLength 函数返回双端链表的长度。
      LINSERT插入新节点到压缩列表的表头或者表尾时, 使用 ziplistPush 函数; 插入新节点到压缩列表的其他位置时, 使用 ziplistInsert 函数。调用 listInsertNode 函数, 将新节点插入到双端链表的指定位置。
      LREM遍历压缩列表节点, 并调用 ziplistDelete 函数删除包含了给定元素的节点。遍历双端链表节点, 并调用 listDelNode 函数删除包含了给定元素的节点。
      LTRIM调用 ziplistDeleteRange 函数, 删除压缩列表中所有不在指定索引范围内的节点。遍历双端链表节点, 并调用 listDelNode 函数删除链表中所有不在指定索引范围内的节点。
      LSET调用 ziplistDelete 函数, 先删除压缩列表指定索引上的现有节点, 然后调用 ziplistInsert 函数, 将一个包含给定元素的新节点插入到相同索引上面。调用 listIndex 函数, 定位到双端链表指定索引上的节点, 然后通过赋值操作更新节点的值。
  • 哈希对象

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

      • ziplist 编码的哈希对象使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾, 因此:
        • 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
        • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。

      digraph {      label = "\n 图 8-9    ziplist 编码的 profile 哈希对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_HASH | encoding \n REDIS_ENCODING_ZIPLIST |  ptr | ... "];      ziplist [label = " 压缩列表 ", width = 4.0];      redisObject:ptr -> ziplist;  }

      digraph {      label = "\n 图 8-10    profile 哈希对象的压缩列表底层实现";      //      node [shape = record];      ziplist [label = " zlbytes | zltail | zllen |  "name" |  "Tom" |  "age" |  25 |  "career" |  "Programmer" | zlend "];      node [shape = plaintext];      edge [style = dashed];      kv1 [label = "第一个添加的键值对"];     kv1 -> ziplist:key1 [label = "键"];     kv1 -> ziplist:value1 [label = "值"];      kv2 [label = "第二个添加的键值对"];     kv2 -> ziplist:key2;     kv2 -> ziplist:value2;      kvN [label = "最新添加的键值对"];     kvN -> ziplist:key3;     kvN -> ziplist:value3;  }

      • hashtable 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存:
        • 字典的每个键都是一个字符串对象, 对象中保存了键值对的键;
        • 字典的每个值都是一个字符串对象, 对象中保存了键值对的值。

      digraph {      label = "\n 图 8-11    hashtable 编码的 profile 哈希对象";      rankdir = LR;      //      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_HASH | encoding \n REDIS_ENCODING_HT |  ptr | ... "];      dict [label = "  dict |  StringObject \n "age" |  StringObject \n "career" |  StringObject \n "name" ", width = 1.5];      age_value [label = "StringObject \n 25"];     career_value [label = "StringObject \n "Programmer""];     name_value [label = "StringObject \n "Tom""];      //      redisObject:ptr -> dict:head;      dict:key1 -> age_value;     dict:key2 -> career_value;     dict:key3 -> name_value;  }

    • 编码转换

      当哈希对象可以同时满足以下两个条件时, 哈希对象使用 ziplist 编码,不能满足这两个条件的哈希对象需要使用 hashtable 编码:

      1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节;
      2. 哈希对象保存的键值对数量小于 512 个;
      • 哈希对象因为键值对的键长度太大而引起编码转换的情况:

        # 哈希对象只包含一个键和值都不超过 64 个字节的键值对
        redis> HSET book name "Mastering C++ in 21 days"
        (integer) 1
        
        redis> OBJECT ENCODING book
        "ziplist"
        
        # 向哈希对象添加一个新的键值对,键的长度为 66 字节
        redis> HSET book long_long_long_long_long_long_long_long_long_long_long_description "content"
        (integer) 1
        
        # 编码已改变
        redis> OBJECT ENCODING book
        "hashtable"
        
      • 值的长度太大也引起编码转换

        # 哈希对象只包含一个键和值都不超过 64 个字节的键值对
        redis> HSET blah greeting "hello world"
        (integer) 1
        
        redis> OBJECT ENCODING blah
        "ziplist"
        
        # 向哈希对象添加一个新的键值对,值的长度为 68 字节
        redis> HSET blah story "many string ... many string ... many string ... many string ... many"
        (integer) 1
        
        # 编码已改变
        redis> OBJECT ENCODING blah
        "hashtable"
        
      • 哈希对象因为包含的键值对数量过多而引起编码转换的情况

        # 创建一个包含 512 个键值对的哈希对象
        redis> EVAL "for i=1, 512 do redis.call('HSET', KEYS[1], i, i) end" 1 "numbers"
        (nil)
        
        redis> HLEN numbers
        (integer) 512
        
        redis> OBJECT ENCODING numbers
        "ziplist"
        
        # 再向哈希对象添加一个新的键值对,使得键值对的数量变成 513 个
        redis> HMSET numbers "key" "value"
        OK
        
        redis> HLEN numbers
        (integer) 513
        
        # 编码改变
        redis> OBJECT ENCODING numbers
        "hashtable"
        
    • 哈希命令的实现
      命令ziplist 编码实现方法hashtable 编码的实现方法
      HSET首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用 ziplistPush 函数, 将值推入到压缩列表的表尾。调用 dictAdd 函数, 将新节点添加到字典里面。
      HGET首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。调用 dictFind 函数, 在字典中查找给定键, 然后调用 dictGetVal 函数, 返回该键所对应的值。
      HEXISTS调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。
      HDEL调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。
      HLEN调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。
      HGETALL遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。遍历整个字典, 用 dictGetKey 函数返回字典的键, 用 dictGetVal 函数返回字典的值。
  • 集合对象

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

      • intset 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面。

      digraph {      label = "\n 图 8-12    intset 编码的 numbers 集合对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_SET | encoding \n REDIS_ENCODING_INTSET |  ptr | ... "];     intset [label = "  intset | encoding \n INTSET_ENC_INT16 | length \n 3 |  contents "];      contents [label = " { 1 | 3 | 5 } "];      redisObject:ptr -> intset:head;     intset:contents -> contents;  }

      • hashtable 编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 NULL

      digraph {      label = "\n 图 8-13    hashtable 编码的 fruits 集合对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_SET | encoding \n REDIS_ENCODING_HT |  ptr | ... "];      dict [label = "  dict |  StringObject \n "cherry" |  StringObject \n "apple" |  StringObject \n "banana" ", width = 1.5];      redisObject:ptr -> dict:head;      node [shape = plaintext, label = "NULL"];      dict:apple -> nullX;     dict:banana -> nullY;     dict:cherry -> nullZ;  }

    • 编码的转换
      • 当集合对象可以同时满足以下两个条件时, 对象使用 intset 编码,不能满足这两个条件的集合对象需要使用 hashtable 编码:
        1. 集合对象保存的所有元素都是整数值;
        2. 集合对象保存的元素数量不超过 512 个;
    • 集合命令的实现
      命令intset 编码的实现方法hashtable 编码的实现方法
      SADD调用 intsetAdd 函数, 将所有新元素添加到整数集合里面。调用 dictAdd , 以新元素为键, NULL 为值, 将键值对添加到字典里面。
      SCARD调用 intsetLen 函数, 返回整数集合所包含的元素数量, 这个数量就是集合对象所包含的元素数量。调用 dictSize 函数, 返回字典所包含的键值对数量, 这个数量就是集合对象所包含的元素数量。
      SISMEMBER调用 intsetFind 函数, 在整数集合中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。调用 dictFind 函数, 在字典的键中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。
      SMEMBERS遍历整个整数集合, 使用 intsetGet 函数返回集合元素。遍历整个字典, 使用 dictGetKey 函数返回字典的键作为集合元素。
      SRANDMEMBER调用 intsetRandom 函数, 从整数集合中随机返回一个元素。调用 dictGetRandomKey 函数, 从字典中随机返回一个字典键。
      SPOP调用 intsetRandom 函数, 从整数集合中随机取出一个元素, 在将这个随机元素返回给客户端之后, 调用 intsetRemove 函数, 将随机元素从整数集合中删除掉。调用 dictGetRandomKey 函数, 从字典中随机取出一个字典键, 在将这个随机字典键的值返回给客户端之后, 调用 dictDelete 函数, 从字典中删除随机字典键所对应的键值对。
      SREM调用 intsetRemove 函数, 从整数集合中删除所有给定的元素。调用 dictDelete 函数, 从字典中删除所有键为给定元素的键值对。
  • 有序集合对象

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

      • ziplist 编码的有序集合对象使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。

      digraph {      label = "\n 图 8-14    ziplist 编码的有序集合对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_ZSET | encoding \n REDIS_ENCODING_ZIPLIST |  ptr | ... "];      ziplist [label = "压缩列表", width = 4.0];      redisObject:ptr -> ziplist;  }

      digraph {      label = "\n 图 8-15    有序集合元素在压缩列表中按分值从小到大排列";      //      node [shape = record];      ziplist [label = " zlbytes | zltail | zllen |  "banana" | <banana_price> 5.0 |  "cherry" | <cherry_price> 6.0 |  "apple" | <apple_price> 8.5 | zlend  "];      node [shape = plaintext];      banana [label = "分值最少的元素"];     cherry [label = "分值排第二的元素"];     apple [label = "分值最大的元素"];      //      edge [style = dashed]      banana -> ziplist:banana [label = "成员"];     banana -> ziplist:banana_price [label = "分值"];      cherry -> ziplist:cherry;     cherry -> ziplist:cherry_price;      apple -> ziplist:apple;     apple -> ziplist:apple_price;  }

      • skiplist 编码的有序集合对象使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典和一个跳跃表:
      typedef struct zset {
      
          zskiplist *zsl;
      
          dict *dict;
      
      } zset;
      

      digraph {      label = "\n 图 8-16    skiplist 编码的有序集合对象";      rankdir = LR;      node [shape = record];      redisObject [label = " redisObject | type \n REDIS_ZSET | encoding \n REDIS_ENCODING_SKIPLIST |  ptr | ... "];      zset [label = "  zset |  dict |  zsl "];      node [shape = plaintext];      dict [label = "..."];      zsl [label = "..."];      redisObject:ptr -> zset:head;     zset:dict -> dict;     zset:zsl -> zsl;  }

      digraph {      rankdir = LR;      //      node [shape = record];      zset [label = "  zset |  dict |  zsl "];      dict [label = "  dict | ... |  ht[0] | ... "];      ht0 [label = "  dictht | ... |  table | ... "];      table [label = "  StringObject \n "banana" |  StringObject \n "apple" |  StringObject \n "cherry" "];      node [shape = plaintext];      apple_price [label = "8.5"];     banana_price [label = "5.0"];     cherry_price [label = "6.0"];      //      zset:dict -> dict:head;     dict:ht0 -> ht0:head;     ht0:table -> table:head;      table:apple -> apple_price;     table:banana -> banana_price;     table:cherry -> cherry_price;      //      node [shape = record, width = "0.5"];      //      l [label = "  header |  tail | level \n 5 | length \n 3 "];      subgraph cluster_nodes {          style = invisible;          header [label = "  L32 | ... |  L5 |  L4 |  L3 |  L2 |  L1 "];          bw_null [label = "NULL", shape = plaintext];          level_null [label = "NULL", shape = plaintext];          A [label = "  L4 |  L3 |  L2 |  L1 |  BW | 5.0 | StringObject \n "banana" "];          B [label = "  L2 |  L1 |  BW | 6.0 | StringObject \n "cherry" "];          C [label = "  L5 |  L4 |  L3 |  L2 |  L1 |  BW | 8.5 | StringObject \n "apple" "];      }      subgraph cluster_nulls {          style = invisible;          n1 [label = "NULL", shape = plaintext];         n2 [label = "NULL", shape = plaintext];         n3 [label = "NULL", shape = plaintext];         n4 [label = "NULL", shape = plaintext];         n5 [label = "NULL", shape = plaintext];      }      //      l:header -> header;     l:tail -> C;      header:l32 -> level_null;     header:l5 -> C:l5;     header:l4 -> A:l4;     header:l3 -> A:l3;     header:l2 -> A:l2;     header:l1 -> A:l1;      A:l4 -> C:l4;     A:l3 -> C:l3;     A:l2 -> B:l2;     A:l1 -> B:l1;      B:l2 -> C:l2;     B:l1 -> C:l1;      C:l5 -> n5;     C:l4 -> n4;     C:l3 -> n3;     C:l2 -> n2;     C:l1 -> n1;      bw_null -> A:backward -> B:backward -> C:backward [dir = back];      zset:zsl -> l:header;      // HACK: 放在开头的话 NULL 指针的长度会有异样     label = "\n 图 8-17    有序集合元素同时被保存在字典和跳跃表中";  }

    • 编码的转换
      • 当有序集合对象可以同时满足以下两个条件时, 对象使用 ziplist 编码,不能满足以上两个条件的有序集合对象将使用 skiplist 编码:

        1. 有序集合保存的元素数量小于 128 个;
        2. 有序集合保存的所有元素成员的长度都小于 64 字节;
        • 有序集合对象因为包含了过多元素而引发编码转换的情况:
        # 对象包含了 128 个元素
        redis> EVAL "for i=1, 128 do redis.call('ZADD', KEYS[1], i, i) end" 1 numbers
        (nil)
        
        redis> ZCARD numbers
        (integer) 128
        
        redis> OBJECT ENCODING numbers
        "ziplist"
        
        # 再添加一个新元素
        redis> ZADD numbers 3.14 pi
        (integer) 1
        
        # 对象包含的元素数量变为 129 个
        redis> ZCARD numbers
        (integer) 129
        
        # 编码已改变
        redis> OBJECT ENCODING numbers
        "skiplist"
        
        • 有序集合对象因为元素的成员过长而引发编码转换的情况:
        # 向有序集合添加一个成员只有三字节长的元素
        redis> ZADD blah 1.0 www
        (integer) 1
        
        redis> OBJECT ENCODING blah
        "ziplist"
        
        # 向有序集合添加一个成员为 66 字节长的元素
        redis> ZADD blah 2.0 oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo
        (integer) 1
        
        # 编码已改变
        redis> OBJECT ENCODING blah
        "skiplist"
        
    • 有序集合命令的实现方法

      命令ziplist 编码的实现方法zset 编码的实现方法
      ZADD调用 ziplistInsert 函数, 将成员和分值作为两个节点分别插入到压缩列表。先调用 zslInsert 函数, 将新元素添加到跳跃表, 然后调用 dictAdd 函数, 将新元素关联到字典。
      ZCARD调用 ziplistLen 函数, 获得压缩列表包含节点的数量, 将这个数量除以 2 得出集合元素的数量。访问跳跃表数据结构的 length 属性, 直接返回集合元素的数量。
      ZCOUNT遍历压缩列表, 统计分值在给定范围内的节点的数量。遍历跳跃表, 统计分值在给定范围内的节点的数量。
      ZRANGE从表头向表尾遍历压缩列表, 返回给定索引范围内的所有元素。从表头向表尾遍历跳跃表, 返回给定索引范围内的所有元素。
      ZREVRANGE从表尾向表头遍历压缩列表, 返回给定索引范围内的所有元素。从表尾向表头遍历跳跃表, 返回给定索引范围内的所有元素。
      ZRANK从表头向表尾遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。从表头向表尾遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。
      ZREVRANK从表尾向表头遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。从表尾向表头遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。
      ZREM遍历压缩列表, 删除所有包含给定成员的节点, 以及被删除成员节点旁边的分值节点。遍历跳跃表, 删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。
      ZSCORE遍历压缩列表, 查找包含了给定成员的节点, 然后取出成员节点旁边的分值节点保存的元素分值。直接从字典中取出给定成员的分值。

内存回收与对象共享

  • Redis 在自己的对象系统中构建了一个引用计数reference counting)技术实现的内存回收机制, 通过这一机制, 程序可以通过跟踪对象的引用计数信息, 在适当的时候自动释放对象并进行内存回收。

    typedef struct redisObject {
    
        // ...
    
        // 引用计数
        int refcount;
    
        // ...
    
    } robj;
    
  • 对象的引用计数信息会随着对象的使用状态而不断变化:

    • 在创建一个新对象时, 引用计数的值会被初始化为 1
    • 当对象被一个新程序使用时, 它的引用计数值会被增一;
    • 当对象不再被一个程序使用时, 它的引用计数值会被减一;
    • 当对象的引用计数值变为 0 时, 对象所占用的内存会被释放。
  • 在 Redis 中, 让多个键共享同一个值对象需要执行以下两个步骤:

    1. 将数据库键的值指针指向一个现有的值对象;
    2. 将被共享的值对象的引用计数增一。

    digraph {      label = "\n 图 8-21    被共享的字符串对象";      rankdir = LR;      key_a [label = "键 A", shape = box, width = 1.5];     key_b [label = "键 B", shape = box, width = 1.5];      redisObject [label = "  redisObject | type \n REDIS_STRING | encoding \n REDIS_ENCODING_INT |  ptr | refcount \n 2 | ... ", shape = record];      node [shape = plaintext];      number [label = "100"]      redisObject:ptr -> number;      key_a -> redisObject:head;     key_b -> redisObject:head;  }

对象的空转时长

  • OBJECT IDLETIME 命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的 lru 时间计算得出的:

    redis> SET msg "hello world"
    OK
    
    # 等待一小段时间
    redis> OBJECT IDLETIME msg
    (integer) 20
    
    # 等待一阵子
    redis> OBJECT IDLETIME msg
    (integer) 180
    
    # 访问 msg 键的值
    redis> GET msg
    "hello world"
    
    # 键处于活跃状态,空转时长为 0
    redis> OBJECT IDLETIME msg
    (integer) 0
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值