Redis数据结构基础

本文简单记录redis目前支持的5种数据类型。和他们底层的数据结构以及需要关注的点。

基础结构和底层类型

类型STRING

三种底层类型 分别是 int,embstr,raw

如果是纯数字,使用int表示,如果保存的是字符串,并且小于39个字节,则使用embstr结构表示,否则 使用raw类型表示。
首先使用数字类型表示一个健肯定是最节省内存的。并且进行比较和共享等操作也是复杂度最低的。所以如果是数字则优先使用int结构表示
如果是字符串,要注意 : embstr是针对短字符串的一种优化编码方式,这个编码和raw编码一样 都使用redisObjectsdshdr结构来表示字符串对象。但是raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构。而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间依次包含redisObjectsdshdr两个结构。因此可以说使用embstr能够提升性能,内存释放也比较快。另外使用的是连续的内存,也能有效利用缓存。

可以使用命令:object encoding $key 这个命令来查看某个key具体使用的是哪种结构。

struct sdshdr {
  //记录buf数组已经使用的字节数量,等于sds所保存的字符串长度
 int len;
//记录buf数组种未使用的字节数量
int free;
//字节数组,用于保存字符串
char buf[];
};

另外,sds可以通过一些方式减少字符串修改所带来的内存重分配次数。从而获得更好的性能

  • 空间预分配
    可以看到上面的结构有一个free字段。如果对sds修改之后,sds的长度(len)小于1M,那么程序分配和len属性相同大小的未使用空间(free)
    如果修改sdks之后,它的长度大于1M,那么程序会分配和1M的未使用空间。通过这个预分配,redis可以减少连续执行执行字符串增长操作所需要的内存重新分配的次数。
  • 惰性空间释放
    这个特性用于优化sds的字符串缩短操作。当sds需要缩短保存的字符串时,程序不会立即执行内存重分配来挥手多出来的字节。而是使用free属性吧这些字节数量记录起来,等待将来使用。

二进制安全相关

所有的sds的api提供的是以二进制的方式来处理sds存放在buf里面的数据的。并没有做任何的限制或者过滤。存入的时候时什么,取出的时后就是什么。

类型LIST

两种底层数据结构 ziplist,linkedlist

当列表的对象同时满足下面两个条件的时候,列表对象会使用ziplist编码:

  • 列表对象保存的所有字符串元素长度都小于64字节
  • 列表对象保存的元素数量小于512个

链表特性

  • 双端:链表节点带有prev 和 next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1).
  • 无环:表头的prev和表尾的next指针指向的都是null,对链表的访问以null作为终点。
  • 获取链表的表头节点和表尾节点以及长度属性的时间复杂度都是O(1)

压缩列表
压缩列表时redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序行内存结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

  • 结构
zlbytes zltail zllen entry1 entry2 ... entryN zlend
  • 说明
属性 类型 长度 用途
zlbytes unit32_t 4字节 记录压缩列表占用内存字节数
zltail uint32_t 4 记录压缩列表尾节点距离压缩列表的其实地址由多少字节:通过这个偏移量,程序可以直接定位尾节点的地址
zllen uint32_t 2 记录了压缩列表的节点数量
entryX 列表节点 不定 各个节点,长度由保存的内容决定
zlend uint8_t 1 特殊值:0xff(255) ,标记压缩列表的结束

类型HASH

两种底层结构ziplist,hash

当hash对象满足下面两个条件的时候,hash才使用ziplist编码

  • hash对象所保存的所有键值对的健和值的字符串长度都<64字节。
  • hash对象保存的键值对数量小于512个

ziplist编码的hash对象使用压缩列表作为底层实现。每当有新的对象加入到hash对象时,程序先将保存了健的压缩列表节点推入压缩列表尾部。再将保存了值的压缩列表节点推到尾部。因此:
保存了同一键值对的两个节点总是紧凑的挨在一起。健的节点在前,值的节点在后; ziplist的结构参看上一小结。

字典是由hash表实现的,hash表里面保存的是hash节点的数组。先看hash的结构

//redis 字典所使用的hash表结构
typedef struct dictht {
  //hash表数组  
  dictEntry **table;
  //hash表大小
  unsigned long size;
  //hash表大小掩码,用于计算索引
  unsigned long sizemask;
  //该hash表 已经有的节点数量
  unsigned long used;
}
//hash 表结构用到的hash表节点的结构
typedef struct dictEntry {
    //健
    void *key;
    //值
    union{
        void *val;
        uint64_t u64;
        int64_t s64;
    }
    //指向下一个节点 ,形成一个链表
    struct dictEntry *next;
}

