Redis数据结构实现

Redis五种基本数据结构:String、List、Set、Hash、Zset

一、Redis对象基本结构

Redis是键值对方式的存储,每一个Redis存储对象都包括两个对象,一个是Key,一个是Value。

Redisc创建的对象RedisObject数据结构如下:

unsigned type: 4  // 类型,其中type的值对应5中redis数据类型的类型常量

unsigned encoding: 4 // 编码,encoding则记录了底层实现所用的数据结构

void *ptr // 指向底层数据结构指针

二、内部数据结构

2.1 简单动态字符串

redis实现了一个SDS结构(简单动态字符串)类型来替代C语言中的原生字符串, 结构如下

struct sdshdr {

    int len; //记录buf中字符串的实际长度

    int free; //记录buf数组空闲长度

    char buf[];//字节数组,用于保存字符串

};

和C语言原生字符串相比, 其有一下优点:

1、因为存储了字符串长度,可以在长量时间内获取字符串长度。

2、同样由于存储了字符串实际长度和buff空闲长度,字符串变化时不需要每次都重新分配内存;同时也避免了字符串长度变化可能导致的缓冲区溢出和内存泄漏。

3、SDS的设计由于根据字符串长度来判断字符串的末尾,因此中间可以存储任何数据包括空字符

2.2 双向链表

redis中的链表类型就是最常用的双向链表,涉及数据结构如下

//链表类型

typedef struct list {

listNode * head;//表头节点

listNode * tail;//表未节点,方便逆向遍历

unsigned long len;//节点数目, 优化链表求长度

}list;

2.3、Hash表

redis中的字典类型也就是基本的hash表

Redis hash 数据结构

redis中的rehash就是通过创建一个临时的字典,将正在使用的字典中的所有键值对复制到临时字典中(这个过程需要重新计算hash值和索引值,因此称为rehash),最后释放老的字典,并将其指针指向临时字典。

当数据量比较小的时候,这个过程可以一次性完成,但是数据量大的时候,庞大的计算量可能导致整个服务停止,因此redis采用了渐进式rehash:

1、同时维护两个hash表hd[0]和hd[1]

2、设置rehash_idx设置为0,在0< rehash_idx < 槽位数组长度时, 对hash表的任何操作除了执行正常的操作过程以外,还会将hd[0]槽位数组rehash_idx索引上的所有键值对rehash到hd[1]

3、同时所有的添加键值对操作只会在hd[1]上进行,保证了hd[0]只减不增。

4、rehash完成后,hash表指针指向hd[1],释放hd[0]内存。

2.4 跳跃表

跳跃表是一种有序的数据结构,支持平均O(logN)复杂度的节点查找,其效率可以和平衡树媲美,同时实现简单,而且由于不需要reblance操作,在高并发情况下表现更加出色。redis中的跳跃表也就是基本的跳跃表实现, 涉及数据结构如下。

