深入理解Redis(一)—基本数据结构

来源:互联网 发布:人工智能展馆 编辑:程序博客网 时间:2024/06/04 23:36

深入理解Redis(一)—基本数据结构

Redis相比于其他内存数据库的一个很明显的特点就是丰富的数据结构为开发提供了很大的便利,本文主要整理了Redis几种基本的数据结构以及具体的底层实现;

个人主页:tuzhenyu’s page
原文地址:深入理解Redis(一)—基本数据结构

一,基本数据结构

  • Redis的数据结构包括内部数据结构,内存映射数据结构;

    • 内部数据结构是Redis针对不同的使用情景构建的一系列高效的数据结构;

    • 内存映射数据结构是一系列经过特殊编码的字节序列, 创建它们所消耗的内存通常比作用类似的内部数据结构要少得多, 如果使用得当, 内存映射数据结构可以为用户节省大量的内存。

  • 内部数据结构主要包括:动态字符串,双端链表,字典,跳跃表;

  • 内存映射数据结果主要包括:整数集合,压缩列表;

1.动态字符串SDS(Simple Dynamic String)

  • Redis没有直接使用C语言的字符串,而是构建了一种名为简单动态字符串(SDS)的抽象数据类型;SDS实际上是一种对传统C语言字符串进行包装的抽象类型struct结构;

  • SDS的抽象结构为:

struct sds{int len;int free;char buf[];}
  • SDS相对于C语言字符串的优势在于:

    • SDS的获取字符串长度的复杂度为O(1),而C语言的时间复杂度为O(n);

    • SDS修改字符串时候会首先检查空间是否满足修改需求。如果不满足则重新分配内存,避免了内存溢出;

    • SDS留有未使用内存空间用free指向,减少修改字符串时带来的内存重新分配次数;

    • 通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略;

      • 空间预分配策略:空间预分配策略用于优化字符串长度拓展修改时,如果需要对SDS空间进行拓展时,redis不仅会修改必要的空间还会留出同样大小的预留空间;如果预留空间大于1M则按照1M进行分配;

      • 惰性空间释放策略:惰性空间释放用于优化字符串缩短操作;当字符串缩短操作后不会立即释放多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用;

    • SDS使用抽象结构中的len来判断字符创的结尾,而不是像C一样使用空字符”\0”表示结尾,保证了二进制的安全;

2.链表

  • Redis的链表是通过链表和链表节点实现的

  • 链表节点数据结构

typedef struct listNode{    struct listNode *prev;    struct listNode *next;    void *value;}listNode
  • 链表数据结构,双向链表
typedef struct list{    struct listNode *head;    struct listNode *tail;    unsigned long len;}list

3.字典

  • 字典是一种用于保存键值对(key-value)的抽象数据结构

  • Redis的字典使用哈希表实现的,一个哈希表可以有多个哈希表节点,每一个哈希表节点保存字典中的一个键值对;

  • 字典的数据结构

