工作以来一直都再跟redis打交道,诸如热数据缓存、数据结构选型、缓存优化以及数据同步。于是就想去深入了解一下redis的原理,也好在之后的工作中可以用的更加得心应手。毕竟作为一个后端开发,redis一知半解说不过去嘛。。。
首先
在讨论原理之前,必须要说明的是,
1)真的去看了redis源码,可以说70%可以懂,剩下的底层调用说实话不熟悉
2)能够有这样的对redis的重新认识,真的要十分感谢《redis设计与实现》的作者,毕竟我还是个菜鸡。。
什么是redis?
这里可能要简单的说一下,redis是一种内存级别的、nosql的缓存技术(可以把他理解成一种游走在内存中的集装箱,也可以把它当做方便调用的数据结构对象)。【对象这一概念很重要,我在一段时间内都走近了一个误区,把redis当做了纯粹的数据结构】
什么时候我们会用到redis?
一般情况下,做过一点开发的都会回答"当然是缓存",很对,但是redis 的应用场景远不止步于次,还有分布式锁等等,这里不再赘述,有需要的可以去自行google。如今redis应用的大部分场景是分布式下使用,这就涉及到了主从复制,同步异步的选型等等。。(当然更深的我还没了解到,一知半解不敢说。。。)
redis的操作的是对象,而不是直接触碰数据结构
redis数据结构以及运作特征
动态字符串sds
在博客上已经有很多关于sds的解析,这里不再赘述了,简单说下,类比C语言的字符串,C语言的字符串可以理解成以「\0」结尾的字符数组,存储简单但是也造成了一定的问题,比如越界问题、字符串中间不能出现\0,计算长度的时间复杂度为O(n) , 如果涉及到加长字符串的问题,那牵扯的事情就更多了,比如扩展内存,字符移动等等。。redis的字符串,与其说是字符串不如说是表示字符串的数据结构,结构体中记录了:【内存申请的大小】,【当前字符串的长度】,【当前剩余未使用的内存大小】以及【数据本身】等,从这几个特征就可以看出以下几个特征:
1)可扩展
2)获取字符串长度的复杂度为O(1),
3)数据本身依旧是按照C模型存储,所以兼容部分c语言的字符串操作函数。
实际上:关于申请内存的方面,sds采用的是【贪婪申请、惰性释放】的策略,也就是字面意思,一方面方便扩展,另一方面提高效率。这种策略可以对比系统编程中的向磁盘写入、或者是从物理存储空间读出数据是异曲同工。
字典(又叫符号表或者关联数组)
保存键值对形式的值的抽象数据结构。其中键值对的值可以是字符串,也可以是list,还可以是hash。。也正因为字典的存值多样性,它也是redis对象中使用率非常高的数据结构(最高的当然是简单动态字符串。。),
那么字典这种数据结构也不去深入说了,这里只去提及redis中的字典的结构
结构特征(用三个结构体去表示一个字典)
一:table结构:
【哈希表数组】【哈希表大小】【掩码(与系统编程中的授权管理异曲同工)】【节点数量(也就是下面要提到的redis节点)】
二:node结构:
【键】【值(含有void*、以及整数)】【next指针】(实际上这里的数据结构就能够看出来,redis解决哈希冲突的方式是链地址法)
三:字典结构:
【类型特定函数指针】【私有数据】(前两者都是因多态字典而设置),
【哈希表【2】】(两个哈希表是用来rehash处理数据用的)【rehash索引】
其中在结构体中【值】得选型为什么会有整型与void* 做区分,有人觉得岂不是一个void*就可以了,那其实这个跟redis 的设计有关,redis作为内存型数据库,有两个非常关键的点:
1)效率
2)容量
考虑到键值重用,以及读写耗时,redis将一部分整数存入内存块中,并且不进行更改,当用到的时候就回去拿(通常数字0~9999,当然这个是默认值,一般会将其调整按照业务规划,这也就是为什么我们总说:能存数字的,就不要存字符串)。这个特征在其它redis对象中依然会有体现。
对于字典结构来说,最重要的一点莫过于rehash,那么什么时候需要做什么样的策略,如果你接触过线程池的话,那么这种扩展和回收的思想是相通的,redis会在需要的时候进行扩展和回收,当然,不会小于最开始申请的大小(惰性释放)。
rehash,redis不会在其本身申请的已使用的内存上进行rehash,而是会在另一个未使用的空间上进行rehash,重新分配节点。并且之后新进来的数据会在rehash的内存上进行增加,最后rehash完毕之后,会将rehash好的空间作为主空间,回收原有空间,并开辟出一个新的待rehash用的空间。(一般情况下,字典并不会一次性rehash,而是采用渐进式的rehash策略,也就是在非忙碌状态下进行rehash,尤其是数据量很大的情况下,一次性rehash是不可取的)
跳跃表(有序集合的专属数据结构)
跳跃表是有序的,它是为了有序集合的快速操作而存在的。跳跃表相关概念可以翻阅其它的博客。
跳跃表中节点的结构:
【后退指针】【分值】【对象】【层【前进指针】【跨度】】(由此可见,跳跃表之所以可以跳跃就是因为层结构,跨度记录了同层,指针记录了同层的哪一个节点。那么如何找到同层呢?redis用了一个非常有趣的方式,那就是随机。)
(总结上面两个结构体会发现,明明节点就可以搞定的,为什么在外面多加一层数据结构,这一方面方便操作管理整个数据结构,另一个方面我想应该是数据完整性和易升级)
跳跃表结构:
【节点结构】【节点数量】【最大层数】清晰明了,不再赘述。
整数集合
整数集合,保存整数值,不重复,并且无序,以下是整数集合结构:
【编码方式】(喜闻乐见的编码,方便而且通用)
【包含节点的数量】(不做过度解读)
【保存元素的数组】(这里要说的是,redis的整数集合的数组表示【int8_t contents[]】,这并不表示数组中存储的只能是int8_t的数据类型,真正的数据类型是由编码确定的)
整数集合设计中采用数组的方式,极大程度的节约了内存,并且其升级降级的策略也提高了其灵活性。
简单来说,集合的升级,升级依赖于encoding编码,其标识了当前集合中存储的元素类型,如果当前存储的都是16位大小的数字,那么当前的编码为int16_t,一旦有一个64的数字进来之后,集合就需要进行整体升级,整个数组会先将数组从尾部扩展,之后从数组中最后一个元素进行升级,依次向后推,最后形成一个升级后得数组。并将编码改变成升级后得编码。降低相反,它是从数组的头部进行扩展的。
压缩列表
压缩列表这种数据结构可以简单的理解成redis为了节省内存空间,提升工作效率而在内存中选择一块连续的地址空间,存储数据的数据结构,它十分适合小整数列表键和短字符串列表键的存储。
以下是压缩列表的数据结构:
【压缩列表大小】(指的是整个列表占用内存的字节数,内存重分配时使用)
【表尾偏移量】 (这方便了尾部寻址,方便扩容等其它的操作)
【节点数量】(值小于65535时,值就是节点数量,大于时需要遍历整个列表才能得知大小)
【末尾标记】
【节点】(结构体)(其实也就是整个压缩列表采用的是链式存储,旦它并不是记录下一个节点的地址信息,而是记录前一个节点的大小,充分的利用了空间没有产生浪费,又可以为优化列表长度提供依据)
以下是节点的结构体:
【前一个节点大小】(这个节点长度有一个特征:前一节点长度小于254时,此属性长度为1,大于等于254时,此属性则为长度4),(正是因为这个特殊的属性,在遇到所有的节点都为251~253时,一旦有相对大一些的节点进入后【插入到前面】,就会产生连锁更新,又因为整个数据都是在同一数据链条上,就会导致连锁反应)(但这种情况的发生少之又少)
【编码】
【数组】不再赘述
对象
之前看完这些的时候,我就认为redis存储也就是一种选型对应的一种数据结构,然后再进行存储就可以了,但实际上redis是面向对象的,起始redis并没有直接操作这些数据结构从而去形成一个键值对数据库,而是形成了一个对象系统,每个对象,通过不同的编码解析出不同的数据结构,同时可切换,可同时存在。同时对象可共享,不同的键可以共享同一个对象 ,从而来达到节约内存的目的。
对于redis 的对象来说:
要搞清楚,redis的键和值是两个对象,而redis 的键总是字符串对象,而当我们用type命令去探究数据类型的时候,显示的数据类型其实是值的数据类型,其实这也正合乎情理。
以下是对象的结构:
【类型】(记录着对象的类型)
【编码】(决定着底层的数据结构的选型)(其实每一个redis对象都对应了至少两种数据类型)
【泛型指针】(指向对应的底层数据结构)
一.字符串对象:
字符串对象有三种编码int ,embstr,raw,其中embstr我理解有像是压缩列表的字符串版本。
二.列表对象:
有两种编码格式,一个是压缩列表,另一个就是链表(其中编码都是可以相互专换的,redis优化,往往这些底层的重置是最基本的策略)
三.hash对象:
有两种编码格式,一个是压缩列表,一个是hash表
四.集合对象:
可以是intset(也就是整数集合作为底层实现),和hash表
五,有序集合对象:
实际上有序集合对象需要利用两种数据结构一起实现快速插入和遍历(也就是压缩列表和跳跃表)
redis命令
redis 的命令可以分为两种:1,针对特定类型的命令2,通用的命令
这两种命令实际上都需要先判断操作的值对象的数据类型,通用的命令实际上是多态实现。而针对特定类型的命令实际上又可以应对不同的编码格式。
redis对象的内存回收
redis的内存回收可以类比linux的硬链接计数,当操作时,操作的是同一个对象,每增加一个键去操作,增加计数+1,每当释放一个键,-1,当计数到达0的时候,对象才会真正释放。
redis对象共享
正因为对象共享,所以才会有默认存储的数字对象(为啥不存储字符串对象?得不偿失呗!)
redis对象空转时长
lru表示空转时间,当redis 所在内存不足时,空转时间更长的对象会被高优释放。
以上就是简单的总结,还有很多重要的事情没有涉及,后续会有更新,欢迎吐槽(菜鸟就是脸皮厚)。