redis底层数据结构(redis底层数据如何实现)
本篇文章给大家谈谈redis底层数据结构,以及redis底层数据如何实现对应的知识点,希望对各位有所帮助,不要忘了收藏本站喔。
本文目录一览:
- 1、Redis中hash、set、zset的底层数据结构原理
- 2、Redis - zset底层数据结构实现
- 3、Redis底层数据结构之string
- 4、Redis的五种数据结构及其底层实现原理
- 5、redis数据结构
Redis中hash、set、zset的底层数据结构原理
Redis-哈希对象(hash)
Redis-集合对象(set)
其中hashtable的key为set中元素的值氏族,而value为null
inset为可慎雹以理解为数组,使用inset数据结构需要满足下述两个条件:
intset的底层结构
查询方式一般采用二分查找法,实际查询复杂度也就在log(n)
Redis-有序集合对象(zset)
底层实现为 字典(dict) + 跳歼孝弊表(skiplist),当数据比较少的时候用ziplist编码结构存储。
同时满足以下两个条件采用ziplist存储:
ziplist存储方式
总结
[img]Redis - zset底层数据结构实现
zset为有序的,自动去重的集合数据类型,zset数据结构底层实现为字典(dict) + 跳表(skiplist),当数据比较少时,用ziplist编码结构存储,element和score各占一个entry.
ele1 - score1 - ele2 - score2 ...
redis.conf有以下两个参数控制
zset-max-ziplist-entries 128 // 元素个数超过128,将用skiplist编码
zset-max-ziplist-value 64 // 单个元素大小超过64byte,将用skiplist编码
加入的元素个数较少,且key的长度小于64byte时,使用ziplist有序存储元素。
往zset添加一个长度为65byte的key,此时底层选用skiplist存储元素。
利用dict可以以O(1)时间复杂度判断某个key'是否存在,以及取出key的分值。
skiplist是一个有多个索引层,一个数据层的数据结构,索引层和数据层都是有序的双向列表。
每个索引层的元素,都有两个指针字段,分别指向当前索引层下一个元素,以及下一索引层的本元素。
索引层1: 23 ----------- 52 ----------- 65 --------------- 76 ---------- null
索引层2: 23 ----43---- 52 ----59---- 65 -----70------- 76 ----84---- null
数据层: 23 -32-43-47- 52 -55-59-61- 65 --67-70-74- 76 -79-84--86-null
N: 节点数量
index: 1 --- N/2^1
index: 2 --- N/2^2
index: 3 --- N/2^3
index: k --- N/2^k
顶层只有两个首答元素: 2^k = N / 2 --- k = log2(N-1)
时间者瞎慧复杂度:神尘每一个索引层最多查找两次,层数为 log2(N-1) --- 2 * log2(N-1) = log(N),与二分查找时间复杂度一样。
Redis底层数据结构之string
我们都知道, Redis 是由 C 语言编写的。在 C 语言中,字符串标准形式是以空字符 \0 作为结束符的,但是 Redis 里面的字符串却没有直接沿用 C 语言的字符串。主要是因为 C 语言中获取字符串长度可以调用 strlen 这个标准函数,这个函数的时间复杂度是 O(N) ,由于 Redis 是单线程的,承受不了这个时间复杂度。
在上一篇文亮亏章中,我们介绍了 Redis 的 RedisObject 的数据结构,如下所示:
对于不同的对象, Redis 会使用不同的类型来存储。对于同一种类型 type 会有不同的存储形式 encoding 。对于 string 类型的字符串,其底层编码方式共有三种,分别为 int 、 embstr 和 raw 。
使用 object encoding key 可以查看 key 对应的 encoding 类型,如下所示:
对于 embstr 和 raw 这两种 encoding 类型,其存储方式还不太一样。对于 embstr 类型,它将 RedisObject 对象头和 SDS 对象在内存中地址是连在一起的,但对于派亩 raw 类型,二者在内存地址不是连续的。
在介绍 string 类型的存储类型时,我们说到,对于 embstr 和 raw 两种类型其存储方式不一样,但 ptr 指针最后都指向一个 SDS 的结构。那什么是 SDS 呢? Redis 中的字符串称之为 Simple Dynamic String ,简称为 SDS 。与普通 C 语言的原始字符串结构相比, sds 多了一个 sdshdr 的头部信息, sdshdr 基本数据结构如下所示:
可以看出, SDS 的结构有点类似于 Java 中的 ArrayList 。 buf[] 表示真正存储的字符串内容, alloc 表示所分配的数组的长度, len 表示字符串的实际长度,并且由于 len 这个属性的存在, Redis 可以在 O(1) 的时间复杂度内获取数组长度。
为了追求对于内存的极致优化,对于不同长度的字符串, Redis 底层会采用不同的结构体来表示。在 Redis 中的 sds.h 源码中存在着五种 sdshdr ,分别如下:
上面说了, Redis 底层会根据字符串的长度来决定具体使用哪种类型的 sdshdr 。可以看出, sdshdr5 明显区别于其他四种结构,它一般只用于存储长度不会变化,且长度小于32个字符的字符串。但现在一般都不再使用该结构, 因为其结构没有 len 和 alloc 这两个属性,不具备动态扩容操作 ,一旦预分配的内存空间使用完,就需要重新分配内存并完成数据的复制和迁移,类似于 ArrayList 的扩容操作,这种操作对性能的影响很大。
上面介绍 sdshdr 属性的时候说过, flag 这个属性用于标识使用哪种 sdshdr 类型, flag 的低三位标识当前 sds 的类型,分别如下所示:
同时,注意到在每个 sdshdr 的头定义上都有一个 attribute((packed)) ,这个是为了告诉 gcc 取消优化对齐 ,这样,每个字段分配的内存地址就是 紧紧排列在一起的 , Redis 中字符串参数的传递直接使用 char* 指针,其实现原理在于,由于 sdshdr 内存分配禁止了优化对齐,所以 sds[-1] 指向的就是 flags 属性的内存地址,而通过 flags 属性又可以确定 sdshdr 的属性,进而可以读取头部字段确定 sds 的相关属性。
sds的逻辑图如下所示:
相比较于 C 语言原始的字符串,尘键森 sdshdr 的具备一些优势。
由于 sdshdr 中存在 len 这个属性,所以可以在 O(1) 的时间复杂度下获得长度;而传统的 C 语言得使用 strlen 这个标准函数获取,时间复杂度为 O(N) 。
原始的 C 语言一直使用与长度匹配的内存,这样在追加字符串导致字符串长度发生变化时,就必须进行内存的重新分配。内存重新分配涉及到复杂算法和系统调用,耗费性能和时间。对于 Redis 来说,它是单线程的,如果使用原始的字符串结构,势必会引发频繁的内存重分配,这个显然是不合理的。
因而, sds 每次进行内存分配时,都会通过内存的预分配来减少因为修改字符串而引发的内存重分配次数。这个原理可以参数 Java 中的 ArrayList ,一般在使用 ArrayList 时都会建议使用带有容量的构造方式,这样可以避免频繁 resize 。
对于 SDS 来说,当其使用 append 进行字符串追加时,程序会用 alloc-len 比较下剩下的空余内存是否足够分配追加的内容 ,如果不够自然触发内存重分配,而如果剩余未使用内存空间足够放下,那么将直接进行分配,无需内存重分配。其扩容策略为, 当字符串占用大小小于1M时,每次分配为 len * 2,也就是保留100%的冗余;大于1M后,为了避免浪费,只多分配1M的空间。
通过这种预分配策略, SDS 将连续增长 N 次字符串所需的内存重分配次数 从必定 N 次降低为最多 N 次。
缓冲区溢出是指当某个数据超过了处理程序限制的范围时,程序出现的异常操作。 原始的 C 语言中,是由编码者自己来分配字符串的内存,当出现内存分配不足时就会发生 缓存区溢出 。而 sds 的修改函数在修改前会判断内存,动态的分配内存,杜绝了 缓冲区溢出 的可能性。
对于原始的 C 语言字符串来说,它会通过判断当前字符串中是否存在空字符 \0 来确定是否已经是字符串的结尾。因而在某些情况下,如使用空格进行分割一段字符串时,或者是图片或者视频等二进制文件中存在 \0 等,就会出问题。而 sds 不是通过空字符串来判断字符串是否已经到结尾,而是通过 len 这个字段的值。所以说, sds 还具备 二进制安全 这个特性,即可以安全的存储具有特殊格式的二进制数据。
Redis的五种数据结构及其底层实现原理
redis的字符串类型是由一种叫做简单动态字符串(SDS)的数据类型来实现
SDC和C语言字符串的区别:
1:SDS保存了字符串的长度,而C语言不保存,盯棚凯只能遍历找到第一个\0的结束符才能确定字符串的长度
2:修改SDS,会检查空间是否足够,不足会先扩展空间,防止缓冲区溢出,C字符串不会检查
3:SDS的预分配空间机制,可以减少为字符串重新分配空间的次数
备注:重新分配空间方式,小于1M的数据 翻倍+1,例如:13K+13K+1,如果大于1M,每次多分配1M,例如:10M+1M+1,如果字符串变短,并不会立即缩短,而是采用惰性空间释放,有专门的API可以释放多余空间
hash结构里其实是一个字典,有许多的键值对
redis的哈希表是一个dictht结构体:
哈希表节点的结构体如下:
hash算法:
当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出哈希值和索引值, 然后再根据索引值, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
hash冲突解决方式:链表法,后入的放到最前面
rehash:
键值数据量变动时,时为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或和仿者收缩。
如果是扩充,新数组的空间大小为 大于2*used的2的n次方,比如:used=5,则去大于10的第一个2的n次方,为16
如果是缩小,新数组的空间大小为第一个不大于used的2的n次方,比如:used=5,则新大小为4
redis的list列表是使用双向链表来实现的
···
typedef struct listNode {
struct listNode * pre; //前置节点
struct listNode * next; //后置节点
void * value; //节点的值
}
typedef struct list {
listNode *head; //表头节点
listNode tail; //表尾节点
unsigned long len; //链表所包含的节点数量
void ( dup) (void ptr); //节点值赋值函数 这里有问题
void ( free) (void ptr); //节点值释放函数
int ( match) (void *ptr, void *key) //节点值对比函数
}
···
1:有序集合的底层实现之一是跳表, 除此之外跳表它在 Redis 中没有其他应用。
2:整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
3:数据少是,使用ziplist(压缩列表),占用连续内存,每项元素都是(数据+score)的方式连续存储,按照score从小到大排序。ziplist为了节省内存,每个元素占用的空间可以不同,对于大数据(long long),就多用一些字节存储,而对于小的数据(short),就少用一些字节来存储。因此查找的时候需要按顺序遍历。ziplist省内存但是查找效率低。
无序集合可以用整数集合(intset)或者凯唤字典实现
Redis的5.0版本中,放出一个新的数据结构Stream。其实也是一个队列,没一个不同的key对应的是不同的队列,没个队列的元素,也就是消息,都有一个msgid,并且需要保证msgid是严格递增的。在Stream当中,消息是默认持久化的,即便是Redis重启,也能够读取到信息。
Stream的多播,与其它队列系统相似,对不同的消费者,也有消费者Group这样的概念,不同的消费组,可以消费通一个消息,对于不同的消费组,都维护一个Idx下标,表示这一个消费群组费到了哪里,每次进行消费,都会更新一下这个下标,往后面一位进行偏移。
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而大道快速访问节点的目的,具有以下性质:
1:有很多层结构组成
2:每一层都是一个有序的链表,排列顺序为由高到低,都至少包含两个链表节点,分别是前面的head节点和后面的nil节点
3:最底层的链表包含了所有的元素
4:如果一个元素出现在某一层的链表中,那么在该层之下的链表也全部都会出现
5:链表中的每个节点都包含两个指针,一个指向同一层的下一个链表节点,另一个指向下一层的通一个链表节点
多个跳跃表节点构成一个跳跃表
1:搜索,从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也及时和当前层的下一层的节点下一个节点
2:插入,首先确定插入的层数,有一种方法是抛一个硬币,如果是正面就累加,直到遇到反面为止,最后记录正面的次数作为插入的层数,当确定插入的层数K后,则需要将新元素插入从底层到K层
3:删除,在各个层中找到包含指定值得节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层。
整数集合是Redis用于保存整数值集合的抽象数据类型,它可以保存int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素。
整数集合的每个元素都是contents数组的一个数据项,他们按照从小到大的顺序排列,并且不包含任何重复项。
length属性记录了contents数组的大小。
需要注意的是虽然contents数组声明为int8_t类型,但是实际上contents数组并不保存任何int8_t类型的值,其真正类型由encoding来决定。
压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或一个整数值。
压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩的,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
压缩列表的每个节点构成如下:
redis数据结构
redis数据结构
Redis是一种存储key-value的内存型数据库,它的key都是字符串类型,value支持存储5种类型的数据:String(字符串类型)、List(列表类型)、Hash(哈希表类型、即key-value类型)、Set(无序集合类型,元素不可重复)、Zset(有序集合类型,元素不可重复)。
针对这5种数据类型,Redis在底层都是使用的redisObject对象表示的。redisObject有3个重要的属性:type、encoding、ptr。
其中,type表示value的数据类型,也就是我们上面说的5种数据类型(REDIS_STRING、REDIS_LIST、REDIS_HASH、REDIS_SET、REDIS_ZSET);encoding表示value的编码,即底层使用了哪种数据结构;ptr是一个指向保存value的底层数据结构的指针。
其中type和ptr属性不用做过多的解释,一看就知道什么意思,本篇文章主要分析value的encoding编码,也就是不同数据类型的value对应的底层数据结构是什么以及数蔽余据结构的原理分析。
Redis(Remote Dictionary Server ),即远程字典服务,是一个开源的使用ANSI C语言编写、支持网络、磨清可基于内存亦可持久瞎并前化的日志型、Key-Value数据库,并提供多种语言的API。
redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
关于redis底层数据结构和redis底层数据如何实现的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。