一文了解Redis高级数据结构

841 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

高级数据结构

基础对象 object

定义

#define LRU_BITS 24



typedef struct redisObject {

    // 存储对象类型

    unsigned type:4;

    // 记录底层数据结构

    unsigned encoding:4;

    // 可能是lru/lfu

    // 记录对象最后访问时间/低8位记录对象访问频率,高16位记录对象访问时间

    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). */

    int refcount;

    void *ptr;

    

} robj;

补充

  • 位域

采用了c的位域来节省内存,简便操作

在移植的情况下需要适当的字节填充

  • 时间计算

获取时间的函数属于系统调用,比较耗费资源

redis采用缓存的方式在定时任务中定期更新。

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {

     //....

    /* Update the time cache. */

    updateCachedTime();

    //....

}

更新的时候如果在lruclock中使用了atomicGet可能是因为别的线程也会用到该时间,例如集群状态

字符串 string

定义

分了三种类型

  • Int

    • 小于20字节(一个传给长整型范围内)且不是浮点数
    • 利于节省空间(比如123456等,如果是int需要4字节,但是是string需要6字节)
  • Embstr

    • 长度小于等于44字节的字符串,包括浮点数
    • 主要是适应jemalloc分配64k的arean
    • 如果修改了会被转为raw。且不会回退
  • Raw

    • 长度大于44字节的字符串,包括浮点数

补充

tryObjectEncoding

类型特点优点缺点
embstr1.只分配一次内存空间,因此robj和sds是连续的;2.只读;3.Embstr字符串需要修改时,会转成raw,之后一直为raw1.创建和删除只需要一次; 2.寻找速度快1.重分配涉及到robj和sds整个对象,因此embstr是只读的
raw1.robj和sds非连续; 2.可修改
  • append会修改字符串底层类型

  • 字符串长度不能大于512M
static int checkStringLength(client *c, long long size) {

    if (size > 512*1024*1024) {

        addReplyError(c,"string exceeds maximum allowed size (512MB)");

        return C_ERR;

    }

    return C_OK;

}

应用场景

  • 缓存功能:mysql存储,redis做缓存
  • 计数器:如点赞次数,视频播放次数
  • 限流:见基于redis的分布式限流

列表 list

只有quicklist一种类型,是一个双向链表

定义

/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.

 * We use bit fields keep the quicklistNode at 32 bytes.

 * count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).

 * encoding: 2 bits, RAW=1, LZF=2.

 * container: 2 bits, NONE=1, ZIPLIST=2.

 * recompress: 1 bit, bool, true if node is temporarry decompressed for usage.

 * attempted_compress: 1 bit, boolean, used for verifying during testing.

 * extra: 10 bits, free for future use; pads out the remainder of 32 bits */

typedef struct quicklistNode {

    //双向链表的前节点

    struct quicklistNode *prev;

    //双向链表的后节点

    struct quicklistNode *next;

    ///不设置压缩数据参数recompress时指向一个ziplist结构

    //设置压缩数据参数recompress指向quicklistLZF结构

    unsigned char *zl;

    //压缩列表ziplist的总长度

    unsigned int sz;             /* ziplist size in bytes */

    //每个ziplist中entry总个数

    unsigned int count : 16;     /* count of items in ziplist */

    //表示是否采用了LZF压缩算法压缩quicklist节点,1表示压缩过,2表示没压缩,占2 bits长度

    unsigned int encoding : 2;   /* RAW==1 or LZF==2 */

    //表示是否启用ziplist来进行存储

    unsigned int container : 2;  /* NONE==1 or ZIPLIST==2 */

    //记录该节点之前是否被压缩过

    unsigned int recompress : 1; /* was this node previous compressed? */

    //测试是使用

    unsigned int attempted_compress : 1; /* node can't compress; too small */

    //额外扩展位,占10bits长度

    unsigned int extra : 10; /* more bits to steal for future usage */

} quicklistNode;



/*

当指定使用lzf压缩算法压缩ziplist的entry节点时,

quicklistNode结构的zl成员指向quicklistLZF结构

quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.

 * 'sz' is byte length of 'compressed' field.

 * 'compressed' is LZF data with total (compressed) length 'sz'

 * NOTE: uncompressed length is stored in quicklistNode->sz.

 * When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */

typedef struct quicklistLZF {

    ////表示被LZF算法压缩后的ziplist的大小

    unsigned int sz; /* LZF size in bytes*/

    //压缩之后的数据,柔性数组

    char compressed[];

} quicklistLZF;



/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.

 * 'count' is the number of total entries.

 * 'len' is the number of quicklist nodes.

 * 'compress' is: -1 if compression disabled, otherwise it's the number

 *                of quicklistNodes to leave uncompressed at ends of quicklist.

 * 'fill' is the user-requested (or default) fill factor. */

