5.redis学习笔记-压缩列表&对象.md

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jun8148/article/details/82748970

6. 压缩列表

6.1. 什么时候会用到压缩列表?

当一个列表键包含少量的列表项,并且每个列表项要么是小整数值,要么是长度短的字符串,Redis就会采用压缩列表来做列表键的底层实现。

6.2. 为甚要用到压缩列表以及压缩列表是怎么构成的?

6.2.1. 压缩列表的作用

在一定程度上节约内存。

6.2.2. 压缩列表的构成

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

6.2.2.1. 压缩列表节点

节点可以保存字节数组或者一个整数
  • 字节数组必须是下面的其中一种

    • 长度小于等于63(26-1)字节的字节数组
    • 长度小于等于16383(214-1)字节的字节数组
    • 长度小于等于4294967295(232-1)字节的字节数组

    记住三个数字,6、 14、32就行

  • 整数值必须是下面的其中一种

    • 4位长,介于0-12之间的无符号整数
    • 1字节长的有符号整数
    • 3字节长的有符号整数
    • int16_t类型整数
    • int32_t类型整数
    • int64_t类型整数
每个节点有三部分:
  • previous_entry_length(长度是1字节或者5字节)

    以字节为单位,记录压缩列表前一个节点的长度。

    如果前一节点的长度小于254字节,previous_entry_length的长度就是1字节。否则就是5字节。

    通过previous_entry_length的值,我们可以计算出当前节点的前一个节点的起始地址。

  • encoding

    用于记录content保存的数组的数据类型和长度:

    • 一字节、两字节或者五字节长,值的最高位是00,01或者10的是字节数组编码,这种编码表示节点的content属性保存这字节数组,数组的长度由编码除去最高两位之后的其它位记录。
    • 一字节长,值的最高位是11,这种编码表示content保存的是整数,整数值的类型和长度由编码除去最高两位之后的其它位记录

    列举下字节数数组编码和整数编码:

    字节数组编码:(_ 表示留空,其余字母[a,b,c,x…]代表实际的二进制数据)

    编码 编码长度 content保存的值
    00bbbbbb 1字节 长度小于等于63字节的字节数组
    01bbbbbb xxxxxxxx 2字节 长度小于等于16383字节的字节数组
    10_ _ _ _ _ _ aaaaaaaa bbbbbbbb cccccccc dddddddd 5字节 长度小于等于4294967295的字节数组

    整数编码:

    编码 编码长度 content保存的值
    11000000 1字节 int16_t类型的整数
    11010000 1字节 int32_t类型的整数
    11100000 1字节 int64_t类型的整数
    11110000 1字节 24位有符号整数
    11111110 1字节 8位有符号整数
    1111xxxx 1字节 使用这个编码的节点没有content属性,因为编码本身的xxxx保存了一个0-12之间的值,无须content属性
  • content

    负责保存节点的值

6.3. API

函数 作用 时间复杂度
ziplistNew 创建一个新的压缩列表 O(1)
ziplistPush 创建一个包含给定值的新节点,并将这个新节点添加到压缩列表的表头或者表尾 平均O(N),最坏O(N2)
ziplistInsert 将包含给定值的新节点插入到给定节点之后 平均O(N),最坏O(N2)
ziplistIndex 返回压缩列表给定索引上的节点 O(N)
ziplistFind 在压缩列表中查找并返回包含了给定值的节点 因为节点的值可能是一个字节数组,所以检查节点值和给定值是否相同的复杂度是O(N),而查找整个列表的复杂度是O(N2)
ziplistNext 返回给定节点的下一个节点 O(1)
ziplistPrev 返回给定节点的前一个节点 O(1)
ziplistGet 获取给定节点所保存的值 O(1)
ziplistDelete 从压缩列表中删除给定的节点 平均O(N),最坏O(N2)
ziplistDeleteRange 删除压缩列表在给定索引上的连续多个节点 平均O(N),最坏O(N2)
ziplistBlobLen 返回压缩列表目前占用的内存字节数 O(1)
ziplistLen 返回压缩列表目前包含的节点数量 节点数量小于655535是为O(1),大于65535时为O(N)

7. 对象

Redis 中没有直接使用我们之前学习的那些数据结构(SDS、链表、字典、、压缩列表、整数集合等)来实现键值对数据库,而是基于这些数据结构创建了一个对象系统。

使用对象的好处:

  • Redis在执行命令之前,可以根据对象的类型判断一个对象是否可以执行给定的命令
  • 针对不同的环境,可以为对象设置多种不同数据结构,优化使用效率
  • 对象系统实现了基于引用计数的内存回收机制,会自动释放对象所占用的内存,节约内存,实现了对象共享机制
  • Redis的对象带有访问时间记录,可以用于计算数据库键的空转时长,在服务器启用;额maxmemory功能的情况下,空转时间较大的那些键可能会优先被服务器删除