typedef struct zskiplistNode{

    struct zskiplistLevel{   // 层数组,相当于索引,其中包括了当前节点砸每一层的前进指针以及到下一节点的跨度

           struct zskiplistNode *forward; // 本层节点中的下一节点

            unsinged int span; // 跨度,用于计算节点的排位。

   } level[]

   struct zskiplistNode *backWard; //指向前一个节点

   duble score; // 所有节点按分值排序

    robj *obj // 成员对象

typedef struct zskiplist {

    struct zskiplistNode *header ,*tail;

    unsigned long length; // 表中节点数

    in level; 最大层数

}


redis 跳表

实际上对有序链表稍加改造,我们就可以对链表进行二分查找。这就是我们要说的跳表。下面我们来看一下,跳表是怎么跳的。

原始链表

上图是一个简单的有序的单链表,如果要查找某个数据,只能从头至尾遍历链表,查找到值与给定元素时返回该结点,这样的查询效率很低,时间复杂度是为O(n)。

假如对链表进行改造,先对链表中每两个节点建立第一级索引,再对第一级索引每两个节点建立第二级索引。如下图所示:

跳表建立索引

对于上图中的带二级索引的链表中,我们查询元素 16,先从第二级索引查询 1 -> 7->13,发现16大于13 ,然后通过 13 的 down 指针找到第一级索引的 17,发现 16 小于17 ,再通过13 的 down 指针找到链表中的 16,只需要遍历 6 个节点就完成 16 的查找。如果在单链表中直接查找 16 的话,只能顺序遍历,需要遍历 10 个节点,是不是效率上有所提升呢,由于数据量较小,遍历 10 个节点到遍历 6 个节点你可能觉得没有提升多少性能。

所以,当链表的长度 n 比较大时,比如 1000、10000 的时候,在构建索引之后,查找效率的提升就会非常明显。

跳表有多占内存?

假如有 n 个元素的链表,第一级索引为 n/2 个,第二级为 n/4 个,第三级为 n/8 个,......,最后一级为 2 个。这几级索引的结点总和就是n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是 O(n)。也就是说,如果将包含 n 个结点的单链表构造成跳表,我们需要额外再用接近 n 个结点的存储空间。那我们有没有办法降低索引占用的内存空间呢?

其实 redis 中有序集合支持的核心操作也就是这几个。这里说下为什么 redis 使用跳表而不使用红黑树。

1、红黑树在查找区间元素的效率没有跳表高,其他操作时间复杂度一致。

2、相比红黑树,跳表的实现还是简单的,简单就意味着不容易出错,bug 少,稳定,易读,易维护。

3、跳表更加灵活,通过改变索引构建策略,有效平衡效率和内存消耗

2.5 压缩表

压缩列表是Redis为了节约内存而开发的, 是由一系列特殊编码的连续内存块组成的顺序型数据结构,每个压缩列表节点可以保存一个字节数组或一个整数值。其由三部分构成,如下所示:


zlbytes 表示的是整个压缩列表使用的内存字节数

zltail 指定了压缩列表的尾节点的偏移量

zllen 是压缩列表 entry 的数量

entry 就是 ziplist 的节点

zlend 标记压缩列表的末端

再看看一个 entry 的结构

typedef struct zlentry { 

// prevrawlen 前置节点的长度 ;prevrawlensize 编码 prevrawlen 所需的字节大小

 unsigned int prevrawlensize, prevrawlen;

 // len 当前节点值的长度 ,lensize 编码 len 所需的字节大小

  unsigned int lensize, len; 

// 当前节点 header 的大小  等于 prevrawlensize + lensize 

unsigned int headersize;     

 // 当前节点值所使用的编码类型 

unsigned char encoding; 

// 指向当前节点的指针 

unsigned char *p;

 } zlentry;      

prevrawlen 前置节点的长度,这里多了一个 size,其实是记录了 prevrawlen 的尺寸。Redis 为了节约内存并不是直接使用默认的 int 的长度,而是逐渐升级的。

同理 len 记录的是当前节点的长度,lensize 记录的是 len 的长度。

三、数据类型

String类型

String类型在Redis底层可以是int,raw和embstr。

如果一个String对象保存的是整数值,并且可以使用long来表示,那么将会以整数形式保存。

如果一个String对象保存的是字符串值,并且其长度大于32字节,那么就直接使用SDS结构来保存,其encoding标记为raw。

如果一个String对象保存的字符串长度小于32字节,那么会使用embstr编码,embstr是一种短字符串的优化,其存储还是使用SDS结构,但raw编码会调用两次内存分配函数来分别创建redisObject结构和SDS结构,而embstr编码则通过调用一次内存分配函数来分配 一块连续的空间,空间中依次包含redisObject和SDS结构。

List类型

List类型在Redis底层可以是ziplist(压缩列表)或linkedlist(双向列表)。

Hash类型

Hash类型在Redis底层可以是ziplist(压缩列表)或hashtable(Hash表),ziplist编码的哈希对象每当 有新的键值对要插入时,会将保存了键和值的节点依次推入到压缩列表表尾,因此同一键值对顺序存放在一起。

Set类型

Set类型在Redis底层可以是intset(整数集合)或hashtable(Hash表),只有当Set中的所有元素均为整数类型时才会使用intset。

Zset类型

Zset类型在Redis底层可以是ziplist(压缩列表)或skiplist(跳跃


数据结构

[1]Redis的数据结构

[2] 跳表的原理

[3] 晴天哥_374 redis hash底层数据结构

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

推荐阅读更多精彩内容