快牵着我的袜子

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

redis基础数据结构和编码方式

一、底层数据结构

1)简单动态字符串(SDS)

SDS是二进制安全的。buf用于保存的是一系列二进制数据。

struct sdshdr{

  int len;    //记录数组中已使用字节的数量

  int free;    //记录数组中未使用字节的数量

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

};

空间预分配

  1、如果修改后SDS的长度len小于1MB,那么将分配与len相同长度的未使用空间,

如:修改后SDS的长度为13字节,那么将分配13字节未使用的空间,buf数组的长度将变为13+13+1=27字节(额外的1字节用于保存空字符)。

  2、如果修改后SDS的长度大于1MB,那么将分配1MB的未使用长度,

如:修改后SDS的30MB,那么将分配1MB的未使用的空间,buf数组的长度变为30MB+1MB+1byte。

 

2)双端链表 

typedef struct list{

  listNode *head;        //头结点

  listNode *tail;         //尾结点

  unsinged long len;       //节点数量

  void *(*dup)(void *ptr);      //节点值复制函数

  void *(*free)(void *ptr);      //节点值释放函数

  int (*match)(void *ptr,void *key);  //节点值对比函数

};

 

typedef struct listNode{

  struct listNode *prev;    //前置指针

  struct listNode *next;    //后置指针

  void *value;        //节点的值

};

 redis链表实现的特性

  1、双端:链表节点带有头尾指针

  2、无环:表头节点的prev指针和表尾节点的next指针都指向null,对链表的访问以null为终点

  3、带表头指针和表尾指针,获取表头和表尾节点的复杂度为O(1)。

  4、链表长度计数器,获取节点数量的复杂度为O(1)。

  5、多态:链表节点使用void*指针来保存节点值,可以通过dup,free,match三个函数为节点设置类型特定值。

 

3)字典

typedef struct dict{

  dictType *type;    //类型特性函数

  void *privdata;    //私有数据

  dictht ht[2];      //哈希表

  int trehashidx;     //rehash索引

};

typedef struct dictType{

  unsigned int(*hashFunction)(const void *key);            //计算哈希值函数

  void *(*keyDup)(void *prevdata,const void *key);            //复制键函数

  void *(*valDup)(void *prevdata,const void *obj);            //复制值函数

  void *(*keyCompare)(void *prevdata,const void *key1,void *key2);    //对比键函数

  void *(*keyDestructor)(void *prevdata,const void *key);          //销毁键函数

  void *(*valDestructor)(void *prevdata,const void *obj);          //销毁值函数

};

typedef struct dictht{

  dictEntry **table;      //哈希表数组

  unsigned long size;      //哈希表大小

  unsigned long sizemask;    //掩码,用于计算索引值,总是等于size-1

  unsigned long usd;      //哈希表已有节点数量

};

typedef struct dictEntry{

  void *key;      //键

  union{    

    void *val;

    uint64_t u64;

    int64_t s64;

  }v;        //值

  struct dictEntry *next;  //指向下一个哈希表节点,形成链表。

};

 

 哈希算法:

  1、hash=dict->type->hashFunction(key);  //使用字典设置的哈希函数,计算键key的哈希值

  2、index=hash & dict->ht[x].sizemask;   //根据哈希表的不同,使用对应哈希表的sizemask和hash值计算出索引值

 

解决键冲突:

  链地址法,每个哈希表节点都有一个next指针,多个哈希表节点可以使用next指针构成一个单向链表,被分配到同一个索引的多个节点可以

用这个单向链表连接起来,从而解决键冲突问题。

 

rehash:

哈希表执行rehash的操作步骤如下:

  1、为字典ht[1]哈希表分配空间,空间大小取决于以下判断

    (1)如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2^n(2的n次方幂)

    (2)如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n。

  2、将保存在ht[0]中多有键值对rehash到ht[1]上面,rehsh指的是重新计算键的哈希值和索引值,然后将键放置到ht[1]哈希表指定的位置上。

  3、当ht[0]所有的键值对都迁移至ht[1]上后,释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。

渐进式rehash:

  为避免对服务器性能造成影响,采取分多次,渐进式地将ht[0]的键值对慢慢的rehash到ht[1]。步骤如下

  1)为ht[1]分配内存,让字典同时持有ht[0]和ht[1]两个哈希表

  2)在字典中维持一个索引计数变量rehashidx,并将它的值设置为0.表示rehash开始。

  3)在rehash进行期间,每次对字典执行增删查改时,程序除执行指定操作外,还会将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1]上,当rehash工作完成后,rehashidx值加一。

  4)随着rehash的进行,最终在某个节点完成所有复制,将rehashidx的值设置为-1,表示rehash完成。

