文章目录
- 面试题
- 源码核心
- Redis基本的数据结构(骨架)
- Redis数据库的实现
- Redis服务端和客户端实现
- 其他
- K-V实现
- 怎样实现键值对(key-value)数据库的
- 传统五大基本数据类型和新五大数据类型
- 5大数据结构底层C语言源码分析
- 示例
- redisObject
- 五大数据结构解析
- 定义
- Debug Object key
- String SDS数据结构
- Hash数据结构
- redis6:ziplist
- 源码分析:t_hash.c
- 源码分析:ziplist.c
- redis7:packlist
- List
- Set
- ZSet
- 总结
- redis6类型编码映射
- 数据类型对应的底层数据结构
- redis6数据类型以及数据结构的关系
- redis7数据类型以及数据结构的关系
- redis数据类型以及数据结构的时间复杂度
- skiplist跳表
- 时间复杂度
- 空间复杂度
- 优缺点
面试题
Redis数据类型的底层数据结构
- SDS动态字符串
- 双向链表
- 压缩列表ziplist
- 哈希表hashtable
- 跳表skiplist
- 整数集合intset
- 快速列表quicklist
- 紧凑列表listpack
内部redis重构使用
书籍
源码核心
Redis基本的数据结构(骨架)
- Redis对象object.d
- 字符串t_string.c
- 列表t_list.c
- 字典t hash.c
- 集合及有序集合t_set.c和t_zset.c
- 数据流t_stream.c:底层实现结构listpack.c和rax.c
- 简单动态字符串sds.c
- 整数集合intset.c
- 压缩列表ziplist.c
- 快速链表quicklist.c
- listpack
- 字典dict.c
Redis数据库的实现
- 数据库的底层实现db.c
- 持久化rdb.c和aof.c
Redis服务端和客户端实现
- 事件驱动ae.c和ae_epoll.c
- 网络连接anet.c和networking.c
- 服务端程序server.c
- 客户端程序redis-cli.c
其他
- 主从复制replication.c
- 哨兵sentinel.c
- 集群cluster.c
- 其他数据结构,如hyperloglog.c、geo.c等
- 其他功能,如pub/sub、Lua脚本
K-V实现
怎样实现键值对(key-value)数据库的
key一般为String类型的字符串对象
value类型则为redis对象(redisObject),可以是以下几种redis数据类型
传统五大基本数据类型和新五大数据类型
传统五大基本数据类型
- String
- List
- Hash
- Set
- ZSet
新五大基本数据类型
- bitmap实质String
- hyperLogLog实质String
- GEO实质Zset
- Stream实质Stream
- BITFIELD看具体key
每个Redis对象都是redisObject结构
redisObject
5大数据结构底层C语言源码分析
- SDS动态字符串
- 双向链表
- 压缩列表ziplist
- 哈希表hashtable
- 跳表skiplist
- 整数集合intset
- 快速列表quicklist
- 紧凑列表listpack
Redis6
Redis7
示例
set hello word为例,因为Redis是KV键值对的数据库,每个键值对都会有一个dictEntry(源码位置:dict.h),里面指向了key和value的指针,next 指向下一个 dictEntry。
key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在redis自定义的 SDS中。
value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。
实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。
redisObject
为了便于操作,Redis采用redisObjec结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObjec中定义了type和encoding字段对不同的数据类型加以区别。简单地说,redisObjec就是string、hash、list、set、zset的父类,可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObjec结构来到达同样的目的。
-
类型:4位的type表示具体的数据类型
-
编码,此处是数字类型:4位的encoding表示该类型的物理编码方式见下表,同一种数据类型可能有不同的编码方式。(比如String就提供了3种:int embstr raw)
-
最近被访问的时间:lru字段表示当内存超限时采用LRU算法清除内存中的对象。
-
表示当前对象被引用的次数:refcount表示对象的引用计数。
-
ptr指针指向真正的底层数据结构的指针。
五大数据结构解析
定义
Debug Object key
前提:开启该命令
Value at: 内存地址
refcount: 引用次数
encoding: 物理编码类型
serializedlength: 序列化后的长度(注意这里的长度是序列化后的长度,保存为rdb文件时使用了该算法,不是真正存贮在内存的大小),会对字串做一些可能的压缩以便底层优化
lru:记录最近使用时间戳
lru_seconds_idle:空闲时间
String SDS数据结构
动态字符串
- int:保存long 型(长整型)的64位(8个字节)有符号整数,上面数字最多19位只有整数才会使用 int,如果是浮点数, Redis内部其实先将浮点数转化为字符串值,然后再保存。
- embstr(嵌入式的String):代表 embstr 格式的 SDS(Simple Dynamic String简单动态字符串),保存长度小于44字节的字符串
- raw:保存长度大于44字节的字符串
SDS数据结构
Redis中字符串的实现,SDS有多种结构(sds.h):
sdshdr5、(2^5=32byte)
sdshdr8、(2 ^ 8=256byte)
sdshdr16、(2 ^ 16=65536byte=64KB)
sdshdr32、 (2 ^ 32byte=4GB)
sdshdr64,2的64次方byte=17179869184G用于存储不同的长度的字符串。
len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串。
alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。
buf 表示字符串数组,真存数据的。
官网:https://github.com/antirez/sds
C语言 | SDS | |
---|---|---|
字符串长度处理 | 需要从头开始遍历,直到遇到 ‘\0’ 为止,时间复杂度O(N) | 记录当前字符串的长度,直接读取即可,时间复杂度 O(1) |
内存重新分配 | 分配内存空间超过后,会导致数组下标越级或者内存分配溢出 | 空间预分配 SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。惰性空间释放有空间分配对应的就有空间释放。SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。 |
二进制安全 | 二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 ‘\0’ 等。前面提到过,C中字符串遇到 ‘\0’ 会结束,那 ‘\0’ 之后的数据就读取不上了 | 根据 len 长度来判断字符串结束的,二进制安全的问题就解决了 |
int
命令示例:
set k1 123
当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。内部的内存结构表示如下:
Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!
set k1 123
set k2 123
server.h
object.c
redis7源代码:object.c
流程图
结论
只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。
embstr 与 raw 类型两者的区别见下图:
int | Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。 |
embstr | 当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片 |
raw | 当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr 结构 |
Hash数据结构
redis6:ziplist
结构:
hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。
Hash类型键的字段个数 小于 hash-max-ziplist-entries 并且每个字段名和字段值的长度 小于 hash-max-ziplist-value 时,
Redis才会使用 OBJ_ENCODING_ZIPLIST来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT的编码方式
结论:
- 哈希对象保存的键值对数量小于 512 个;
- 所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节)时用ziplist,反之用hashtable
ziplist升级到hashtable可以,反过来降级不可以:一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存而不会再转回压缩列表了。在节省内存空间方面哈希表就没有压缩列表高效了。
流程图
源码分析:t_hash.c
在 Redis 中,hashtable 被称为字典(dictionary),它是一个数组+链表的结构
OBJENCODING HT 编码分析:每个键值对都会有一个dictEntry
- OBJ_ENCODING_HT 这种编码方式内部才是真正的哈希表结构,或称为字典结构,其可以实现O(1)复杂度的读写操作,因此效率很高。
- 在 Redis内部,从 OBJ_ENCODING_HT类型到底层真正的散列表数据结构是一层层嵌套下去的
dict.h
源码分析:ziplist.c
为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组
ziplist是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
redis7:packlist
- 哈希对象保存的键值对数量小于 512 个;
- 所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节)时用listpack,反之用hashtable
- listpack升级到hashtable,反过来降级不可以
有ziplist为什么出现listpack
ziplist连锁更新问题
压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
案例:压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患
第一步:现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:
因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值
第二步:这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:
因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。
第三步:连续更新问题出现
entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点…一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」
结论:listpack 是 Redis 设计用来取代掉 ziplist 的数据结构,它通过每个节点记录自己的长度且放在节点的尾部,来彻底解决掉了 ziplist 存在的连锁更新的问题
listpack结构
https://github.com/antirez/listpack/blob/master/listpack.md
Total Bytes:为整个listpack的空间大小,占用4个字节,每个listpack最多占用4294967295Bytes。
num-elements:为listpack中的元素个数,即Entry的个数占用2个字节
element-1~element-N:为每个具体的元素
listpack-end-byte:为listpack结束标志,占用1个字节,内容为0xFF。
listpack如何解决连锁更新问题
和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项不再像ziplist列表项那样保存其前一个列表项的长度。
List
Redis6、7都是quicklist差异在于:
redis6:实际是ziplist
redis7:实际是listpack
listpack紧凑列表是用来替代 ziplist 的新数据结构,在 7.0 版本已经没有 ziplist 的配置了(6.0版本仅部分数据类型作为过渡阶段在使用)
quicklist
实际上是 zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。
redis6
Redis7的List的编码格式
list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个listpack
quicklist是listpack和linkedlist的结合体
Set
两种编码方式:
intset
hashset
- Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。
- 如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。
- 集合元素都是longlong类型并且元素个数<=set-max-intset-entries编码就是intset,反之就是hashtable
ZSet
两种编码格式
- ziplist+skiplist(redis6)
- listpack+skiplist(redis7)
redis6
当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳表作为有序集合的底层实现。否则会使用ziplist作为有序集合的底层实现
总结
redis6类型编码映射
数据类型对应的底层数据结构
- 字符串
int:8个字节的长整型。
embstr:小于等于44个字节的字符串。
raw:大于44个字节的字符串。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
- 哈希
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,
Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的 结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使 用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。
- 列表
ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节),
Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。
linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist作为列表的内部实现。quicklist ziplist和linkedlist的结合以ziplist为节点的链表(linkedlist)
- 集合
intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会用intset来作为集合的内部实现,从而减少内存的使用。
hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
- 有序集合
ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时,
Redis会用ziplist来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作 为内部实现,因为此时ziplist的读写效率会下降。
redis6数据类型以及数据结构的关系
redis7数据类型以及数据结构的关系
redis数据类型以及数据结构的时间复杂度
skiplist跳表
从单链表优化:索引升级
优化1
从这个例子里,我们看出,加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。
优化2
跳表是什么
skiplist是一种以空间换取时间的结构
优点:由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表
缺点:由于索引也要占据一定空间的,所以,索引添加的越多,空间占用的越多
时间复杂度
空间复杂度
比起单纯的单链表,跳表需要存储多级索引,肯定要消耗更多的存储空间
- 首先原始链表长度为n,
- 两两取首,每层索引的结点数:n/2, n/4, n/8 … , 8, 4, 2 每上升一级就减少一半,直到剩下2个结点,以此类推;如果我们把每层索引的结点数写出来,就是一个等比数列。
这几级索引的结点总和就是n/2+n/4+n/8…+8+4+2=n-2。所以,是O(n)
三三取首同理是等比数列…
优缺点
优点:
跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的
缺点:
维护成本相对要高,在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1)
新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(log n)