字典结构如下 ,使用了hash的类型

typedef struct dict{
    //类型特定函数
    dictType *type;
    //私有数据
    void *privdata;
    //hash表
    dictht ht[2];
    //rehash 索引 当rehash没有在进行是,值为-1
    int trehashidx;
}

hash表的扩展和收缩
当一下条件被满足时,程序会对hash表执行扩展:

  • 服务器目前没有在执行bgsave或者bgrewriteaof命令,并且hash表的负载因子大于1
  • 服务器目前正在执行bgsave或者bgrewriteaof命令,并且hash表的负载因子大于

负载银子计算公式:
负载因子 = hash表已保存的节点数量 / hash表大小
另一方面: 当hash表的负载因子小于0.1时,程序自动执行收缩操作。

rehash

从上面的字典的数据接口,可以看到dict由2个大小。ht属性是一个包含2个项的数组。每个项都是dictht hash表,一般情况下,字典只使用ht[0] hash表,ht[1]hash表只会在对ht[0]进行 rehash的情况下使用。

对hash表进行rehash的步骤

  1. 为字典的ht[1]hash表分配空间,这个hash表的空间的大小取决于要执行的操作,以及ht[0]当前包含的键值对的数量(ht[0].used的值)
  • 如果执行的时扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2的n次幂
  • 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0]的2的n次幂
  1. 将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算健的hash值和索引值,然后将健放到ht[1]对应的hash表指定位置上。
  2. 当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]设置为一个空白hash表,为下一次rehash做准备

上面的操作实际上是渐进式执行的。因为一个hash可能有上亿元素,如果一下子执行,会导致服务器卡顿或者段时间无法提供服务。rehash开始的时候 ,会吧rehashidx设置为0,表示rehash开始了。在rehash进行期间,增加、删除、查找、更新等操作,会顺带对ht[0]的rehashidx属性值加一,等全部完成。rehashidx会值-1.

在执行rehash期间 ,外部的访问。 对字典进行删除,查找,更新等操作会在两个hash表上进行。增加操作会在ht[1]上进行。保证ht[0]上没有任何添加操作。

类型SET

底层数据结构 intset,hash

当集合中的元素都是整数的时候,使用intset作为底层实现。intset条件如下

  • 集合对象保存的所有元素都是整数值
  • 集合对象保存的元素数量不会超过512个

整数集合的数据结构

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

contents 数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个item,各个item在数组中按值的大小从小到大排列,并且不包含重复项。

类型ZSET

底层数据结构 ziplist,skiplist+hash

当zset满足下面条件时,使用ziplist结构

  • 集合元素小于128个
  • 集合的所有元素成员长度小于64字节

ziplist作为底层实现的时候,每个元素使用2个挨在一起的压缩列表来保存,第一个节点是成员(member),第二个节点是分值(score)。压缩列表内的集合元素按照大小从小到大排序,分值较小的元素排在表头为止,反之在表尾。

skiplist作为底层实现的时候,一个zset同时包含一个字典和一个跳表

typedef struct zset{
    zskiplist *zsl;
    dict *dict;
}

这种结构中,zsl跳表按分值从小到大保存了所有的集合元素,每个跳表节点都保存了一个集合元素:跳表节点的object保存了元素的成员,而节点的score保存了分值。通过这个跳表,程序可以进行有序的范围型操作,比如zrank,zrange就是使用跳表api实现。
另外dict为有序集合创建了一个从成员到分值的映射,字典中每个键值都保存了一个集合元素。通过这个字典,程序可以通过O(1)的复杂度获取分值(zscore)。虽然它使用2个结构来保存数据,但是两种结构会通过指针来共享相同元素的成员和分值,所以不会浪费额外的内存。

跳表

  • 跳表是一种有序数据结构,它通过每个节点种维持多个其他节点的指针,从而达到快速访问的目的。
  • 跳表支持平均O(logN),最坏O(N)的复杂度查找节点,还可以通过顺序操作来批量处理节点。
  • 大部分情况下,跳表的效率可以和平衡树媲美,并且跳表的实现比平衡树简单。
    更多跳表的介绍参考
    https://blog.csdn.net/pcwl1206/article/details/83512600

你可能感兴趣的:(Redis数据结构基础)