redis源码分析(三):基本数据结构

redis中使用的数据结构有:

  • dict 字典,就是个哈希表,实现和HashMap类似,不做阐述;不同的是在哈希表resize()的时候是分步执行的,后续篇幅再说明。

  • sds 很多项目都对自己的字符串进行了封装,作用类似于leveldb的slice。

  • linkedlist 双端链表,迭代器的实现是通过链表的pre和next实现的,是个BidirctionalIterator。代码中只实现了ForwardIterator的功能。

  • zipmap 已不再使用了

  • inset 一个紧致的有序整型数组。

  • ziplist 也是一个链表,但是它的内存是连续的,在数据量小的时候使用,增长到一定的大小会转化成list或者skiplist

  • skiplist 跳跃表,实现在t_zset.c中

本文只阐述inset,ziplist,skiplist三种数据结构。

1.ziplist

直接借用代码注释上ziplist的存储结构:

/*
area        |<---- ziplist header ---->|<----------- entries ------------->|<-end->|

size          4 bytes  4 bytes  2 bytes    ?        ?        ?        ?     1 byte
            +---------+--------+-------+--------+--------+--------+--------+-------+
component   | zlbytes | zltail | zllen | entry1 | entry2 |  ...   | entryN | zlend |
            +---------+--------+-------+--------+--------+--------+--------+-------+
                                       ^                          ^        ^
address                                |                          |        |
                                ZIPLIST_ENTRY_HEAD                |   ZIPLIST_ENTRY_END
                                                                  |
                                                        ZIPLIST_ENTRY_TAIL
  • zlbytes 记录了整个ziplist占用的内存大小;
  • zltail 记录尾节点的相对于ziplist实例指针的偏移量;
  • zllen 是整个list的节点数量的无符号数,占2个字节,可以最大表示2**16-1,实际使用保留了0xffff ffff ,即大于等于这个数的时候,已经不能通过此值来取得节点的数量了;
  • zlend 固定为0xff。
  • entry 的结构并不是代码中给的结构体zlentry。而是如下结构:
size              1/5 bytes      1/2/5 bytes    ?   
            +---------------+---------------+-------+
component   | pre_entry_len | cur_entry_len | value |
            +---------------+---------------+-------+

这是个变长的结构,pre节点小于254则pre_entry_len占用1字节,否则5字节,为什么不是255字节呢?255会和zlend产生冲突;cur_entry_len中包含了编码信息(以字符串编码还是数字,数字编码能减少内存长度)和本节点的长度信息,数字格式编码固定占用1字节;字符串格式编码数据小于127字节占1字节,小于0x3fff 占用2字节,否则5个字节。

因此ziplist有如下特点:

  • 内存连续,数据紧凑。
  • 查找效率o(n)
  • 双端查询,可前后遍历,类似于nodelist.

了解了数据结构,思考下如何对其增加一个节点p,p的大小为sp。

  • 1.ziplist长度发生变化,zlbytes要增加entry的内存大小;
  • 2.zltail也要做相应改变,如果插入的是尾节点,则zltail等于ziplist到新节点p的距离,否则是原值加上新节点p的长度。
  • 3.内存大小发生改变,需要realloc个更大的内存。
  • 4.对于在新节点以后的节点,后移sp个单位。接下来要更新p以后的节点内容:
  • 4.1如果p的前一个节点的长度和p节点的长度一样长,那么皆大欢喜。只需要更新p节点后一个节点的pre_entry_len值;
  • 4.2如果比p节点的长度大(5字节对1字节),也ok,存下来,浪费4个字节内存。
  • 4.3如果比p节点的长度小(1字节对5字节),意味着p以后的节点长度会发生变化,增加4个字节来存储p的长度,zltail和ziplist也要同时加4;同时由于p->next节点长度发生变化,p->next->next也要去做一次判断,需要重复步骤4,直到不满足4.3的情况为止。

经过以上步骤,就可以大概还原redis中的代码实现了,也可以看出来ziplist的增删代价是很大的,改变一个节点的复杂度最差是(n^2);一个节点改动,可能牵一发而动全身,所以ziplist是不适合大数据量的存储的。

2.inset

inset 实际是一个整数数组,结构如下:

typedef struct intset {
    
    uint32_t encoding;

    uint32_t length;

    int8_t contents[];

} intset;

其中encoding是编码方式,有16位,32位,64位三种取值;length是inset集合中保存的元素个数,contents为保存的数据。下面是inset的特点和实现方式.

  • 1.初始化的inset是16位编码的,如果inset里面只保存了16位的数字,那么这个编码方式将保持不变。

  • 2.inset是有序不重复的,从小到大排列,每次插入/删除会触发inset的大小调整,length+1。

  • 3.如果插入了比当前编码范围更大的数字,会触发intsetUpgradeAndAdd(),下面以16位-> 32位扩展为例。

  • 3.1 重新调整contents大小,大小为原(size+1)*2,更新encoding的值为INTSET_ENC_INT32;

  • 3.2 新数字肯定是位于数组开头或者结尾,如果是在结尾,将原数组的每个数从原位置pos移到pos2的位置,否则是(pos+1)2;

  • 3.3 插入新的数据,intsetUpgradeAndAdd()过程结束

  • 4.删除不改变inset的编码方式,即如果原inset为32位,删光了inset里的16位不能表示的数字以后,inset也不会恢复到16位,代价太大,即intsetUpgradeAndAdd()的过程是不可逆的。

  • 5.在inset中查找一个数采用二分查找。

根据这些特点,写出inset的实现,想必不难,inset的特点是在存储的数据为小值的时候,占用的内存较小,但是只要插入了一个64位的数据,那么inset就没有优势了,插入和删除的时间复杂度都是o(n),因此不适合做大数量的存储。

3.skiplist

跳表是一个特殊的有序链表,盗一张网图吧。

跳表结构

结构如下

typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;

    // 表中节点的数量
    unsigned long length;

    // 表中层数最大的节点的层数
    int level;

} zskiplist;

节点的结构

typedef struct zskiplistNode {

    // 成员对象
    robj *obj;

    // 分值
    double score;

    // 后退指针
    struct zskiplistNode *backward;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

简述下跳表的特点

  • 跳表是一个排序链表,redis中以score的大小做排序,增删改查的效率均是o(logn),由于其实现更易于理解和实现而被广泛使用。
  • 跳表是不稳定的,即同样是创建一个跳表,插入若干相同的值,内部的数据结构可能是不一致的。
  • 每个节点的大小不固定,不固定的原因是节点的拥有的forward指针个数不定,这个数值是在节点创建的时候随机选取的一个正整数,redis实现默认最大层数是32。
  • redis中的跳表增加了一个指向前节点的backward来往前遍历;一个span,用于计算forward节点和本节点中间的跨度,以方便zcount等命令来计算范围内score的数量。

如何插入一个数据?redis排序顺序是从头到尾,按照score从小到大。如果要增加一个值,首先需要确定插入后的顺序,假设跳表为skl ,新节点为n。

    1. 找到每一层比新节点score小,而且距离最近的节点,保存为update[]。
    1. 随机产生新节点的层高,对每一层的forward节点复制,令n->level[i]->forward = update[i]->level[i]->forward,给新节点的所有forward指针赋值。令update[i]->forward = n使新节点和其所有前置节点关联;实际上和双向链表操作类似。

如何查找一个节点p?

  • 场景1.根据score范围查找内容,对应zrange命令。查找左界和右界,中间的范围及所求。左界和右界算法类似,从head的顶层forward节点开始,如果比p的score小,那么让head = forward 继续此步骤.如果等于score那么返回。如果小于那么代表需要找的左界在head和forward之间,跳到下一层重复此步骤。

  • 场景2.根据内容查找score,对应zscore命令。redis不查找skiplist,而是额外使用了个hash表来达到o(1)的效率。

迭代器如何设计?

  • 可知n->level[0]->forward的步长是1,循环forward可以遍历整个跳表。
    推荐跳表的leveldb版实现
    综上,redis的数据结构分析告一段落。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容