Redis笔记(1)数据结构与对象

1.前言

  此系列博客记录redis设计与实现一书的笔记,提取书本中的知识点,省略相关说明,方便查阅。

2.基本数据结构

2.1 简单动态字符串SDS(simple dynamic string)

  结构体定义: 

    len:  buf数组中已使用字节的数量,使用len判断实际内容长度,而不是'\0'字符

    free: 未使用字节的数量,查询该值,杜绝内存溢出

    buf[]: 实际分配空间及存储内容(字节数组,保证二进制安全,怎么存怎么取)

  保留C语言的习惯,字符串以'\0'结束,好处在于可以兼容使用C的API。

  分配策略

    1.修改后小于1MB,需要扩展,分配与修改后len相同长度的额外空间,即buf总大小变成len+len+1(结尾字符)

    2.修改后大于等于1MB,需要扩展,分配1MB,即buf大小变成len+1MB

  释放策略:惰性释放,长度变短时不进释放空间,有需要时释放。

2.2 链表

  在redis中使用广泛,列表键底层实现之一就是链表,当一个列表键包含了数量较多的元素,或者元素都是比较长的字符串,会使用链表作为列表键的底层实现。

  LLEN,LRANGE等命令:即list

  链表节点结构体定义listNode:

    listNode *prev: 前置节点

    listNode *next: 后置节点

    void *valud: 节点的值

  链表节点有前后节点,可以构成双向链表

  链表的结构体list定义如下:

    listNode *head: 表头节点

    listNode *tail: 表尾节点

    unsigned long len: 节点数量

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

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

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

  特性

    双端:前后查询O(1)

    无环:以NULL节点终结

    头尾指针:获取头尾迅速O(1)

    长度计数:O(1)

    多态: 三个函数可以支持保存各种不通类型的值

2.3 字典map

  使用广泛,比如数据库就是使用字典作为底层实现的。SET msg "hello world" 或者 HLEN  HGETALL等命令。就是map和普通键值对。

  哈希表dictht定义

    dictEntry  **table: 哈希表数组

    unsigned long size: 哈希表大小

    unsigned long sizemask: 哈希表大小掩码,用于计算索引值,等于size-1

    unsigned long used: 哈希表有节点的数量

  哈希节点dictEntry结构定义

    void *key: 键

    union {

      void *val;

      unit64_t u64;

      int64_t s64;

    }v:值

    dictEntry *next:下一个哈希表节点,形成链表,解决hash冲突

  字典dict结构定义

    dictType  *type: 类型特定函数,多态,针对不同类型的键值对

    void *privdata: 私有数据

    dictht ht[2]: 哈希表 2个空间用于rehash操作,一般使用0,下标1的在rehash时使用

    int trehashidx: rehash索引,不进行时,为-1

  表位置计算

    1.hash值  MurmurHash2算法

    2.hash & sizemask

  rehash步骤

    0.rehash条件:

      负载因子: used / 表大小

      满足一个即可:

          1)没有执行BGSAVE或者BGREWRITEAOF命令时,负载因子大于等于1

          2)执行了上诉两个命令,且负载因子大于等于5

          3)负载因子小于0.1时,自动收缩

    1.为ht[1]哈希表分配空间:

      扩展操作或收缩操作,ht[1]的大小>=ht[0].used*2^n(n取值使得右边最小)

      比如used为7  那么新表大小为8=2^3>7

    2.设置rehashidx,将其设置为0,表示开始rehash

    3.从hash表的rehashidx下标的链表开始,重新计算hash值,将其完全移动至另一个hash表,之后rehashidx增加1

    4.rehash期间,所有的增删改查操作会在两个hash表上进行:

      新增的只会添加在ht[1]表上,查找先在ht[0]上进行,没找到再去ht[1]查找。修改删除一样。

      单线程执行保证没有并发问题,渐进式rehash也是为了避免数据太大,造成一段时间内停止服务,所以一个下标一个下标移动。扩容机制以及hash算法保证hash碰撞不会过于集中,决定了单个下标数据不会很多。

    5.ht[0]上的所有键值对都放入到ht[1]后,rehash完成,rehashidx置为-1。

    6.转移完毕后释放ht[0]空间,将ht[1]设置成ht[0],在ht[1]新创建一个空白的hash表,为下一次rehash准备。

