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 | 遍历压缩列表, 查找包含了给定成员的节点, 然后取出成员节点旁边的分值节点保存的元素分值。 | 直接从字典中取出给定成员的分值。 |