底层数据结构
简单动态字符串
SDS的定义
len:SDS的总长度 alloc:已分配的长度 buf:存放实际的字符串(末尾要加一位的结束符)
SDS与C字符串的区别
SDS封装了C字符串,用空间换效率,满足了Redis要求的安全性、效率性以及功能方面的要求
1.常数复杂度获得字符串长度
通过len属性直接获取字符串的长度,len长度在SDS的API执行时自动修改
2.避免缓冲区溢出
SDS的空间分配策略杜绝了发生缓冲区溢出的可能性
- 当需要对SDS进行修改时,会先检查SDS的空间是否满足修改所需的大小
- 如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小
- 在确保空间足够的情况下才会执行实际的修改操作
3.减少修改字符串带来的重分配次数
空间预分配:SDS每次扩展的时候会额外分配未使用空间,减少分配次数
- 修改后的长度小于1MB则空间翻倍
- 修改后的空间大于1MB则(n+1)MB
惰性空间释放:SDS每次进行缩短操作的时候不会进行len缩小,为将来有需要的时候做准备
4.二进制安全
由于C语言的字符串必须符合某种编码,而且除了字符串的末尾以外不能包含空字符,所以存储的字符有限,而SDS就没有这些限制。 SDS以处理二进制的方式来处理SDS存放在buf数组里的数据,程序不会对其中的数据做任何限制、过滤、或者假设。数据在写入和读取时是一样的。 这都归功于SDS的alloc属性来判断字符串是否结束。
5.兼容部分C字符串函数
因为SDS存储时buf的结尾会添加一个结束符,来模拟一个C字符串
6.比较
C字符串 | SDS |
---|
获得字符串长度的复杂度为O(N) | 获得字符串长度的复杂度为O(1) | API是不安全的,可能造成缓冲区溢出 | API是安全的,不会造成缓冲区溢出 | 修改字符串长度N必然需要执行N次内存重分配 | 修改字符串长度N次最多需要执行N次内存重分配 | 只能保存文本数据 | 可以保存文本或者二进制数据 | 可以使用<string.h>库中的函数 | 可以使用一部分<String.h>库中的函数 |
链表
链表和链表节点的实现
多个链表可以通过pre和next指针组成双端链表 Redis的链表实现的特性可以总结如下
- 双端:链表节点带有pre和next指针
- 无环:链表是无环的
- 带头尾指针:可以通过O(1)复杂度访问头尾
- 带链表长度计数器:O(1)复杂度获得长度
- 多态:链表节点可以通过void*指针保存节点值,保存不同类型的值
字典
字典的实现
Redis的字典使用哈希表来作为底层实现,一个哈希表当中可以有多个哈希表节点,每个哈希表节点就保存了字典中的一个键值对
哈希表
哈希表的table属性是一个数组,数组中的每个元素都指向一个dirEntry结构的指针 size属性记录了哈希表的大小
哈希算法:通过n & (size - 1)
解决键冲突:采用链地址法
rehash
随着操作的不断执行,哈希表保存的键值对会增加或减少,这时程序就需要根据哈希表的大小进行相应的扩容。这是通过rehash来实现的
rehash过程
- 根据ht[0]的已使用空间 n 来给ht[1]分配空间
- 扩展操作:大于 2n 的第一个数字
- 收缩操作:大于 n 的第一个数字
- 将保存在ht[0]中的所有键值对rehash到ht[1]上面
- 此时ht[0]是空表,将ht[0]指向ht[1],ht[1]置为空,以便下一次使用
渐进式rehash
如果数据规模很大,一次性rehash会造成系统阻塞,所以需要渐进式rehash,将ht[0]中的键值对分多次、渐进式地转移到ht[1]中
- 为ht[1]分配送检,让字典同时拥有ht[0]和ht[1]两个哈希表
- 在字典中维持一个索引计数器rehashidx,设置为0,表示将要转移第0个桶位
- 每次对字典的读或写操作除了命令本身外,还会顺带将ht[0]的rehashidex桶位上的所有键值对转移到ht[1]表中,并将rehashidx+1
- 随着字典操作的不断进行,最终在某个时间点上,ht[0]的所有键值对都会被rehash到ht[1]上,然后设置rehashidx为-1,表示已完成
渐进式rehash采用的是一种分而治之的思想,将一次性的rehash操作分摊到对字典的每个添加、删除、查找和更新操作上,从而避免了rehash带来的庞大开销
注意:查找操作会在ht[0]和ht[1]表当中分别查找,新添加的键会放在ht[1]表中,保证ht[0]只减不增。
跳跃表
跳跃表是一个有序数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的 跳跃表头
- header:指向头结点
- tail:指向尾结点
- level:记录最大层数(不考阔表头节点)
- length:记录跳跃表长度
跳跃表节点
- level:
- 前进指针:记录当前层的下一个节点位置
- 跨度:下一个节点位置与当前节点位置的举例
- backward:后退指针:用于表尾向表头遍历
- score:分值,用于分值从小到大排序
- obj:成员对象
跳跃表节点
层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度。 前进指针:遍历时需要从高到低寻找跨度为1的层级前进 跨度:用来计算排位,即元素在表当中的为此 后退指针:用于倒序遍历跳跃表中的所有节点 分值和成员:字典排序分值来确定先后顺序,小在前,大在后
整数集合
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现
整数集合的实现
encoding:编码方式,有16位、32位、64位三种实现 length:集合元素的数量 contents[]:保存元素的数组,从小到大排序,不包含任何重复项,数据类型真正取决于encoding
升级
当我们需要将一个新元素添加到整数集合当中时,如果新元素的类型(encoding)比现有的元素类型都要长时,整数集合需要先进行升级,将content中的所有数据都升级为新元素的类型,然后再添加新元素
升级步骤
- 根据新元素的类型,扩展整数集合底层数组空间大小,并为新元素分配空间
- 将底层数组中现有的元素转换成相同类型,然后将类型转换后的元素放到正确的位置上(先往后放)
- 将新元素添加进去
升级的好处
- 提升整数集合的灵活性:动态升级,不必担心类型错误的出现
- 尽可能的节约内存:只有需要的时候才会进行升级
不支持降级
压缩列表
压缩列表是列表建和哈希键的底层实现之一。用于保存少量数值,是为了节约内存而开发的。
压缩列表的构成
zlbytes:记录整个压缩列表所占用的内存字节数 zltail:记录压缩列表表尾距离起始地址共有多少字节,通过zltail可以获得表尾节点地址 zllen:记录了压缩列表中的节点数量 entryX:压缩列表包含的各个节点,节点的长度由节点保存的内容决定 zlend:特殊值0xFF,用于标记压缩列表的末端
压缩列表节点的构成
压缩列表节点可以保存一个字节数组或一个整数值 字节数组:2^6-1, 2^14-1, 2^32-1三种长度之一 整数值:4bit(0~12)之间的无符号数,1B的有符号整数,3B的有符号整数,int16,int32,int64
-
previous_entry_length:0xFE00002766
- 如果前一个节点长度小于254字节,则previous_entry_length为1
- 如果前一个节点长度大于254字节,则previous_entry_length为5,然后第一个字节位0xFE
程序可以通过指针运算,根据当前节点的起始地址计算出前一个节点的起始地址 -
encoding 记录节点的content属性所保存数据的类型
- 00、01、10是字节数组编码,表示content为字节数组,分别代表1/2/5字节
- 11表示整数编码
-
len:剩下的4B记录长度,记录节点的content属性所保存数据的长度 -
content:记录具体存储的内容
连锁更新
在压缩列表中,由于previous_entry_length属性会根据前一个元素的大小而发生位数的变化,同时这种变化还可能会连续造成后续元素的变化,这样就产生了连锁更新的现象(前一个元素长度变化导致) 如果出现了很多连锁更新,那么会对性能造成极大的影响,但是发生的概率很小,而且三五个节点的连续更新对性能的影响不是很大。
底层数据结构总结
简单动态字符串
- Redis只会使用C字符串作为字面量,在大多数的情况下,Redis采用SDS作为字符串表示
- 常数复杂度获取字符串长度
- 杜绝缓冲区溢出
- 减少修改字符串长度时所需的内存重分配次数
- 二进制安全
- 兼容部分C字符串函数
链表
- 广泛应用于实现Redis的各种功能,比如列表键、发布和订阅、慢查询、监视器等
- 每个链表节点由一个ListNode结构来表示,每个节点都有一个指向前置节点和后置节点的指针,所以是双端链表
- 每个链表由一个list结构来表示,这个结构带有表头节点指针、表尾节点指针,以及链表长度等信息
- 因为链表的前置节点和后置节点的末尾元素都指向null,所以Redis的链表实现是无环链表
- 通过为链表设置不同的类型特定函数,Redis的链表可以用于保存各种不同类型的值
字典
- 字典被广泛用于实现Redis的各种功能,其中包括数据库和哈希键
- Redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用
- 当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis采用MurmurHash2算法来计算哈希键的哈希值
- 哈希表使用链地址法来解决键冲突,被分配到同一个索引上的多个键值对会连接成一个单向链表
- 在对哈希表进行扩展或者收缩操作时,程序需要将现有哈希表包含的所有键值对rehash到新哈希表里面,并且这个rehash过程并不是一次性完成的,而是渐进式完成的
跳跃表
- 跳跃表是有序集合的底层实现之一
- Reids跳跃表是由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息(比如表头节点、表尾节点、长度),而zskiplistNode则用于表示跳跃表节点
- 每个跳跃表节点的层高都是1至32之间的随机数
- 在同一个跳跃表当中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的
- 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序
整数集合
- 整数集合是集合键的底层实现之一
- 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型,改变这个数组的类型
- 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
- 整数集合只支持升级操作,不支持降级操作
压缩列表
- 压缩列表是一种为节约内存而开发的顺序型数据结构
- 压缩列表被用作列表键和哈希键的底层实现之一
- 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或整数值
- 添加新节点到压缩列表,或者从压缩列表中删除节点,可能会引发连锁更新操作,但这种操作出现的几率不高
|