2.4 跳跃表

  跳跃表只有在有序集合键中和集群节点中使用到了,其余时候没有作用。有序集合键如ZRANGE  ZCARD命令相关。

  跳跃表节点zskiplistNode结构

    zskiplistNode *backward: 后退指针

    double socre: 分值

    robj *obj: 成员对象

    zskiplistLevel {

      zskiplistNode *forward: 前进指针

      unsigned int span: 跨度

    } level[]; // 层

  层level数组可以包含多个元素,每个元素包含一个指向其他节点的指针,可以通过层加快访问速度。层数越多,速度越快。创建节点时会随机生成一个1~32的数值作为该节点的level数组大小。

  跨度指的是两个节点之间的距离,NULL的前指针跨度为0。跨度可用来计算目标节点在跳跃表中的位置。

  后退指针只有一个,只能退到上一个节点。

  跳跃表中的成员通过分值从小到大排列,成员对象指向一个字符串对象,即SDS值。成员对象唯一,多个成员对象可以有相同的分值

  跳跃表zskiplist结构

    zskiplistNode *header, *tail:头尾节点

    unsigned long length: 表中节点数量

    int level: 表中层数最大的节点的层数

2.5 整数集合

  整数集合用于数量不多,且都是整型的情况,比如 SADD numbers 1 3 5 7 9, OBJECT ENCODING numbers可以显示 "intset".

  可以保存int16_t、int32_t、int64_t类型的值。

  整数集合intset结构

    uint32_t  encoding: 编码方式

    unit32_t  length: 包含的元素数量

    int8_t contents[]: 保存元素的数组

  集合中每一个元素都是contents中的一个项item,从小到大排列,不能重复。contents声明为int8_t,但是实际上不保存这个类型的值,真正类型取决于encoding,INTSET_ENC_INT(16,32,64)。contents的数组大小等于encoding*length。比如是16位的整型,5个元素,contents大小就是80。

  升级过程:比如原本encoding是16的整型,现在新增一个32的,就需要升级。

    1.根据新类型和元素数量,扩展contents的大小,分配空间。

    2.将原元素转换成新元素类型,放入正确的位置,保证顺序不变。

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

    4.修改encoding的值,length+1

  每次添加元素都可能造成升级,每次升级要处理所有的元素,时间复杂度为o(N)。

  升级提升灵活度,随意将16,32,64位的整型添加到集合中。节约内存,要存放16,32,64位的整型最好的方法是直接使用64位的,升级可以减少内存消耗。

  inset集合不支持降级操作

