Redis底层数据结构详解

我们知道Redis提供了五种数据结构:字符串,链表,集合,哈希表,有序集合这五种结构,并提供了各自的api接口供我们操作。虽然我们经常使用Redis提供的各种api,但是我们很少知道Redis底层是如何实现这几种结构的,接下来一起看下Redis底层的几种数据结构,这几种数据结构在很多Redis核心功能上随处可见。
一.简单动态字符串
Redis底层使用C语言编写,所以大多数人都会以为Redis的字符串结构就是直接使用了C语言的字符串,其实不然,Redis自定义了一个简单动态字符串结构,作为Redis中字符串的实现。
比如我们使用命令:set msg "hello world",这时候Redis会创建两个动态字符串对象,"msg"对象和"hello world"对象,Redis系统中大量使用了简单动态字符串,接下来我们看看Redis为什么自定义了动态字符串结构,动态字符串比C语言自带的字符串强在哪里。
动态字符串结构里定义了几个属性:len(int),free(int),buf(char[]),其中len用来表示数组中已使用字节的数量,free用来表示数组中未使用字节的数量,buf数组用来保存字符串。动态字符串遵循C语言字符串中以空字符'\0'结尾的特征,这样的好处是可以重用一部分C语言自带字符串函数。
Redis为什么自定义了简单动态字符串而不是直接使用C语言的字符串呢?简单动态字符串有哪些好处呢?
1.可以常数复杂度获取字符串长度
因为简单动态字符串保存了len属性,我们可以通过len属性直接获取字符串长度,而C语言的字符串则需要遍历字符串数组才能获取字符串长度。
2.防止缓冲区溢出
C语言字符串的另一个问题是容易造成缓冲区溢出。假如我们对字符串str执行strcat字符串拼接操作,如果str没有分配足够的内存,就会产生缓冲区溢出问题。如果str和str2在内存中相邻,当我们为str执行strcat之前没有为str分配足够的空间,那么strcat执行后,str的数据将溢出到str2的空间中,导致str2保存的内容被意外更改。而简单动态字符串杜绝了这一问题,当我们需要对简单动态字符串进行修改时,api会检查空间是否满足修改所需的需求,如果不满足,api会自动将字符串扩容到需要的大小,再执行修改操作。
3.减少修改字符串导致内存重分配的次数
对于C语言字符串来说,每次增长或缩短字符串,程序都需要对这个字符串数据进行内存重分配,因为内存重分配相对来说较复杂且耗时,所以Redis的动态字符串优化了这一步骤,通过未使用空间free解除了每次修改字符串都会内存重分配的问题。简单动态字符串实现了空间预分配和惰性空间释放的策略。
空间预分配即当对一个字符串进行修改需要扩展空间的时候,程序不仅为字符串分配修改需要的空间,还会分配额外的未使用空间,使用free记录未使用空间。通过空间预分配策略,Redis可以减少连续执行字符串增长所需的内存重分配次数。
惰性空间释放即当一个字符串需要缩短空间的时候,使用free记录需要缩短的空间,等待将来使用,而不是直接执行内存重分配,需要缩短的空间记录在free里面。动态字符串提供了相应的API,让我们在需要时,真正释放未使用的空间
4.二进制安全
C语言字符串由于都以字符'\0'结尾,所以我们不能在C语言字符串中不能存储任何包含空字符的数据,否则会被误认为字符串的结尾了,这些限制使得不能保存图片,音频等数据,而Redis由于使用len记录数据长度,而不是使用空字符判断字符串是否结束,所以简单动态字符串可以存储包含空字符的数据。
以上就是Redis中简单动态字符串和C语言字符串的区别以及Redis为什么使用简单动态字符串的原因,后续我们提到的字符串统一理解为Redis的简单动态字符串,而不是C语言中的字符串。
二.链表
Redis里的链表并没有什么特别需要说明的地方,和其他语言中的链表类似,定义了链表节点listNode结构,包含prev(listNode)属性,next(listNode)属性,value属性的结构,同时使用list来持有链表,list的结构包含head(listNode)属性,tail(listNode)属性,len(long)属性,还有一些方法,如复制,释放,对比函数。这几个属性都比较好理解,这里不再介绍。
三.字典
字典又称为符号表,是一种保存键值对的抽象数据结构,相当于Java中的HashMap结构。字典在Redis中的应用相当广泛,比如Redis中的所有键值对就是使用字典来保存的,具体字典的功能这里不详细介绍。
其实本来不打算介绍Redis字典的实现了,因为使用过Java的可能都知道HashMap结构,并清除其实现原理,Redis里的字典实现原理和HashMap基本一致,都使用了链式结构来解决键冲突的问题,即当有两个键hash到统一节点是,使用链来连接两个节点。不过Redis里的字典有一个地方和HashMap有很大区别,即rehash的时候Redis使用了渐进式扩展的方式扩展字典结构。简单说下rehash,如果字典保存的数据len超过了字典数组的size,即哈希表保存的键值对数量太多或者太少时,哈希表会进行相应的扩展或者收缩。Redis的字典保存了两个哈希表,ht[0]和ht[1],大多数时候ht[1]执行null,在rehash的时候ht[1]会指向一个两倍于ht[0]的空字典数组,将ht[0]里的数据全部移动到ht[1]里,移动完后将ht[0]执行ht[1],释放ht[0]的空间,执行null,将ht[0]变为ht[1]。
Redis字典的渐进式rehash
说到这里就来看下Redis里的渐进式rehash,实现渐进式rehash的目的只要是为了避免字典里的数据过大,一次性将致谢键值对全部rehash到ht[1]的话,庞大的计算量会导致服务器在一段时间内停止服务。所以Redis不是使用一次性将ht[0]的所有键值对全部rehash到ht[1],而是分多次,渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。
Redis的字典中维持一个索引计数器rehashidx,在正常的时候rehashidx的值为-1,表示没有在rehash,当rehash时,将rehashidx设置为0,表示rehash开始,在rehash期间,每次对字典执行添加,删除,查找或者更新操作时,程序除了执行指定的操作外,还会将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],完成后,rehashidx属性+1。随着字典操作的不断执行,最终在某个时间点,ht[0]的数据全部rehash到ht[1]上,这是将rehashidx设为-1,表示rehash操作完成。
因为渐进式rehash时,字典会同时使用ht[0]和ht[1],所以rehash期间,字典的所有操作都会在两个哈希表上进行,程序会有限在ht[0]上查找,如果没找到就会在ht[1]里面找。另外渐进式rehash期间,新添加到字典的键值对一律会被添加到ht[1]里面,ht[0]不执行任何添加操作。
四.跳跃表
跳跃表是一种有序数据结构,主要是用来解决有序链表的快速查找问题,这里简单介绍下跳跃表,感兴趣的可以查下跳跃表的资料,很有意思。
跳跃表可以支持平均O(logN),最坏O(N)复杂的节点查找。在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且跳跃表实现简单,所以不少程序使用跳跃表代替平衡树。
加入我们有一个有序链表1->3->5->7->9->11->13->15,虽然我们知道是有序的,但是限于链表的实现原理,我们不能使用二分查找方法来快速查找元素。跳跃表就是用来解决这个问题的。跳跃表会在刚才的1-15个节点的基础上,再添加一层节点,比如包含3->7->11->15作为第二层链表,7->15作为第三层链表,这样我们可以从最上层节点查找,找的到直接返回,找不到的话也能确定下下一层的方位,在下一层的方位上查询,基于这一思想实现跳跃表的快速查找。
五.压缩列表
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的实现。压缩列表的主要作用是为了节约内存。
以上就是Redis底层的数据结构,可以看到Redis为了实现庞大的功能,自定义了几个十分优雅的数据结构,里面有很多值得借鉴的地方。
参考内容:Redis设计与实现-黄健宏
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值