> 文档中心 > 详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

  前面我们详细探讨了Redis的部分数据结构,如SDS、链表、压缩列表、字典以及跳跃表的实现。Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串对象、列表对象、集合对象、有序集合对象等。而每一个对象都由一个RedisObject表示。如下。

typedef struct redisObject {// 类型,STRING、LIST、HASH、SET、ZSET...    unsigned type:4;    // 编码方式,INT、EMBSTR、RAW、HT、LINKEDLIST、ZIPLIST、INTSET、SKIPLIST...    unsigned encoding:4;    unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or* LFU data (least significant 8 bits frequency* and most significant 16 bits access time). */    // Redis实现的引用计数法用于实现对象内存的回收    int refcount;    // 指向底层实现数据结构的指针    void *ptr;} robj;

下面我们分别聊聊几种常见对象的实现

一、STRING 字符串对象

这个对象可以说是我们最常见常用的对象了,字符串对象有三种编码方式INT、RAW、EMBSTR,且对应着不同的实现。

  1. INT
    当字符串对象保存的是整数值的时候,且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面,并将这个字符串对象的编码设置为INT。
    详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

比如我们可以用在REDIS中存入一个数值520。

> SET LOVE 520OK> OBJECT ENCODING LOVEint
  1. RAW
    如果字符串对象保存的是一个字符串值,且长度大于39字节,那么字符串对象将使用SDS来保存这个字符串的值,并且将对象的编码设置为RAW。
    如,我们输入一个大于39字符的值
> SET longValue "this is test for love , and what do you think test value"OK> STRLEN longValue56> OBJECT ENCODING longValueraw

详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

  1. EMBSTR
    当然,如果保存的为字符串值,却字符串小于39字节,那么将会采用embstr的编码方式来进行保存。embstr编码也是由sds来进行表示的,embstr只是专门用于保存短字符串的一种优化编码方式。区别就是:raw编码会调用两次内分配函数来分别创建redisObject结构和sds结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sds两个结构。而且在释放内存的时候也只用调用一次内存释放函数。
    如我们插入一个小于39字节的字符串:
> SET shortValue "test value"OK> STRLEN shortValue10> OBJECT ENCODING shortValueembstr

当然我们在操作字符串的时候,这三种编码方式也会进行相应的转换,比如,我们给LOVE键的值拼接上字符串,那么编码格式将自动转为raw

> OBJECT ENCODING LOVEint> APPEND LOVE "append test"14> OBJECT ENCODING LOVEraw

我们,可以看到字符串对象的编码格式由int转为乐raw,但为什么不是embstr呢?LOVE的值明明小于39字节且为字符串值。
实际上,Redis并没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串有)。当我们对embstr编码的字符串对象进行任何的修改命令时,程序会先将embstr编码转换为raw然后再进行修改命令。换句话说,embstr编码的字符串对象是只读的。我们可以通过以下操作来进行验证:

> OBJECT ENCODING shortValueembstr> STRLEN shortValue10> APPEND shortValue " test"15> STRLEN shortValue15> OBJECT ENCODING shortValueraw

我们可以看出,当我们给embstr编码的shortValue键追加了字符串后,虽然总长度还是小于39字节的,但是编码格式仍然从embstr变为乐raw.

二、LIST 列表对象

关于列表对象,其实我们在之前聊链表、压缩列表的时候就已经提到过了。列表对象的编码实现可以是ziplist或者linkedlist。
当列表对象可以同时满足以下两个条件的时候,列表对象使用ziplist编码实现:

  1. 列表对象保存的所有字符串元素的长度都小于64字节
  2. 列表对象保存的元素数量小于512个
    当不能同时满足以上两个条件的时候,将采用linkedlist编码

上面两个条件的限制值都是可以修改的(list-max-ziplist-value、list-max-ziplist-entries)
当然,这只是3.2之前的实现,3.2之后,Redis结合ziplist和linkedlist的优点,开发出了一种新的结构:quicklist。我们可以实操看看:

> RPUSH zipLIstValue "hello" "test" "value"3> OBJECT ENCODING zipLIstValuequicklist

Redis对于quikList的具体定义如下