注意,在进行渐进式rehash过程中,删查改会在两个表中进行,而增加操作一律在新表中进行操作。

 

4)跳跃表

  支持平均O(logN)、最坏O(N)复杂度的节点查找,允许重复分数,排序不止根据分数,还可能根据成员对象(当分数相同时);每一层有一个前继指针,因此在第1层,就形成了一个双向链表,从而可以方便的从表尾向表头遍历。

  注意:表头节点和其他节点的构造是一样的:表头节点也有后退指针、分值和成员对象,不过表头节点没有用到这些属性。

typedef struct zskiplist{

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

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

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

}zskiplist;

typedef struct zskiplistNode{

  struct zskiplistNode *backward;  //后退指针

  double score;        //分值

  robj *obj;          //成员对象

  struct zskiplistLevel{

    struct zskiplistNode *forward;    //前进指针

    unsgned int span;          //跨度

  }level[];

}zskiplistNode;

后退指针:

  它指向当前节点的前一个节点,后退指针在程序从表尾向表头遍历时使用

分值:

  是一个浮点数,跳跃表中的所有结点,都是根据score从小到大来排序的。   

成员对象:

  该结点的成员对象指针。

注意: 同一个跳跃表中,各个结点保存的成员对象必须是唯一的,但是多个结点保存的分值却可以是相同的:分值相同的结点将按照成员对象的字典顺序从小到大进行排序。

 层:

  每个跳跃表节点都可能含有很多的层,层数通过以下函数生成

  int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
      level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
  }

  结果为最终返回level为1的概率是1-0.25=0.75,返回level为2的概率为0.25*0.75,返回level为3的概率为0.25*0.25*0.75......因此,

  如果返回level为k的概率为x,则返回level为k+1的概率为0.25*x,换句话说,如果k层的结点数是x,那么k+1层就是0.25*x了。

  这就是所谓的幂次定律(powerlaw),越大的数出现的概率越小。

 

  前进指针: 

    每一层都包含一个指向下一个节点的指针,用于从表头向表尾方向访问节点。

  跨度:

    跨度也叫层跨度,用于记录节点某一层距离下一个节点的距离。与普通跳跃表的区别之一,就是包含了层跨度(level[i].span)的概念

查找:

  每次从最高层开始,如一个跳跃表的最高层数为4,则从第4层开始遍历查找,如查找到数据则返回,当碰到null后结束;接着循环从第三层进行查找,当第一层遍历后即一定可以获取到想要的结果。

 

5)整数集合

typedef struct intset{

  uint32_t encoding;    //编码方式

  uint32_t length;      //元素数量

  int8_t contents[];     //保存元素的数组

}intset;

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

  升级:(三步)

    1、根据新元素的类型,扩展整数集合底层数组的空间大小,为新元素分配空间

    2、将底层数组现有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位置上,并且保持有序

    3、将新元素添加到底层数组里面。

  例子:一个包含三个元素的数组,每个元素的大小为16位,现将一个32位的数字插入数组,则

  1、将原来数组的大小(16*3=48位)扩展128位(4*32)

  2、由于前三个元素大小为48位,占据数组的0~47位,因为每个元素要扩展为32位,所以先将原来的第三个元素移动到64~95的空间内,接着将第二个元素移动到

    32~63的空间,因为第一个元素排名第一,所以被安排到0~31位的空间上。

  3、 将新元素放置到数组的96~127的位置上。

  最后更改encoding的值,修改length的值

 

6)压缩列表

  压缩列表是一些列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

  压缩列表组成部分如图所示

zlbytes zltail zllen entry1 entry2 ... entryN zlend

  zlbytes:记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者重新计算zlend的位置时使用

  zltail:几率压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无须遍历压缩列表就可以确定表尾节点的地址

  zllen:记录压缩列表包含的节点数量当小于UINT16_MAX(65535)时,这个值即节点数量,当大于时,需遍历整个列表得出。

  entryX:节点,不确定数量

  zlend:1字节,标记列表末端

 

  压缩列表节点

