Redis核心数据结构底层实现

String

使用SDS(simple dynamic string)实现。

3.2版本前

struct sdshdr {
    //长度
    int len;
    //剩余长度
    int free;
    //数据本体
    char buf[]; 
}

3.2版本以后

主要有以下几个字段

  • len 长度,有uint8_t、uint16_t、uint64_t 这几种类型
  • alloc 已经分配的空间,有uint8_t、uint16_t、uint64_t 这几种类型
  • unsigned char flags 表示类型,在最小长度中还会使用len的功能
  • char buf[] 具体数据存储的地方

详细情况如下所示

typedef char *sds;

/* Note: sdshdr5 is never used, we just access the flags byte directly.
 * However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];// buf[0]: z:  0101001
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};

扩容策略

字符串在长度小于1M之前,扩容空间采用加倍策略,也就是保留100%的冗余空间。
当长度超过1M之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配1M大小的冗余空间。

为什么不直接使用原生C语言的字符串

  1. 原生C语言字符串是通过\0结尾,想要知道字符串长度,需要通过O(n)的时间复杂度的strlen标准函数库来获取(遍历扫描),Redis是单线程的,性能需要做优化。
  2. redis作为开源中间件,可能接受很多类型语言的调用,其他类型语言如果将\0作为字符串的一部分提供给redis,若此时reids使用C语言原生库,则无法支持。

List

旧版本:数据量少时候用ziplist,数据量多的时候使用linkedlist
新版本:使用QuickList 参见下文

Hash(字典)

存储数据结构

zipList

当数据量比较小或者单个元素比较小时,底层用zipList存储。
hash-max-ziplist-entries 512 ziplist元素超过512,将其改为hashtable编码
hash-max-ziplist-value 64 单个元素大小超过64byte时,将其改为hashtable编码

dict

struct dict {
    dictht ht[2]
}

dict结构内部包含两个hashtable,通常情况下只会使用一个hashtable。在dict扩容缩容的时候,需要重新分配hashtable,然后进行渐进式搬迁,这个时候两个hashtable存储的分别是旧的hashtable和新的hashtable。待搬迁结束后,旧的hashtable被删除,新的hashtable取而代之。hashtable的结构和java的hashmap几乎是一样的,通过链表的方式解决hash冲突,第一维是数据,第二维是链表。数据中存储的是第二维链表的第一个元素的指针。

渐进式搬迁

大字典的扩容是比较耗时间的,需要重新申请新的数组,然后将旧字典所有链表中的元素重新挂接到像新的数组下面,这是一个O(n)级别的操作,会非常耗时,所以redis使用渐进式rehash小步搬迁,在hset 和hdel命令中埋伏了搬迁操作。如果客户端空闲了,没有后续命令,则redis会在定时任务中搬迁。

扩容条件

正常情况下,当hash表中的元素的个数等于第一维数组的长度时,就会开始扩容,扩容的新组是原数组大小的2倍。不过若Redis正在做bgsave,为了减少内存页的过多分离(copy on write),Redis尽量不去扩容(dict_can_resize),但是如果hash表已经非常满了,元素的个数已经达到了第一维数组长度的5倍(dict_forece_resize_ratio),说明hash表已经过于拥挤了,这个时候就会强制扩容。

缩容条件

当hash表因为元素的主键删除变得越来越稀疏时,Redis会对hash进行缩容来减少hash表的第一维数组空间占用。缩容条件是元素个数低于数组长度的10%。缩容不会考虑Redis是否正在做bgsave

Set

intset

当set集合容纳单元素都是整数并且元素个数比较少时,使用intset来存储元素
set-max-int-set-entries 512 intset能存储的最大元素个数,超过则使用hashtable编码

图示

image.png

hashtable

与Hash相同,不过value都是null

ZSet(Sorted Set)

数据量少的时候,用ziplist实现
数据量多的时候,zset内部实现是一个hash字典加一个跳跃链表(skiplist)

ZipList(压缩列表)

设计目的是为了提高存储效率。可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。ziplist能以O(1)的时间复杂度在表的两端提供push和pop操作。

图示

image.png

增加元素

插入一个新元素需要realloc扩展内存,取决于内存分配器的算法和当前的ziplist,如果realloc可能需要重新分配新的内存空间,并将之前的内容一次性拷贝到新的地址,也可在原有的地址上进行扩容。

级联更新

由于每个entry都会有一个prerawlan,如果内容小于254,则prerawlen使用一个字节,否则就是5个字节。那么某个entry进过了修改操作从253字节变成了254字节,那么它的下一个entry的prerawlen就需要更新,从1个字节扩展到5个字节:如果这个entry的长度本来也是253字节,那么后面的entry的prerawlen字段还得继续更新。
如果ziplist里面的每个entry加好都存储了253字节的内容,那么第一个entry内容的修改就会导致后续所有entry的级联更新。

未来发展

可能会被listpack(紧凑列表)代替

quickList(快速列表)

ziplist和linkedlist的混合体

图示

image.png

每个ziplist存储多少元素

quicklist内部默认单个ziplist长度为8k字节,超出了这个字节长度,就会起一个新的ziplist。ziplist的长度由配置list-max-ziplist-size决定

压缩深度

quicklist默认的压缩深度是0,也就是不压缩。实际压缩深度由配置参数list-compress-depth决定。为了支持快读的push/pop操作,queicklist的首尾两个ziplist不压缩,此时深度为1。如果深度为2,就表示quicklist首尾第一个ziplist以及首尾第二个ziplist都不压缩。

Skiplist(跳跃链表)

类似于跳表的结构,不同点如下

图示

image.png

查找过程

类似于跳表

随机层高

对于新插入节点,都需要调用一个随机算法给他分配一个合理的层高,每层晋升概率大概是50%.
redis标准源码中晋升概率只有25%,相对官方跳跃链表更加扁平化。

插入过程

首先我们在搜索合适的插入点的过程中将[搜索路径]摸出来了,然后就可以开始创建新节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过向前向后指针串起来。如果分配的新节点的高度高于当前跳跃列表最大高度,就要更新一下跳跃列表的最大高度。

更新过程

当我们调用zadd方法时,如果对应的value不存在,就是插入过程。如果value已经存在了,只是调整一下score值,就是更新过程。如果score值不会带来排序位置改变,那么更新就能返回。(该逻辑已经被redis官方接受)如果排序位置改变了,就需要调整位置。

调整位置

简单的策略就是先删除值,再插入这个元素,需要经过两次路径搜索。

如何计算排名

redis在skiplist的forward指针上进行了优化,给每个forward指针都增加了span属性,span是跨度的意思,表示从前一个节点沿着当前层的forward指针跳到当前这个节点中间会跳过多少个节点。redis在插入删除操作时会更新span值。这样在计算一个元素的排名时,只需要将搜索路径上进过的所有节点span值进行叠加,就可以计算出元素的最终rank值。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容