redis数据结构底层编码,Api使用,使用场景(5000字)

文章目录

redis常用数据结构以及底层编码

redis对象

redis数据结构底层编码,Api使用,使用场景(5000字)

在redis中,使用对象来表示数据库中的键值。redis中的对象可以用一个redisobject对象来表示。
在redis中有五种常用的数据类型:

REDIS_String 字符串对象
Redis_LIST 列表对象
Redis_HASH 哈希对象
Redis_SET 集合对象
Redis_ZSET 有序集合对象

这里需要强调的一点是,上面每种数据结构对象,其内部编码在不同的场景下可以有不同的实现,这里在后面分析具体的底层编码后会有具体的说明。
下图简要说明了redisobject的组成:

字符串对象

字符串对象使用的编码:

编码 对象
REDIS_ENCODING_INT 当字符串的值保存整数时使用
REDIS_ENCODING_EMBSTR 使用embstr编码的简单动态字符
REDIS_ENCODING_RAW 使用简单动态字符串实现的字符串对象

简单动态字符串(SDS)是什么?
redis没有使用c语言的字符串,而是自己实现了一个名为简单动态字符串的抽象类型,在redis数据库里面,字符串的键值对在底层都是由SDS实现的,这里需要说明的一点,sds还被用作缓冲区,AOF中的AOF缓冲区,客户端输入,输出缓冲区都是由SDS实现的。

SDS的结构:{
int len //记录字符串的长度,所以在获取字符串长度命令时,其时间复杂度为o(1)
设置和更新SDS长度的工作是由SDS的API在执行时自动完成的。
char buf[] //字节数组,用于保存字符串
int free // 记录buf数组中未使用字节的数量
}

SDS:有效的避免了缓冲区(buf)溢出:

当执行SDS API时,比如执行append命令时,API会先检查SDS的空间是否满足修改所需的要求,如果空间大小不满足的话,API会自动将SDS的空间扩展至执行修改所需的大小。然后才执行实际的修改操作。
修改字符串的值可能带来的问题:

  1. 如果对字符串进行增长,如上述所说,需要进行内存分配来扩展底层buf数组空间的大小,如果没有这一步内存分配,就会造成缓冲区溢出。
  2. 如果对字符串修改后,缩短了字符串,那么就需要重新分配内存释放掉字符串缩短的那部分内存空间,如果没有做,就会产生内存泄漏。

内存分配需要执行系统调用,所以通常是比较耗时的操作,在SDS中,通过未使用空间free,实现了空间预分配和惰性空间释放两种优化策略。

空间预分配

对SDS修改后,SDS的长度(len)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS的len属性的值,将和free属性的值相同。即相当于把未使用的值扩展为现在buf数组已使用的值,如果下次对字符串进行增长,增长长度小于free的值,就不会进行内存分配,只有在下次buf数组不够用时,才会再次按照上述规则进行再次进行内存分配。

惰性空间释放

当需要缩短SDS保存的字符串时,不会立即使用内存重分配来回收缩短的字节,而是使用free属性将这些字节的数量记录下来,并等待将来使用。这种策略,避免了缩短字符串所需的内存重分配操作,并为可能出现的增长操作提供了优化。

介绍完了SDS,下面我们继续回到字符串对象的介绍,并比较raw编码与embstr编码的区别:
如果字符串对象保存的字符串的值长度>39字节,那么字符串对象将使用一个SDS来保存字符串的值,并将对象的编码设置为raw。
如果字符串对象保存的字符串的值长度<=39字节,那么字符串对象将使用一个SDS来保存字符串的值,并将对象的编码设置为embstr。

embstr与raw两者之间的差异:

embstr:是用于保存短字符串的一种优化编码方式,embstr编码与raw编码一样,都是用redisobject与sds结构来表示字符串对象,但是raw编码会调用两次内存分配函数分别创建redisobject结构和sds结构(所以sds能够表示较长的字符串),而embstr编码则通过调用一次内存分配函数来分配一块连续的内存空间,空间中依次包含redisobject与sds结构。
embstr优点:
将创建字符串对象所需内存分配次数降低了1次,只需要执行一次内存释放函数就可以释放。
说明:
我们可以在redis中使用long,double类型的浮点数,但是浮点数在redis是以字符串值来保存的,如果保存一个浮点数,会先将浮点数转化成字符串值,然后保存转换所得到的的字符串值。
Redis中没有为embstr编码的字符串对象编写任何相应的修改程序(只有int编码的字符串对象和raw编码的字符串对象有修改程序)即embstr编码的字符串对象只是可读的,当对embstr编码的字符串对象执行修改命令时,会先将对象的编码从embstr转换成raw,然后再执行修改命令。