7.1. 对象的类型和编码

Redis中的对象的结构redisObjec:

typedef struct redisObject{
    // 类型
    unsigned type:4;
    // 编码
    unsigned encoding:4;
    // 指向底层实现数据结构的指针
    void *ptr;
    // ...
    ....
}robj;
/* 
解释下
unsigned type:4 
是位域的用法,自行百度吧,这个表示占4wei
默认unsigned 为unsigned int
*/

7.1.1. 类型

类型常量 对象的名称
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

键只能是字符串对象,二值不一定,可以是以上五种其中一种。

7.1.2. 编码和底层实现

对象的ptr指针指向的对象的底层实现数据结构是由对象的encoding属性决定(编码)。

编码常量 编码所对应的底层数据结构
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典

在这里插入图片描述

7.2. 字符串对象

  • 如果一个字符串对象保存的是整数值,并且这个整数值可以用long整型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性中(将void* 转换成long),并将字符串独享的编码设置为int
  • 如果字符串对象保存的是一个字符串值,并且这个字符串的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将其编码设置为raw
  • 如果字符串对象保存的是一个字符串值,并且这个字符串的长度小于等于32字节,那么字符串对象将使用embstr编码方式来保存这个值

解释下embstr吧:

embstr编码是专门用来那批次吗短字符串的一种优化编码方式,这种编码只会调用一次内存分配函数来创建redisObjec和sdshdr结构(raw两次)

在这里插入图片描述

注意:编码方式是可以转换的

7.2.1. 字符串命令的实现

命令 int 编码的实现方法 embstr 编码的实现方法 raw 编码的实现方法
SET 使用 int 编码保存值。 使用 embstr 编码保存值。 使用 raw 编码保存值。
GET 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后向客户端返回这个字符串值。 直接向客户端返回字符串值。 直接向客户端返回字符串值。
APPEND 将对象转换成 raw 编码, 然后按 raw编码的方式执行此操作。 将对象转换成 raw 编码, 然后按 raw 编码的方式执行此操作。 调用 sdscatlen 函数, 将给定字符串追加到现有字符串的末尾。
INCRBYFLOAT 取出整数值并将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 取出字符串值并尝试将其转换成long double 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。 取出字符串值并尝试将其转换成 longdouble 类型的浮点数, 对这个浮点数进行加法计算, 然后将得出的浮点数结果保存起来。 如果字符串值不能被转换成浮点数, 那么向客户端返回一个错误。
INCRBY 对整数值进行加法计算, 得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令, 向客户端返回一个错误。 raw 编码不能执行此命令, 向客户端返回一个错误。
DECRBY 对整数值进行减法计算, 得出的计算结果会作为整数被保存起来。 embstr 编码不能执行此命令, 向客户端返回一个错误。 raw 编码不能执行此命令, 向客户端返回一个错误。
STRLEN 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 计算并返回这个字符串值的长度。 调用 sdslen 函数, 返回字符串的长度。 调用 sdslen 函数, 返回字符串的长度。
SETRANGE 将对象转换成 raw 编码, 然后按 raw编码的方式执行此命令。 将对象转换成 raw 编码, 然后按 raw 编码的方式执行此命令。 将字符串特定索引上的值设置为给定的字符。
GETRANGE 拷贝对象所保存的整数值, 将这个拷贝转换成字符串值, 然后取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。 直接取出并返回字符串指定索引上的字符。

7.3. 列表对象

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

引用一个例子吧

执行RPUSH numbers 1 “three” 5

ziplist:

在这里插入图片描述

linkedlist:

在这里插入图片描述

7.3.1. 编码转换

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

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

不能满足这两个条件的列表对象需要使用linkedlist编码

7.3.2. 列表命令的实现

