上一篇中我们讲到了redis的4种底层数据结构支撑,这一篇学习存储数据结构的实现。
redis的值value支持5种类型数据的存储。分别是1. 字符串 2.列表 3.有序集合 4.哈希表 5.集合,千万不要觉得这些数据结构和上一篇中的有什么关系,数据结构只是一种通用的抽象化数据的理解方法 ,在哪都可以独立出来。相同的地方只是某个具体实现用同一种数据结构比较好,就像字符串,作键也可以,作值也可以。
由上图可知,5种数据的底层实现都不是唯一的。可以看出来,它们的底层实现是什么东西呢?不就是咱们上一篇所提到的4中数据结构么?至于还有一个整数集合和压缩列表,这是为了节省内存的存储简单数据类型的策略,当占用内存变大的时候,就升级为字典。下一篇专门说明一下这两个数据结构。
要实现上面这张图的数据关联关系,必须得应用到多态的概念。而C中是如何应用多态这个概念的呢?
通过结构体来模仿:
typedef struct redisObject {
// 类型
unsigned type:4;
// 对齐位
unsigned notused:2;
// 编码方式
unsigned encoding:4;
// LRU 时间(相对于 server.lruclock)
unsigned lru:22;
// 引用计数
int refcount;
// 指向对象的值
void *ptr;
} robj;
其中有这些成员:
type:redisObject类型(Type):5种类型
encoding:底层实现结构通过此项确定,上一节的4种基本类型,加上整数集合和压缩列表大约6种
ptr:初始化一个空的指针,根据以上两者的判断之后再将其指向确切的物理地址
知道了它的基本结构,所有的操作都是基于上面的结构来执行的:
1.对数据库执行一个操作时,首先根据key,查看数据库中是否存在对应的可供操作的redisObject,没有就返回null
2.查看redisObject的type是否支持要执行的操作(如集合有集合的操作命令,字典有字典的操作命令,这里判断是不是一一对应的)
3.根据redisObject的encoding属性选择对应的数据结构(上面这一步正确,操作命令对同一种数据结构(任何的底层实现)操作应该都是起效果的(可能物理上的更改不一样,但不影响客户端得到的结果),所以找到对应的数据执行对应操作)
4.返回处理结果
内存共享的理解
针对一些(1)常用的操作返回值大多都不大的情况,(2)还有一些常用的值的对象(像一些整数)
对此,redis并不严格区分不同数据结构,而让它们使用同一块内存,相当于把这些对象放入到一块共用的内存
常要使用的对象直接在其中调用,避免了重复分配,节省了CPU处理时间
字符串:
毫无疑问是redis使用得最多的数据结构,可以作key,这里可以用来作为 set / get 的操作对象。
它的底层有两种实现:
1.REDIS_ENCODING_INT 使用long类型保存long的值
2.REDIS_ENCODING_ROW 使用sdshdr保存sds,long long,double,long double...
第一种是保存long类型的存储,而且只有保存long时用它
除此之外其他字符串的保存都是使用第二种方法,(但是在保存数据到数据库中时,会自动尝试将其转换为第一种类型long式保存,为了节省空间)
哈希表:
它的底层实现也是两种:
1.REDIS_ENCODING_ZIPLIST 压缩列表
2.REDIS_ENCODING_HT 字典
创建新的哈希表时,默认先使用压缩列表作为底层实现数据结构,好处是基本不会产生多余的内存,要多少就申请多少,追加到压缩列表中。
只有当触发了某些条件才会转换成字典。
1.哈希表中某个键或者值的长度大于server.hash_max_ziplist_value(默认为64)
2.压缩列表中的节点数量大于server.hash_max_ziplist_entries(默认为512)
列表:
队列(链表实现)。2种底层实现:
1.REDIS_ENCODING_ZIPLIST
2.REDIS_ENCODING_LINKEDLIST
和哈希表类似,默认是压缩列表,之后也是触发某些条件才会变成双端链表
1.试图往列表中插入一个字符串值,长度大于server..list_max_ziplist_value(默认是64)
2.ziplist包含的节点超过server.list_max_ziplist_entries(默认值为512)
对于列表,还需要提到的一个是阻塞命令:
阻塞原语:BLPOP / BRPOP / BRPOPLPUSH,这些命令都可能造成客户端的阻塞。
当这些命令作用于空列表,造成阻塞(上面的命令根据字面意思就是删除(pop)一个节点,当没有节点的时候就会阻塞到有节点进来,让你删除,之后再释放)
如果被处理的列表不为空,它们就执行无阻塞版本的LPOP / RPOP / RPOPLPUSH (少了Blocked)
要解除阻塞:
1. 添加节点以供删除 ----- 被动脱离
2.超过最长阻塞时间 ----- 主动脱离
3.关机 ------ 强制脱离
集合:
set。底层也是2种实现:
1.REDIS_ENCODING_INTSET 整数集合
2.REDIS_ENCODING_HT
与之前不一样的是:
如果第一个进入到集合的元素是long long类型,那么就使用上面第一种编码方式
否则,就使用字典
转化依旧是有两个任意触发条件:
1.intset保存的整数值个数超过server.set_max_intset_entries(默认值为512)
2.从第二个元素开始,如果插入的元素类型不是long long的,就要转化成第二种
有序集:
有序集中的每一个元素都有一个score,根据score的大小进行排列(如果两个元素的 score 相同, 那么按字典序对 member 进行对比,决定排列顺序)
结果得到的是一个排序过的map。
ziplist的节点指针只能线性地移动,即使我们已经排好了序,在ziplist中查找某个元素也是O(N)的复杂度,每次执行删除或
添加修改都必须经过一次查找,那必然在O(N)之上。这时候为了除了效率低的问题,有序集采用了字典的结构:键---值。这里的键
值是以member作为key,score作为value。
通过检查给的member是否存在于有序集中
有就取出member对应的score值
这样在O(1)内就查出了两个需要的值。
还有一个快被忘了的数据结构:跳跃表.....
通过使用它,可以让有序集支持以下两种操作:
1.在 O(logN) 期望时间、O(N) 最坏时间内根据 score 对 member 进行定位(被很多底层函数使用)
2.范围性查找和处理操作,这是(高效地)实现 ZRANGE 、ZRANK 和 ZINTERSTORE等命令的关键
通过同时使用字典和跳跃表,有序集可以高效地实现按成员查找和按顺序查找两种操作
总结:
好好学习数据结构