previous_entry_length encoding content

  previous_entry_length:

    前一个节点的长度,可以是1字节或者5字节。当前一个节点长度小于254字节时,为1字节,当前一个节点长度大于254字节时,

    用5字节表示:其中属性的第一字节会被设置为0XFE(254)而之后的字节用于保存前一字节的长度

  encoding:

    记录节点的content属性所保存数组的类型和长度。

    1、一字节,两字节或者五字节,值的最高位为00,01,10的是字节数组,也标志着centent属性保存着字节数组,数组的长度由编码出去最高位之后的其他位记录。

    2、一字节长,值的最高位以11开头的是整数编码,标志着centent属性保存着整数值,整数值的类型和长度由编码出去最高位之后的其他位记录。

  content:字节数组或者整数

  连锁更新:当插入或者删除一个元素,导致多个元素需要改变编码,即连锁更新。

二、对象类型与编码

  在redis的数据库中创建一个新的键值对时,总是创建两个对象,一个存储键,一个存储值。

对象的数据结构如下

typedef struct redisObject{

  unsigned typed:4;

  ungigned encoding:4;

  void *ptr;

}robj;

 

  对于redis数据库保存的键值对来说,键总是一个字符串对象,而值可以是字符串对象、列表对象、哈希对象、

集合对象或者有序集合对象其中的一种。使用命令TYPE,可以查看数据库键对应的值对象的类型。

使用OBJECT ENCODEING命令可以查看一个数据库键的值对象的编码。

 

类型常量对应的对象名称

  1)REDIS_String:  字符串对象

  2)REDIS_LIST:    列表对象

  3)REDIS_HASH: 哈希对象

  4)REDIS_SET:    集合对象

  5)REDIS_ZSET: 有序集合对象

编码和底层实现

  1)REDIS_ENCODING_INT:       long类型的整数(长度不超过32位的整数)

  2)REDIS_ENCODING_EMBSTR:    简单动态字符串(3.2版本 小于44字节(之前是39:https://www.zhihu.com/question/25624589))

  3)REDIS_ENCODING_RAW:      简单动态字符串(3.2版本 大于44字节)

  4)  REDIS_ENCODING_HT:       字典

  5)REDIS_ENCODING_LINGKEDLIST:  双端链表

  6)REDIS_ENCODING_ZIPLIST:     压缩列表

  7)REDIS_ENCODING_INTSET:     整数集合

  8)REDIS_ENCONDING_SKIPLIST :   跳跃表和字典

三、 字符串对象

1、字符串的编码:

字符串对象的编码可以是int、raw或者embstr。

  1)如果一个字符串对象保存的是整数值,并且这个整数值可以用lon类型表示,那么字符串对象将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码方式设置为int。

  2)如果字符串对象保存的是一个字符串值,并且这个字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串保存这个值,并将对象的编码设置为raw。

  3)如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。

2、编码的转换

  int编码和embstr编码的字符串对象在满足条件的情况下,会转换为raw编码的字符串对象。

  1)int编码转为raw编码:原对象保存的值不再是整数值,而是一个字符串值,那么会发生编码从int变为raw

  2)redis没有为embstr编码的字符串对象编写任何相应的修改程序(只有int转为raw),所以,embstr编码字符串实际上是只读的,当对embstr编码的字符从执行修改命令时,

  程序会先将对象的embstr转换成raw,然后再执行修改命令。(embstr编码的字符串对象执行APPEND命令后,对象的编码会从embstr变为raw)。

3、常用命令

  1) SET:设置

  2) GET:获取

  3) MSET:设置多个键值对

  4) MGET:获取多个键的值

  5) DEL:删除指定键

  6) STRLEN:获取字符串长度

  7) APPEND:将指定字符串追加到现有字符串末尾

  8) SETTN:如果没有该键,就会添加,如果存在就不添加,返回0

  9) SETEX:设置超时时间

四、列表对象

1、列表对象的编码:

  列表对象的编码可以是ziplist或者linkedlistziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点保存一个列表节点。linkedlist编码的列表对象使用双端链表作为底层

实现。每个双端链表节点(node)都保存一个字符串对象,而每个字符串对象都保存了一个列表元素。

2、编码的转换

当列表对象可以同时满足一下两个条件时,列表对象使用ziplist编码,不能满足这两个条件的列表对象需要使用linkedlist编码。

  1)列表对象保存的所有字符串元素的长度都小于64字节

  2)列表对象保存的元素数量小于512个,

3、常用命令

  1)LPUSH:从表头推入元素

  2)RPUSH:从表尾推入元素

  3)LPOP:移出并获取列表的第一个元素

  4)RPOP:移出并获取列表的最后一个元素

  5)LRANGE:取出指定范围的元素

  6)LREM:移除列表中与参数 VALUE 相等的元素。(格式:LREM key count VALUE count>0,从表头开始,count<0,从表尾开始,count=0,删除全部

  7)LLEN:获取链表长度(元素数量)

  8)LSET key index value:通过索引设置列表元素的值