使用场景:

  • 计数器:因为redis的单线程机制,所以不会存在并发技术冲突。
  • 缓存
  • 分布式锁

列表对象:

列表对象编码:

编码 对象
REDIS_ENCODING_ZIPLIST 使用压缩列表实现的列表对象
REDIS_ENCODING_LinkeList 使用双端链表实现的列表对象

编码:

下面介绍列表底层编码:

链表:

redis数据结构底层编码,Api使用,使用场景(5000字)redis数据结构底层编码,Api使用,使用场景(5000字)
特性:
链表为双端链表,获取某个节点的前端节点与后端节点的复杂度都是o(1)
链表没有环,表头节点的prev指针还有表尾节点的next指针都是null,获取表头节点和表尾节点的复杂度为o(1)
链表自己维护了链表长度属性len,所以获取长度的时间复杂度为o(1)。

使用场景:

发布与订阅功能,慢查询,监视器
redis服务器本身还使用了链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区。

压缩列表

redis数据结构底层编码,Api使用,使用场景(5000字)redis数据结构底层编码,Api使用,使用场景(5000字)redis数据结构底层编码,Api使用,使用场景(5000字)
压缩列表是列表键和哈希键的底层实现之一:
当列表只包含少量的列表项,且列表项是小整数值,或者长度比较短的字符串,redis就会使用压缩列表来做列表的底层实现。
当哈希键只包含少量键值对,并且键值对的键和值要么就是小整数值,或者长度比较短的字符串
redis就会使用压缩列表来做哈希键的底层实现。

压缩列表可能出现的问题(连锁更新):

如果在压缩列表中存在连续的几个节点的长度在251-253间,那么它们的pre_length属性都为1个字节,如果其中任意一个节点长度发生变化增加,那么后面的节点的pre_length都需要扩容到5字节,造成一系列节点的连续扩容,会对系统性能造成很大的影响。

哈希对象

编码 对象
REDIS_ENCODING_ZIPLIST 使用压缩列表实现的对象
REDIS_ENCODING_HT 使用字典实现的哈希对象

ziplist

哈希对象编码如果是ziplist,每当有新的键值对加入到哈希对象时,会先将键的压缩列表节点推入到压缩列表表尾,然后将保存了值的压缩列表节点推入到压缩列表表尾。所以保存了同一个键值对的两个节点总是紧挨在一起,键在前,值在后。

hashtable

hastable编码的哈希对象,使用字典作为底层实现,哈希对象中的每个键值对,都使用一个字典键值对来保存。字典的每个键都是一个字符串对象,对象中保存了键值对的键,字典中的每个值都是一个字符串对象,对象中保存了键值对的值。
字典对象结构图:
redis数据结构底层编码,Api使用,使用场景(5000字)

转换

当哈希对象中保存的所有键值对的键和值的字符串长度都小于64字节,当哈希对象保存的键值对的数量小于512个,都会使用ziplist作为hash对象底层编码。

集合对象

编码 对象
REDIS_ENCODING_INTSET 使用整数集合实现的集合对象
REDIS_ENCODING_ZIPLIST 使用字典实现的有序集合对象

编码之间的相互转换

当满足集合对象保存的所有元素都是整数值,并且集合对象保存的元素数量不超过512个
时会使用整数集合实现的集合对象,注意当使用字典作为集合的底层实现时,字典中的dictEntry键为元素,值为null。

有序集合对象

编码 对象
REDIS_ENCODING_ZIPLIST 使用压缩列表实现的有序集合对象
REDIS_ENCODING_SKIPLIST 使用跳跃表和字典实现的有序集合对象

这里重点介绍一下skiplist编码的有序集合,它使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表。
zset结构中的跳跃表按照分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性,保存了元素的成员,跳跃表节点的score属性保存了元素的分值。通过这个跳跃表,就可以进行范围性操作。但是如果我们想要根据元素查询分值,只有跳跃表的话,时间复杂度就会变成o(logn),所以引入字典为有序集合创建一个从成员到分值的映射,字典的dictEntry中的键保存元素,而值保存元素的分值。基于此,就可以用o(1)复杂度查找给定成员的分值。
总的来说,redis选择了用时使用字典和跳跃表两种数据结构来实现有序集合,让有序集合的查找和范围型操作都尽可能快的执行。

编码之间的相互转换:

当有序集合保存的元素数量小于128,并且保存的所有元素成员的长度都小于64字节,会采用ziplist编码实现有序集合,任一条件不满足都会采用skiplist编码。

上一篇:windows+WSL2搭建docker本地镜像仓库


下一篇:算法笔记:哈希表、映射和集合