typedef struct quicklist {

    //链表表头

    quicklistNode *head;

    //链表表尾

    quicklistNode *tail;

    //所有quicklistnode节点中所有的entry个数

    unsigned long count;        /* total count of all entries in all ziplists */

    //quicklistnode节点个数,也就是quicklist的长度

    unsigned long len;          /* number of quicklistNodes */

    //单个节点的填充因子,也就是ziplist的大小

    int fill : 16;              /* fill factor for individual nodes */

    //保存压缩程度值,配置文件设定,占16bits,0表示不压缩

    unsigned int compress : 16; /* depth of end nodes not to compress;0=off */

} quicklist;



//quicklist中quicklistNode的entry结构

typedef struct quicklistEntry {

    //指向所属的quicklist指针

    const quicklist *quicklist;

    //指向所属的quicklistNode节点的指针

    quicklistNode *node;

    //指向当前ziplist结构的指针

    unsigned char *zi;

    //指向当前ziplist结构的字符串value成员

    unsigned char *value;

    //指向当前ziplist结构的整型value成员

    long long longval;

    //当前ziplist结构的字节数

    unsigned int sz;

    //保存相对ziplist的偏移量

    int offset;

} quicklistEntry;

优点

  • 权衡了数组和双向链表。解决了双向链表对内存不友好造成大量内存碎片,数组插入删除复杂度高的问题。综合来说类似于c++的deque

分析

当list的len=1的时候会退化为ziplist,如果找不到一块较大的连续内存会oom,当ziplist=1的时候会退化为双向链表,对内存不友好。折合来选择靠字段fill

  • fill

16位,用来存放list-max-ziplist-size参数的值,默认为-2

如果为-2代表ziplist容量为8k

如果是正值,表示每个quicklist上ziplist的entry个数

//在原来节点的基础上又需要新添加一个

REDIS_STATIC int _quicklistNodeAllowInsert(const quicklistNode *node,

                                           const int fill, const size_t sz) {

     //...

    //这里这里,count表示当前entry的总个数

    else if ((int)node->count < fill)

        return 1;

     //....

}
  • compress

16位,表示节点压缩深度设置,存放list-compress-depth,默认为0

当数据很多的时候最容易被访问的是两端数据。list提供了选项将中间的数据节点(node级别)进行压缩来节省内存空间

  • 0 -- 都不压缩
  • 1 -- 表示quicklist两端各有一个节点不压缩,中间节点压缩
  • 2 -- 两端各两个节点不压缩,中间节点压缩
  • 3 -- 两端各三个节点不压缩,中间节点压缩

...

  • recompress

对接点暂时解压,在后面某个时刻再将其压缩,减少压缩解压的次数

_quicklistNodeAllowInsert(quicklist->head, quicklist->fill, sz)

....

特性

  • 阻塞和非阻塞

    • 阻塞

当给定的key不存在时,BLPOP或BRPOP命令会被阻塞连接,当另一个client对这些key执行push会解除调用BLPOP和BRPOP的阻塞

就是判断超时或有数据,否则不返回

应用场景

  • 常见数据结构

    • lpush+lpop=Stack(栈)
    • lpush+rpop=Queue(队列)
    • lpush+ltrim=Capped Collection(有限集合)
    • lpush+brpop=Message Queue(消息队列)
  • 文章列表

    • 组合的方式:文章用hash存储,文章列表用list存储

哈希 hash

权衡了连续内存和不连续内存的利弊,使用ziplist和dict两种数据结构存储

当ziplist的entry个数大于512,或value字节数超过64都会转为dict

  • ziplist转为dict的不可逆的
  • 尽量使用ziplist,且长度尽量控制在1000内。长列表存取数据时间复杂度O(N)会导致cpu消耗严重
  • 两个参数可以在配置文件中修改
  • 当ziplist作为底层对象,保存不是顺序保存的,时间复杂度为O(N)

应用场景

set user:1:name tom

set user:1:age 23

set user:1:city beijing



set user:1 serialize(userInfo)



hmset user:1 name tom age 23 city beijing
  • 存储对象

    • 原生字符串

      • 优点:直观,每个属性可以独立更新
      • 缺点:存储大量的键,内存消耗大,用户信息内聚性差,生产较少使用
    • 存储序列化后的字符串(结合pb使用,使用广泛)

      • 优点:简化编程,提高内存使用率
      • 缺点:序列化和反序列化有一定开销
    • 哈希

      • 优点:简化编程,每个用户的属性用一堆field-value,但是只用一个key存储
      • 缺点:要控制在ziplist和hashtable两种编码的转换,hashtable消耗内存更多。

集合 set

也使用两种方式存储

intset当entry个数超过512或集合对象中存在非整数值,会转换为dict

robj *setTypeCreate(sds value){

    if (isSdsRepresentableAsLongLong(value, NULL) == C_OK)

        return createIntsetObject();

    return createSetObject();

}



----> string2ll int
  • 转换操作不可逆
  • set不允许重复
  • set支持交并补,社交场景,标签用的多
  • 参数set-max-intset-entries可以在配置文件中修改
  • dict作为底层对象,value为NULL
  • intset查找时间复杂度为O(logN)

应用场景

sadd user:1:tags tag1 tag2 tag5

sadd user:2:tags tag2 tag3 tag5

...

sadd user:k:tags tag1 tag2 tag4