五、哈希对象

1、哈希对象的编码:

  哈希对象的编码可以是ziplist或者hashtable。

  1)ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入压缩列表表尾。因此

  1. 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
  2. 先添加到哈希对象中的键值对会放在压缩列表的表头方向,而后添加的哈希对象中的键指对会被放在压缩列表的表尾方向。

  2)hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存

  1. 字典的每个键都是一个字符串对象,对象中保存了键值对的键。
  2. 字典的每个值都是一个字符串对象,对象中保存了键值对的值。

2、编码的转换

  当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码,否则使用hashtable编码

  1)哈希对象保存的所有键值对的键和值的字符串长度都小于64个字节

  2)哈希对象保存的键值对数量小于512个。

3、常用命令

  1)HSET key field value:将哈希表 key 中的字段 field 的值设为 value 

  2)HMSET key field1 value1 [field2 value2 ] :同时将多个 field-value (域-值)对设置到哈希表 key 中。

  3)HDEL key field1 [field2] :删除一个或多个哈希表字段

  4)HEXISTS key field :查看哈希表 key 中,指定的字段是否存在。

  5)HGET key field:获取存储在哈希表中指定字段的值。

  6)HMGET key field1 [field2] :获取多个值

  7)HKEYS key:获取所有哈希表中的字段

  8)HGETALL key :获取在哈希表中指定 key 的所有字段和值

  9)HSETNX key field value :只有在字段 field 不存在时,设置哈希表字段的值。

六、集合对象

1、集合对象的编码:

  集合对象的编码可以时intset或者hashtable

  1)intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。

  2)hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象都包含一个集合元素,而字典的值则被全部设置为NULL。

2、编码的转换

  同时满足两个条件使用intset,否则使用hashtable

  1)集合对象保存的所有值都是整数值

  2)集合对象保存的元素数量不超过512个

3、常用命令

  1)SADD key member1 [member2] :向集合添加一个或多个成员

  2)SREM key member1 [member2]:移除集合中一个或多个成员

  3)SCARD key :获取集合的成员数

  4)SMEMBERS key:返回集合中的所有成员

  5)SREM key member1 [member2]:移除集合中一个或多个成员

  6)SPOP key:移除并返回集合中的一个随机元素

  7)SDIFF key1 [key2]:返回第一个集合与其他集合之间的差异

  8)SINTER key1 [key2]:返回给定所有集合的交集

七、有序集合对象

1、有序集合对象的编码:

  有序集合对象的编码可以时ziplist或者skiplist

  1)ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),第二个元素保存元素的分值(score)

  压缩列表内的集合元素按照分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则放置在靠近表尾的方向。

  2)skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。

  1. zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存一个集合元素:跳跃表即诶单的object属性保存了元素成员,而跳跃表节点的score属性则保存了元素的分值。

  通过跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的

   2. zset结构中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素:字典的键保存了元素的成员,而字典的值则保存了元素的分值。通过字典,程序

  可以用O(1)复杂度查找给定成员的分值,ZSCORE命令就是根据这一特性是实现的。

2、编码的转换

  同时满足一下两个条件使用ziplist,否则使用skiplist

  1)有序集合所保存的所有元素成员的长度都小于64字节

  2)有序集合保存的元素数量小于128个

3、常用命令

  1)ZADD key score1 member1 [score2 member2]:向有序集合添加一个或多个成员,或者更新已存在成员的分数

  2)ZCOUNT key min max:计算在有序集合中指定区间分数的成员数

  3)ZRANGE key start stop [WITHSCORES] :返回有序集中,指定区间内(下标)的成员(成员的位置按分数值递增(从小到大)来排序)

  4)ZRANK key member:返回有序集合中指定成员的索引

  5)ZSCORE key member:返回有序集中,成员的分数值

  6)ZREVRANK key member:返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序

  7)ZREVRANGE key start stop [WITHSCORES] :返回有序集中指定区间内的成员,通过索引,分数从高到低

  8)ZREVRANGEBYSCORE key max min [WITHSCORES]:返回有序集中指定分数区间内的成员,分数从高到低排序

   9)ZREM key member [member ...]:移除有序集合中的一个或多个成员

  10)ZCARD key:获取有序集合的成员数

 

参考:《redis设计与实现》

posted on 2020-09-09 17:42  快牵着我的袜子  阅读(1400)  评论(0编辑  收藏  举报