命令 ziplist 编码的实现方法 linkedlist 编码的实现方法
LPUSH 调用 ziplistPush 函数, 将新元素推入到压缩列表的表头。 调用 listAddNodeHead 函数, 将新元素推入到双端链表的表头。
RPUSH 调用 ziplistPush 函数, 将新元素推入到压缩列表的表尾。 调用 listAddNodeTail 函数, 将新元素推入到双端链表的表尾。
LPOP 调用 ziplistIndex 函数定位压缩列表的表头节点, 在向用户返回节点所保存的元素之后, 调用 ziplistDelete 函数删除表头节点。 调用 listFirst 函数定位双端链表的表头节点, 在向用户返回节点所保存的元素之后, 调用 listDelNode 函数删除表头节点。
RPOP 调用 ziplistIndex 函数定位压缩列表的表尾节点, 在向用户返回节点所保存的元素之后, 调用 ziplistDelete 函数删除表尾节点。 调用 listLast 函数定位双端链表的表尾节点, 在向用户返回节点所保存的元素之后, 调用 listDelNode 函数删除表尾节点。
LINDEX 调用 ziplistIndex 函数定位压缩列表中的指定节点, 然后返回节点所保存的元素。 调用 listIndex 函数定位双端链表中的指定节点, 然后返回节点所保存的元素。
LLEN 调用 ziplistLen 函数返回压缩列表的长度。 调用 listLength 函数返回双端链表的长度。
LINSERT 插入新节点到压缩列表的表头或者表尾时, 使用 ziplistPush 函数; 插入新节点到压缩列表的其他位置时, 使用 ziplistInsert 函数。 调用 listInsertNode 函数, 将新节点插入到双端链表的指定位置。
LREM 遍历压缩列表节点, 并调用 ziplistDelete 函数删除包含了给定元素的节点。 遍历双端链表节点, 并调用 listDelNode 函数删除包含了给定元素的节点。
LTRIM 调用 ziplistDeleteRange 函数, 删除压缩列表中所有不在指定索引范围内的节点。 遍历双端链表节点, 并调用 listDelNode 函数删除链表中所有不在指定索引范围内的节点。
LSET 调用 ziplistDelete 函数, 先删除压缩列表指定索引上的现有节点, 然后调用 ziplistInsert 函数, 将一个包含给定元素的新节点插入到相同索引上面。 调用 listIndex 函数, 定位到双端链表指定索引上的节点, 然后通过赋值操作更新节点的值。

7.4. 哈希对象

哈希对象的编码可以是ziplist(使用压缩列表作为底层实现,当有新的键值对要加入到哈希对象的时候,程序现将保存了键的压缩列表节点推入到压缩列表表尾,如何将保存了值的压缩列表节点推入到压缩列表表尾,简言之,键值对总相邻,键在前,值在后)或者hashtable(使用字典作为底层实现,哈希对象中的每个键值对用字典键值对保存)。

7.4.1. 编码转换

当哈希对象满足下面两个条件时,哈希对象使用ziplist 编码:

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

不能同时满足上面两个条件的哈希对象使用hashtable编码。

7.4.2. 哈希命令的实现

命令 ziplist 编码实现方法 hashtable 编码的实现方法
HSET 首先调用 ziplistPush 函数, 将键推入到压缩列表的表尾, 然后再次调用 ziplistPush 函数, 将值推入到压缩列表的表尾。 调用 dictAdd 函数, 将新节点添加到字典里面。
HGET 首先调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后调用 ziplistNext 函数, 将指针移动到键节点旁边的值节点, 最后返回值节点。 调用 dictFind 函数, 在字典中查找给定键, 然后调用 dictGetVal 函数, 返回该键所对应的值。
HEXISTS 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。 调用 dictFind 函数, 在字典中查找给定键, 如果找到的话说明键值对存在, 没找到的话就说明键值对不存在。
HDEL 调用 ziplistFind 函数, 在压缩列表中查找指定键所对应的节点, 然后将相应的键节点、 以及键节点旁边的值节点都删除掉。 调用 dictDelete 函数, 将指定键所对应的键值对从字典中删除掉。
HLEN 调用 ziplistLen 函数, 取得压缩列表包含节点的总数量, 将这个数量除以 2 , 得出的结果就是压缩列表保存的键值对的数量。 调用 dictSize 函数, 返回字典包含的键值对数量, 这个数量就是哈希对象包含的键值对数量。
HGETALL 遍历整个压缩列表, 用 ziplistGet 函数返回所有键和值(都是节点)。 遍历整个字典, 用 dictGetKey 函数返回字典的键, 用 dictGetVal 函数返回字典的值。

7.5. 集合对象

集合对象的编码可以是intset(使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中)或者hashtable(使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,字典的值则全部被设置为NULL)。

7.5.1. 编码的转换

当集合对象同时满足下面两个条件时,对象使用intset编码:

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

不满足上面条件的,使用hashtable。

7.5.2. 集合对象的实现