typedef struct dict{    dictType *type;    dictht ht[2]    //一个字典对应两个哈希表,一个用于存储数据一个用于rehash操作}dict
  • 哈希表的数据结构
typedef struct dictht{    dictEntry **table;    unsigned long size;    insigned long sizemask;    //哈希表大小掩码,用于计算索引值    unsigned long used;}dictht
  • 哈希表节点的数据结构
typedef struct dictEntry{struct dictEntry *next;    //指向下一个哈希表节点,形成链表   void *key;union{    void *val;}}dictEntry
  • 将一个新的键值对添加到字典里时

    • 程序需要先根据兼职对的键值计算出哈希值

    • 再根据键值的哈希值和哈希表大小掩码sizemask(总是等于size-1)计算出索引值

    • 再根据索引值将包含键值对的哈希表节点放到哈希表数组的指定索引上面

    • 如果出现哈希冲突则通过单链表解决,为了插入速度总是将新插入的键值对节点添加到表头位置;

  • Redis的哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,相同索引的哈希表节点用next指针构成一个单向链表;

  • 随着操作的不断执行,哈希表保存键值对会逐渐地增多或者减少,为了让哈希表的负载因子维持在一个合理的范围之内,需要进行rehash重新散列操作;

    • 哈希表的负载因子=哈希表已保存节点数量ht[0].used/哈希表大小ht[0].size

      • 服务器未执行BGSAVE操作或者BGREWRITEAOF操作情况下哈希表的负载因子大于等于1时进行哈希表拓展rehash操作;

      • 服务器执行BGSAVE操作或者BGREWRITEAOF操作情况下哈希表的负载因子大于等于5时进行哈希表拓展rehash操作;

      • 哈希表的负载因子小于0.1的时候,会进行自动的收缩rehash操作

    • 为字典的ht[1]哈希表分配空间

      • 如果是哈希表拓展操作,则ht[1]的大小等于第一个大于等于ht[0].used*2的2的n次方

      • 如果是哈希表收缩操作,则ht[1]的大小等于第一个大于等于ht[0].used的2的n次方

    • 重新计算所有键值对的键值hash值,根据hash值和sizemask将键值对重新放在ht[1]哈希表制定的位置上;

    • 将ht[1]设置为ht[0],重新创建一个空哈希表ht[1]用作下一次的rehash操作;

  • 为了避免rehash期间对服务器性能造成的影响,服务器不是一次性将ht[0]中的所有键值对rehash到ht[1]中,而是分多次渐进式rehash操作;

    • 为ht[1]分配空间,并维持一个索引计数器变量rehashidx用来记录rehash进度;

    • 用户的每次对字典的增改查删操作,程序除了完成指定的操作外还会顺便将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,完成后rehashidx加一;

    • 随着用户对字典的操作的不断执行,最终在某个时间点上,ht[0]所有键值对都会被rehash到ht[1]上,将rehashidx赋值-1表示rehash操作的完成;

4.跳跃表

  • Redis使用跳跃表作为有序集合(sorted set)的底层实现,跳跃表是一种类似红黑树的基于排序的索引结构;

  • 跳跃表的节点

    • 跨度span主要用来记录level数组中该节点与下一节点之间的距离,用来计算排位rank;

    • 跳跃表的所有节点按照score分值的大小排序;

typedef struct zskiplistNode{    struct zskiplistLevel{        struct zskiplistNode *next;        unsigned int span;    }level[];    struct zskiplistNode pre;    double score;    robj *robj;}zskiplistNode;
  • 跳跃表的插入

    • 遍历跳跃表记录score值小于插入节点score值的临界节点

    • 随机创建高度为level的跳跃表节点,并修改跳跃表高度;

    • 将新创建的节点的level数组指向临界节点的下一节点,将临界节点指向新创建的界定

  • 跳跃表的删除

    • 遍历跳跃表记录score值小于插入节点score值的临界节点

    • 将临界节点指向删除节点的下一节点;

5.整数集合

  • 整数集合是集合(set)的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现;

  • 如果在集合中再添加字符串,底层实现会自动从intset整数集合变成hashtable哈希表;

  • 整数集合结构

    • encoding 属性用来保存当前的编程方式,进而决定是否需要升级;

    • contents[] 数组用来保存元素,数组中的元素不重复并且按照从小到大的顺序排列;

typedef struct intset{    uint32_t encoding;    //编码方式    uint32_t length;    //集合包含元素的个数    int8_t contents[];    //保存元素的数组}intset;
  • 元素的插入

    • 通过二分查找的方式判断是否插入元素是否已经存在,如果存在则不添加;

    • 判断当前编码类型是否适用于新元素,如果新元素的值超过了当前编码范围需要对contents数组进行升级

    • 根据新编码类型重新进行内存分配,对原来的元素重新编码,将新元素按照顺序放入数组中;

  • 元素的插入无论升级与否因为要保持顺序性,因此插入的时间复杂度都为O(n)

  • intset整数集合是有序的,因此使用二分查找算法实现查找操作,查找的事件复杂度为O(log(n));

6.压缩列表

  • 压缩列表是Redis为了节约内存而开发的,由一系列特殊编码的连续内存块(字节数组)组成的顺序型数据结构.;主要作为链表(list),哈希表(hash)当相应的键值数目较少长度较短时候的底层实现;

  • 一个压缩列表可以保存任意多个节点,每个节点可以用来保存一个字节数组 或者一个整数值;

  • 一个压缩列表就是一个连续的字节数组,包含zlbytes压缩列表总内存字节数,zltail列表尾节点距离起始地址的字节数,zllen列表中包含的节点数目,entryX不定数据的节点用来保存字节数组或者整数值,zlend特殊字符标记列表末端;

  • 节点结构包括pre_entry_length记录了前一个节点的长度用来跳转到前一个节点,encoding和length共同决定了content中保存的数据的编码类型和长度

二,对象

  • Redis没有直接使用基本数据结构来存储数据,而是基于这些数据结果常见了一个对象系统,主要包括字符串对象,链表对象,哈希对象,集合对象,有序集合对象,这些对象的实现底层用到了至少一种数据结构;

  • 使用对象可以针对不同的情景为对象设置不同的底层数据结构实现,比如在存储数量不大的时候使用内存映射数据结构具有更高的性能,,进而优化对象在不同场景下的使用效率;

  • 对象结构

typedef struct redisObject{    unsigned type;    //用于记录对象类型,字符串对象,列表对象等    unsigned encoding;    //编码方式    void *ptr    //底层实现数据结构指针}

字符串对象

  • 字符串对象的编码方式包括int,raw,embstr(redis3新增)三种

    • 如果字符串对象保存的是一个整数值,并且值可以用long类型表示,则将字符串的编码方式设置为int

    • 如果字符串的长度长度大于32字节则使用动态字符串SDS保存,编码方式设置为raw

    • 如果字符串长度少于32字节则使用embstr编码方式,是对短字符串保存的一种优化方式

  • 字符串对象的常用指令

    • set key value 设定key的值

    • get key 获取key的值

    • incr key 对key的值进行自增

    • desc key 对key的值进行自减

列表对象

  • 列表对象的编码方式包括ziplist压缩列表和linkedlist双端链表

    • 在列表元素较少,字符串元素长度较短的时候采用ziplist压缩列表作为列表的底层实现;

    • 当新添加字符串元素长度超过默认值64,或者列表节点数目超过默认值512时候编码方式会转变为linklist双端链表

  • 列表对象常用指令

    • lpush 将新元素压入链表头

    • rpush 将新元素压入链表尾

    • lpop 从链表头中弹出元素

    • rpop 从链表尾部弹出元素

哈希对象

  • 哈希对象的编码方式包括ziplist压缩链表和hashtable哈希表

    • 在列表元素较少,键值对元素长度较短的时候采用ziplist压缩列表作为哈希的底层实现;新键值对添加的时候,先将键的压缩列表节点压入压缩列表尾部,再将保存了值的节点推入压缩列表的尾部;

      • 当新添加键值对元素长度超过默认值64,或者列表节点数目超过默认值512时候编码方式会转变为hashtable双端链表,会创建键值对节点放入哈希表中;
  • 哈希对象常用指令

    • hset 将哈希节点推入压缩列表或哈希表

    • hget 从哈希表中获取特定节点

集合对象

  • 集合对象的编码方式主要包括intset整数集合和hashtable哈希表

    • 当集合存储对象都为整数且数目不超过512的时候使用intset整数集合作为编码方式

    • 其他情况下使用hashtable作为编码方式

  • 集合常用指令

    • sadd 往集合中添加元素

    • scard 返回集合中元素数目

有序集合

  • 有序结合编码方式有ziplist压缩表和skiplist跳跃表两种

    • 当集合元素长度小于64字节,集合元素数目小于128个的时候采用跳跃表作为有序集合的底层实现;压缩列表使用两个紧挨在一起的节点来保存,第一个节点保存value,第二个节点保存score;集合元素根据score值的大小排序,分值较小的元素放在靠近表头的方向;

    • 其他情况下使用跳跃表作为有序集合的底层实现;

  • 有序集合常用指令

    • zadd 往集合中添加元素

    • zcard 集合中的元素数目

参考

  • Redis设计与实现
原创粉丝点击