2.6 压缩列表

  压缩列表是列表键和哈希键的底层实现之一。列表键中包含少量列表项,并且列表项是小整数值或长度较短的字符串,就会使用压缩列表。

  比如RPUSH lst 1 3 5 10086 "hello" "world"     OBJECT ENCODING lst   输出”ziplist"

    HMSET profile  “name" "jack" ...

  压缩列表用于节约内存,特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以保存一个字节数组或者整数值。

  具体结构按照下列顺序排列

    zlbytes   unit32_t    4字节   记录整个压缩列表占用内存字节数,在进行内存重分配或计算zlend位置时使用。

    zltail       unit32_t    4字节   记录压缩列表尾节点距起始地址有多少字节,通过这个程序无需遍历整个压缩列表可以确定尾节点位置。

    zllen  unit16_t    2字节   记录压缩列表包含的节点数量,小于65535时为真,等于需要遍历才能计算出来。

    entryX    列表节点   不定     各个节点,长度由节点保存内容决定

    zlend      unit8_t      1字节    特殊值0xFF 用于标记压缩列表的末端

  压缩节点的构成entryX:

    1.字节数组长度为下面3种之一:

      长度小于等于63  2^6-1字节的字节数组

      长度小于等于16383  2^14-1字节的字节数组

      长度小于等于2^32-1字节的字节数组

    2.整型可以是下面6种之一:

      4位长度,0~12之间的无符号整型

      1字节长的有符合整数

      3字节长的有符号整数

      int16_t类型整数

      int32_t类型整数

      int64_t类型整数

    3.由下面三个内容构成一个节点:

      previous_entry_length: 记录了压缩列表中前一个节点的长度,该字段长度可以是1字节(前节点长度小于254)或5字节(大于等于254,第一个节点会设置成254后面4个节点保存前一个节点长度)。通过这个属性可以遍历到头节点。

      encoding:保存数据的类型以及长度。由开头前2位判断类型及该字段的长度,后面的判断长度

        00:该字段占用一个字节,后面6个比特位是数组长度,即长度小于等于63的字节数组

        01:该字段占用2个字节,后面6+8个比特位是数组长度,即长度小于等于2^14-1个字节数组

        10:该字段占用5个字节,后面4个字节记录长度,即长度小于等于2^32-1个字节数组

        11000000: int16_t类型整数

        11010000: int32_t类型整数

        11100000: int64_t类型整数

        11110000: 24位有符号整数

        11111110: 8位有符号整数

        1111xxxx: 该值的时候就没有content属性了,因为本身xxxx就足够保存0~12之间的值了。

      content:具体的内容。由encoding决定

  连锁更新:

    由于previous_entry_length记录了之前的节点长度,但是其有两种形态1字节和5字节。这里就会产生一个麻烦,原本前一个节点是小于254个字节的,本节点使用的1字节形态的previous_entry_length记录了这个情况,现在在该节点前插入了一个大于254个字节长度的节点,要将其改成5字节形态。但是这又产生了一个麻烦,比如当前节点是253个字节,由于previous_entry_length由1变成了5,增加了4个字节长度,导致该节点超过了254个字节,进而后置节点的previous_entry_length也要改变形态,这样可能会发生连锁反应。

    删除节点一样会导致连锁反应,都称之为连锁更新。最坏的情况下需要N次空间重新分配,每次最坏O(N),所以最坏为O(N^2)。但是由于可能性太小了,而且只要不是大面积的连锁更新都是可以接受的。所以这个不需要过度担心。

3.对象

  redis中没有直接使用上述数据结构来实现键值对数据库,而是基于此实现了一个对象系统,包含:字符串对象,列表对象,哈希对象,集合对象和有序集合对象。redis使用了引用计数技术来控制内存回收机制,不再使用的对象会被释放。

  对象基本结构redisObject:

    unsigned type:4 类型

    unsigned encoding:4 编码

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

    ...

  类型有5种:REDIS_STRING 字符串对象、REDIS_LIST 列表对象、REDIS_HASH 哈希对象、REDIS_SET 集合对象、REDIS_ZSET 有序集合对象。使用type命令可以查看对象类型。

  encoding决定ptr指向的数据结构,一共有以下几种数据结构:

    REDIS_ENCODING_INT    long类型的整数

    REDIS_ENCODING_EMBSTR  embstr编码的简单动态字符串

    REDIS_ENCODING_RAW    简单动态字符串

    REDIS_ENCODING_HT    字典

    REDIS_ENCODING_LINKEDLIST    双端链表

    REDIS_ENCODING_ZIPLIST    压缩列表

    REDIS_ENCODING_INSET    整数集合

    REDIS_ENCODING_SKIPLIST  跳跃表和字典

  不同的对象类型有相关的encoding,下面是一个对应关系:

    String:  int、embstr、raw

    list:  ziplist、linkedlist

    hash: ziplist、ht

    set: intset、ht

    zset: ziplist、 skiplist

  使用OBJECT ENCODING可以看见对象当前编码。

3.1 字符串对象

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

  如果一个字符串对象保存的是整型,并且可以用long类型表示,就会设置成int编码。

  如果一个字符串对象保存的是字符串值,并且长度大于39字节,那么使用SDS来保存这个字符串值,设置编码为raw

  如果一个字符串对象保存的是字符串值,并且长度小于等于39字节,那么使用embstr来保存这个字符串值。

  raw和embstr的区别在于,raw会开辟两次空间,创建redisObject和sdshdr结构,但是embstr只分配一块连续的区间,依次包含redisObject和sdshdr。对应的释放空间也只需要一次。

  编码的转换:

    int和embstr在条件满足的情况下会被转换成raw编码。

    假如一个字符串对象中保存的是整数值,但是使用了append命令追加了字符串,就会变成raw:set number 123, append number " xxx"

    embstr编码的字符串没有编写任何的修改程序,所以该类型实际上是只读的。修改的时候都会变成raw,再执行修改命令。