命令 intset 编码的实现方法 hashtable 编码的实现方法
SADD 调用 intsetAdd 函数, 将所有新元素添加到整数集合里面。 调用 dictAdd , 以新元素为键, NULL 为值, 将键值对添加到字典里面。
SCARD 调用 intsetLen 函数, 返回整数集合所包含的元素数量, 这个数量就是集合对象所包含的元素数量。 调用 dictSize 函数, 返回字典所包含的键值对数量, 这个数量就是集合对象所包含的元素数量。
SISMEMBER 调用 intsetFind 函数, 在整数集合中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。 调用 dictFind 函数, 在字典的键中查找给定的元素, 如果找到了说明元素存在于集合, 没找到则说明元素不存在于集合。
SMEMBERS 遍历整个整数集合, 使用 intsetGet 函数返回集合元素。 遍历整个字典, 使用 dictGetKey 函数返回字典的键作为集合元素。
SRANDMEMBER 调用 intsetRandom 函数, 从整数集合中随机返回一个元素。 调用 dictGetRandomKey 函数, 从字典中随机返回一个字典键。
SPOP 调用 intsetRandom 函数, 从整数集合中随机取出一个元素, 在将这个随机元素返回给客户端之后, 调用 intsetRemove 函数, 将随机元素从整数集合中删除掉。 调用 dictGetRandomKey 函数, 从字典中随机取出一个字典键, 在将这个随机字典键的值返回给客户端之后, 调用 dictDelete 函数, 从字典中删除随机字典键所对应的键值对。
SREM 调用 intsetRemove 函数, 从整数集合中删除所有给定的元素。 调用 dictDelete 函数, 从字典中删除所有键为给定元素的键值对。

7.6. 有序集合对象

有序集合的编码可以是ziplist(使用压缩列表对象作为底层实现,每个集合元素使用两个紧挨在一起的压缩节点来保存,第一个节点保存元素的成员【member】,第二个元素保存元素的分值【score】,分值小靠近表头吗,分值大靠近表尾)或者skiplist(使用zset结构作为底层实现)。

zset结构包含一个字典和一个跳跃表:

typedef struct zset{
    zskiplist *zsl; //从小到大保存了所有集合元素
    dict *dict;
}zset;

zset中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点保存一个集合元素,跳跃表节点的object属性保存元素的成员,score属性保存元素的分值。通过该跳跃表,可以对有序集合进行范围型操作,比如zrank、zrange命令就是基于跳跃表实现的。

zset中的dict字典为有序集合创建了一个从成员到分值的映射,字典中的每个键值对都保存了一个集合元素,字典的键保存集合元素的成员,字典的值保存集合成员的分值。通过该字典,可以O(1)复杂度查找到特定成员的分值,zscore命令就是根据这一特性来实现的。通过字典+skiplist作为底层实现,各取所长为我所用。

7.6.1. 编码的转换

当有序集合对象满足下面条件时,使用ziplist编码:

  • 有序集合保存的元素数量小于128个
  • 有序集合保存的所有元素成员的长度都小于64字节

不能同时满足条件的话,就使用skiplist编码。

7.6.2. 有序集合命令的实现

命令 ziplist 编码的实现方法 zset 编码的实现方法
ZADD 调用 ziplistInsert 函数, 将成员和分值作为两个节点分别插入到压缩列表。 先调用 zslInsert 函数, 将新元素添加到跳跃表, 然后调用 dictAdd 函数, 将新元素关联到字典。
ZCARD 调用 ziplistLen 函数, 获得压缩列表包含节点的数量, 将这个数量除以 2 得出集合元素的数量。 访问跳跃表数据结构的 length 属性, 直接返回集合元素的数量。
ZCOUNT 遍历压缩列表, 统计分值在给定范围内的节点的数量。 遍历跳跃表, 统计分值在给定范围内的节点的数量。
ZRANGE 从表头向表尾遍历压缩列表, 返回给定索引范围内的所有元素。 从表头向表尾遍历跳跃表, 返回给定索引范围内的所有元素。
ZREVRANGE 从表尾向表头遍历压缩列表, 返回给定索引范围内的所有元素。 从表尾向表头遍历跳跃表, 返回给定索引范围内的所有元素。
ZRANK 从表头向表尾遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 从表头向表尾遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。
ZREVRANK 从表尾向表头遍历压缩列表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。 从表尾向表头遍历跳跃表, 查找给定的成员, 沿途记录经过节点的数量, 当找到给定成员之后, 途经节点的数量就是该成员所对应元素的排名。
ZREM 遍历压缩列表, 删除所有包含给定成员的节点, 以及被删除成员节点旁边的分值节点。 遍历跳跃表, 删除所有包含了给定成员的跳跃表节点。 并在字典中解除被删除元素的成员和分值的关联。
ZSCORE 遍历压缩列表, 查找包含了给定成员的节点, 然后取出成员节点旁边的分值节点保存的元素分值。 直接从字典中取出给定成员的分值。

猜你喜欢

转载自blog.csdn.net/jun8148/article/details/82748970