五种基本数据类型
??Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
简单动态字符串(SDS)
SDS的定义
- free的值为0,表示这个SDS没有分配任何未使用的空间,也就是分配的空间全部被使用了;
- len属性的值为5,表示这个SDS保存了一个5个字节长的字符串;
- buf属性是一个char类型的数组,保存了字符串的内容,最后一个空间保存了空字符’\0’;
SDS和C字符串的区别
常数复杂度获取字符串长度
??C字符串在获取字符串长度的时候需要遍历整个字符串来获取字符串的长度,但是在SDS中存在len属性保存着字符串的长度,所以在获取字符串长度时只需要访问len属性就能获取到字符串的长度。保证了获取字符串长度不会称为Redis性能的瓶颈。
杜绝缓冲区溢出
??C字符串不记录自身长度带来的另一个问题是容易造成缓冲区溢出。SDS的空间分配策略完全杜绝了缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,首先会检查SDS空间是否满足需求,如果不满足的话,首先会自动将SDS的空间扩展至执行修改所需的大小,然后才会执行实际的修改操作,杜绝缓冲区溢出的问题发生。
减少修改字符串时带来的内存重分配次数
??C字符串在每次增长或者缩短时,程序都需要对这个C字符串的数组进行一次内存重分配操作。 ??SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联,通过未使用空间,SDS实现了空间预分配和惰性空间释放两种优化策略。 1、空间预分配 ??空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须使用的空间,还会为SDS分配未使用的额外空间。
- 当修改后SDS的长度小于1MB时,程序会对未使用空间分配同样大小的空间。
- 当修改后SDS的长度大于1MB时,程序会为未使用空间分配1MB大小的空间。
2、惰性空间释放 ??惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来,并等待将来使用。
二进制安全
??C字符串中的字符必须符合某种编码,除了字符串末尾之外不允许出现空字符,否则最先被程序读入的字符串将被误认为字符串的结尾。 ??而SDS中的API都是二进制安全的,可以用来保存一系列的二进制数据,因为SDS使用的是len属性的值而不是空字符串来判断字符串是否结束。
兼容部分C字符串函数
??虽然SDS的API都是二进制安全的,但是一样遵循了C字符串以空字符串结尾的惯例:API总会将SDS保存的数据的末尾设置为空字符,并且总会在为buf数组分配空间时多分配一个字节来容纳这个空字符,保证了保存文本数据的SDS可以重用一部分已有的函数。
总结
链表
链表和链表结点的实现
链表结点的实现 链表的实现 链表
- 双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1);
- 无环:表头节点的prev指针和表尾结点的next指针都指向NULL,对链表的访问以NULL为终点;
- 带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的头节点和尾节点的复杂度为O(1);
- 带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1);
- 多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。
字典
??Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对。
哈希表
- table属性是一个数组,数组中的每个元素都是一个指向哈希表节点的指针;
- 每个哈希表节点保存着一个键值对;
- size属性记录了哈希表的大小,即table数组的大小;
- used属性则记录了哈希表目前已有节点的数量;
- sizemask属性的值总是等于size-1,这个值和哈希值决定了一个键应该被放到table数组的哪个索引上。
哈希表节点
- key属性保存着键值对中的键;
- v属性保存着键值对中的值,其中键值对的值可以是一个指针,或者是一个uint64_t整数,又或者是一个int64_t整数。
- next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,以此来解决哈希冲突的问题。
字典
??type属性和privdata属性时针对不同类型的键值对,为创建多态字典而设置的。
跳跃表
??跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找。 ??Redis使用跳跃表作为有序集合的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳表作为有序集合件的底层实现。
zskiplist
- header:指向跳跃表的表头节点
- tail:指向跳跃表的表尾结点
- level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)
- length:记录跳跃表的长度,及跳跃表目前包含节点的数量(表头节点不计算在内)
zskiplistNode - 层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层。每层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,跨度则记录了前进指针所指向节点和当前节点的距离。
- 后退指针(backward):节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。
- 分值(score):各个节点中的1.0、2.0等是节点所保存的分值。在跳跃表中,节点按照各自保存的分值从小到大排列。
- 成员变量(obj):各个节点中的o1、o2等是节点所保存的成员对象。
- 注:表头节点和其他节点的构造是一样的,不过表头节点的这些属性都不会被使用到,所以图中省略了这部分。
跳跃表节点
- 层:跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,一般来说,层的数量越多,访问其他节点的速度就越快。
- 前进指针:每个层都有一个指向表尾方向的前进指针,用于从表头向表尾方向访问节点
- 跨度:层的跨度用于记录两个结点之间的距离,两个节点之间的跨度越大,他们相距的就越远;指向NULL的所有前进指针的跨度都为0,因为它们没有连向任何节点。
- 后退指针:节点的后退指针用于从表尾向表头方向访问节点,因为每个节点只有一个后退指针,所以每次只能后退到前一个节点。
- 分值:节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有结点都按照分值从小到大来排序。
- 成员:结点的成员对象是一个指针,它指向一个字符串对象,而字符串对象则保存着一个SDS值。
跳跃表
??依靠多个跳跃表节点就可以组成一个跳跃表。
- header用来指向跳跃表的表头;
- tail用来指向跳跃表的表尾结点;
- length用来记录跳跃表中节点的数量;
- level用来在O(1)复杂度内获取跳表汇总层高最大的节点的层的数量,注意表头节点的层高并不计算在内。
整数集合
??整数集合(intset)是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。
整数集合实现
??整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t、或者int64_t的整数值,并且保证集合中不会出现重复元素。
- comtents数组是整数集合的底层实现,整数集合中每个元素都是contents数组的一个数组项,每个项在数组中按值的大小有序的排列,并且不包含重复项。
- length属性记录了整数集合包含的元素数量,也就是数组的长度。
- 注:intset将数组声明为int8_t类型,但是数组并不存储int8_t的值,数组的真正类型取决于encoding属性的值。
升级
??当我们要新插入一个元素到整数集合里面时,如果新元素的类型比整数集合现有所有元素的数据类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合中 步骤:
- 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
- 将底层数组的所有元素都转换为与新元素相同的类型,将类型转换后的元素放到正确的位置上,保证有序性不变。
- 将新元素添加到底层数组里面。
好处: - 提升灵活性
- 节约内存
降级
??整数集合不支持降级操作,一旦对数组进行了升级,就会一直保持升级后的状态。
三种特殊类型
Geospatial 地理空间
??主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。存在6个命令:
- geoadd:添加位置
- geodist:返回给定位置距离
- geohash:返回一个11字符的geohash字符串
- geopos:返回给定名称经纬度
- georadius:找到某一给定位置的半径内元素
- georadiusbymember:以一成员变量为中心,查找指定半径范围内元素。
Hyperloglog
??HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大 时,计算基数所需的空间总是固定的、并且是很小的。典型的使用场景是统计独立访客。 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素 的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。常用命令:
- PFADD key element :添加指定元素到Hyperloglog中。
- PFCOUNT key : 返回给定Hyperloglog的基数估算值。(结果并不准确)
- PFMERGE destkey sourcekey :将多个Hyperloglog合并为一个Hyperloglog
Bitmap
??位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在 Bitmap 中叫做偏移量。Bitmap的长度与集合中元素个数无关,而是与基数的上限有关。 bitmap是位图存储的,都是二进制来进行记录,所有只要是两种状态值的场景,都可以使用 bitmaps来存储,比如:登录,未登录;打卡,未打卡;活跃,不活跃等。常用命令:
- setbit:在位图中添加数据
- getbit:查看位图上某个位置的值
- bitcount:统计位图上value等于1的个数
|