3.2 列表对象

  列表对象的编码可以是ziplist或者是linkedlist。

  编码的转换:

  满足以下2个条件的时候会使用ziplist:

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

    列表对象保存的元素数量小于512个。

  不满足上述条件的会转成linkedlist。

  这两个条件可以进行修改,配置list-max-ziplist-value和list-max-ziplist-entries。

3.3 哈希对象

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

  ziplist的时候,键值是紧挨在一起的,先键放入尾端,再把值放入尾端。

  编码的转换:

  满足以下2个条件的时候会使用ziplist:

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

    列表对象保存的元素数量小于512个。

  不满足上述条件会转成hashtable。

  这两个条件可以进行修改,配置hash-max-ziplist-value和hash-max-ziplist-entries。

3.4 集合对象

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

  编码的转换:

  满足以下两个条件时,对象使用intset编码:

    集合对象保存的所有元素都是整数值

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

  不满足上述条件的会使用hashtable编码

  第二个上限值是可以修改的,配置set-max-intset-entries选项。

3.5 有序集合对象

  有序集合的编码可以是ziplist或者skiplist。

  ziplist作为实现的时候,第一个节点保存元素的成员,第二个元素则保存元素的分值。

  zset结构包含一个zsl跳跃表和一个dict字典表。跳跃表使得有序集合可以进行范围型操作,如ZRANK和ZRANGE命令。字典表可以O(1)复杂度查找给定成员的分值,ZSCORE命令。

  编码的转化:

  满足以下两个条件时,对象使用ziplist编码:

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

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

  不能满足以上两个条件的有序集合对象使用skiplist编码。

  以上两个条件的值可以修改,配置zset-max-ziplist-entries和zset-max-ziplist-value。

4.其它概念

4.1 类型检查和命令多态

  redis的命令基本上分两种类型:

    所有键都能执行,比如delete、expire、rename、type、object。

    特定类型键执行,比如:

      SET、GET、APPEND、STRLEN只能针对字符串键执行

      HDEL、HSET、HGET、HLEN只能针对哈希键执行

      RPUSH、LPOP、LINSERT、LLEN只能针对列表键执行

      SADD、SPOP、SINTER、SCARD只能针对集合键执行

      ZADD、ZCARD、ZRANK、ZSCORE只能针对有序集合键执行

  为了不执行错误的命令,都会先进行类型检查,通过redisObject type来实现。

  相同的类型但是编码是不同的,意味着命令要适应不同编码结构,这就是命令的多态。

4.2 内存回收

  redis构建了一个引用计数技术来实现内存回收机制,在适当的时候自动释放对象,进行内存回收。

  redisObject中有一个refcount字段用于引用计数。

    创建对象时,引用计数的值会被初始化为1.

    被使用时,+1

    不被使用时,-1

    为0时,释放内存。

4.3 对象共享

  键A创建了一个整型100,键B也要创建一个整型100,做法有两种:新建一个或者使用A的。当然后者更节省内存。

    1.B指向A的值

    2.值的引用计数+1

  redis在初始化服务器的时候,会创建一万个字符串对象,包含从0~9999所有整数值。

  redis只共享整型值的字符串对象,因为字符串类型的验证相同操作复杂,多个对象时更复杂。

4.4 对象的空闲时长

  redisObject还有一个属性是lru属性,记录最后一次被程序访问的时间。OBJECT IDLETIME 可以打印出对象从当前时间到最后一次访问时间的空闲时长。这个命令不会修改lru的值。

  空闲时长的一个作用在于,如果服务器打开了maxmemory选项,并且服务器用于回收内存的算法是volatile-lru或者是allkeys-lru,当占用内存超过了maxmemory设置的上限,空转时长较高的键会被优先释放,从而回收内存。

  配置文件中maxmemory和maxmeory-policy选项介绍了相关信息。

posted @ 2018-06-30 18:45  dark_saber  阅读(335)  评论(0编辑  收藏  举报