---------

sadd tag1:users user:1 user:3

sadd tag2:users user:1 user:2 user:3

...

sadd tagk:users user:1 user:2

...

---------

sinter user:1:tags user:2:tags

---------

spop/srandmember
  • 标签,主要用于社交,尽量保证在一个事务内完成

    • 给用户添加标签
    • 给标签标记用户
    • 计算用户共同感兴趣的标签
  • 抽奖随机数

有序集合 zset

比较复杂,单独定义了一个结构体

也有两个底层:ziplist和skiplist

typedef struct zset {

    dict *dict;//字段

    zskiplist *zsl;//线段跳表

} zset;

ziplist当entry个数超过238,或value字节数大于64会转换为skiplist

一个插入排序

如果大于score会插入前面,如果相等,再比较元素。如果再大,就在前面插入

/* Insert (element,score) pair in ziplist. This function assumes the element is

 * not yet present in the list. */

unsigned char *zzlInsert(unsigned char *zl, sds ele, double score) {

    unsigned char *eptr = ziplistIndex(zl,0), *sptr;

    double s;



   // 排序的过程

    while (eptr != NULL) {

        sptr = ziplistNext(zl,eptr);

        serverAssert(sptr != NULL);

        s = zzlGetScore(sptr);

        if (s > score) {

            /* First element with score larger than score for element to be

             * inserted. This means we should take its spot in the list to

             * maintain ordering. */

             // 元素,score

            zl = zzlInsertAt(zl,eptr,ele,score);

            break;

        } else if (s == score) {

            /* Ensure lexicographical ordering for elements. */

            if (zzlCompareElements(eptr,(unsigned char*)ele,sdslen(ele)) > 0) {

                zl = zzlInsertAt(zl,eptr,ele,score);

                break;

            }

        }





        /* Move to next element. */

        eptr = ziplistNext(zl,sptr);

    }





    /* Push on tail of list when it was not yet inserted. */

    if (eptr == NULL)

        zl = zzlInsertAt(zl,NULL,ele,score);

    return zl;

}

转换

为什么skiplist基础上要创建dict

加快速度

需要获取某个元素的score的时候,skiplist查询的时间复杂度为O(lgN),dict时间复杂度为O(1)

当底层为ziplist的时候时间复杂度是O(N)

int zsetScore(robj *zobj, sds member, double *score) {

    if (!zobj || !member) return C_ERR;



    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {

        if (zzlFind(zobj->ptr, member, score) == NULL) return C_ERR;

    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {

        zset *zs = zobj->ptr;

        dictEntry *de = dictFind(zs->dict, member);

        if (de == NULL) return C_ERR;

        *score = *(double*)dictGetVal(de);

    } else {

        serverPanic("Unknown sorted set encoding");

    }

    return C_OK;

}



//重点的zzlFind

unsigned char *zzlFind(unsigned char *zl, sds ele, double *score) {

    unsigned char *eptr = ziplistIndex(zl,0), *sptr;

    // 只是一个while循环遍历,时间复杂度为O(n)

    while (eptr != NULL) {

        sptr = ziplistNext(zl,eptr);

        serverAssert(sptr != NULL);



        if (ziplistCompare(eptr,(unsigned char*)ele,sdslen(ele))) {

            /* Matching element, pull out score. */

            if (score != NULL) *score = zzlGetScore(sptr);

            return eptr;

        }



        /* Move to next element. */

        eptr = ziplistNext(zl,sptr);

    }

    return NULL;

}
  • skiplist和dict的共享元素和score的(指针复制)
  • 转换操作是不可逆的
  • 两个参数可以在配置文件中修改
  • zset不允许重复

应用场景

zadd user:ranking:2016_03_15 3 mike



zincrby user:ranking:2016_03_15 1 mike



zrem user:ranking:2016_03_15 mike



zrevrange user:ranking:2016_03_15 0 9



hgetall user:info:tom

zscore user:ranking:2016_03_15 mike

zrank user:ranking:2016_03_15 mike
  • 优先队列

    • 支持排序
  • 排行榜系统:视频网站上需要对用户上传的视频进行排行榜排序,排行榜是多维的,按照播放量,时间,点赞数。。。

    • 添加用户赞数
    • 有人点赞
    • 取消点赞
    • 展示获赞数前十的用户
    • 展示用户信息以及分数和排名
数据类型适用场景备注
字符串(string)缓存;计算器简单型的。如set stunum studentInfo。 计数器如限流
列表(list)lpush+lpop=Stack(栈) lpush+rpop=Queue(队列) lpush+ltrim=Capped Collection(有限集合) lpush+brpop=Message Queue(消息队列)如阻塞队列,关注列表
哈希(hash)对象属性(尤其不定长的)如缓存studentInfo,hmset stunum stunum 1 stuname dinghaha age 18
集合(set)适用社交场景赞/踩、粉丝、共同好友/喜好、推送
有序集合(set)排行榜;优先队列;缓存相关的元数据(比如按照排序的挑战)

参考:

《Redis设计与实现》

硬核课堂

Redis源码分析(二)