typedef struct quicklistNode { struct quicklistNode *prev;  // 上一个 ziplist  struct quicklistNode *next;  // 下一个 ziplist  unsigned char *zl;    // 数据指针,指向 ziplist 结构,或者 quicklistLZF 结构 unsigned int sz;      // ziplist 占用内存长度(未压缩) unsigned int count : 16;     // ziplist 记录数量 unsigned int encoding : 2;   // 编码方式,1 表示 ziplist ,2 表示 quicklistLZF unsigned int container : 2;   unsigned int recompress : 1;  // 临时解压,1 表示该节点临时解压用于访问 unsigned int attempted_compress : 1;  unsigned int extra : 10;   } quicklistNode;typedef struct quicklistLZF { unsigned int sz;    // 压缩数据长度 char compressed[];  // 压缩数据    } quicklistLZF;typedef struct quicklist { quicklistNode *head; // 列表头部 quicklistNode *tail; // 列表尾部 unsigned long count; // 记录总数 unsigned long len;   // ziplist 数量 int fill : 16;// ziplist 长度限制,每个 ziplist 节点的长度(记录数量/内存占用)不能超过这个值 unsigned int compress : 16; // 压缩深度,表示 quicklist 两端不压缩的 ziplist 节点的个数,为 0 表示所有 ziplist 节点都不压缩    } quicklist;

我们可以看出快速列表(quilList)是linkedList与ziplist的结合,它包含多个内存不连续的节点,每个节点本身就是一个ziplist。

三、HASH 哈希对象

  哈希对象的编码实现可以是ziplist或者hashtable。那么什么时候采用ziplist,什么时候采用hashtable呢?
当hash对象可以同时满足以下两个条件的时候hash对象使用ziplist编码:

  1. hash对象保存的所有键值对键和值的字符串长度都小于64字节
  2. 哈希对象保存的键值对数量小于512个
    当不能同时满足上面两个条件的时候,hash对象将采用hashtable编码实现。

上面两个值也是可以改变的(hash-max-ziplist-value、hash-max-ziplist-entries)
我们分别来瞅瞅这两个编码格式的情况:

  • ziplist
    当使用压缩列表实现hash对象的时候,每当有新的键值对加入到hash对象的时候,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾。
    所以保存的键值对的键节点和值节点总是紧挨在一起,保存键的节点在前,保存值的节点在后。先添加到hash对象中的键值对会被放到压缩列表的表头方向,而后来添加到hash对象中的键值对会被放在压缩列表的表尾方向。
    我们来实操看一下:
> HSET HT name "lordky"1> HSET HT age 181> OBJECT ENCODING HTziplist

我们可以看出,HT键值采用的是ziplist编码。其存储结构如图所示:详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

  • hashtable
    hashtable编码的hash对象底层采用字典实现,hash对象的每一个键值对都适用一个字典键值对来保存。
    例如,我们插入一个超过64字节的键值对查看编码可以看出,采用的是hashtable编码格式
> HSET HTL name "king of test long name and test long value and test long long test"1> OBJECT ENCODING HTLhashtable

存储结构如图所示:
详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

四、SET 集合对象

集合对象的实现也是有两种编码格式:intset以及hashtable
当集合对象同时满足以下两个条件的时候,采用intset(整数集合)编码

  1. 集合对象保存的所有元素都是整数值
  2. 集合对象保存的元素数量不超过512个
    不能满足这两个条件的时候采用hashtable编码

当然,第二个条件的值是可以修改的(set-max-intset-entries)
例如,当我们添加的3个整数值元素的时候,集合对象就是采用的intset编码:

> SADD setINT 1 2 33> OBJECT ENCODING setINTintset

保存的数据结构如下图所示:
详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH
当我们往集合对象中插入字符串时,将会转换为hashtable编码:

> OBJECT ENCODING setINTintset> SADD setINT "king"1> SMEMBERS setINT213test> OBJECT ENCODING setINThashtable

保存的结构示意图,如图所示:
详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

五、ZSET 有序集合对象

有序集合对象也有两种编码格式:ziplist以及skiplist
当有序集合对象同时满足以下两个条件的时候,对象使用ziplist编码:

  1. 有序集合对象保存的元素数量小于128个
  2. 有序集合对象保存的所有元素成员的长度都小于64字节

当然,以上两个条件的值都可以修改的(zset-max-ziplist-entries、zset-max-ziplist-value)

  • ziplist编码
    其实和hash对象的ziplsit编码一样,采用压缩列表实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),第二个元素则保存元素的分值(score)。压缩列表内的集合元素按照分值从小到大的顺序排列。
> ZADD age 20 kevin 18 lordky2> OBJECT ENCODING ageziplist

保存的结构示意图如下所示:
详述常见的Redis对象_STRING、LIST、SET、ZSET、HASH

  • skiplist
    我们可以试着往集合对象中插入大于64字节的元素,可以看出其编码值为skiplist
> ZADD age 1 "this is test for long key to change ziplist to skiplist test long long long long long long long long"1> OBJECT ENCODING ageskiplist