目录
LIST 512 64 压缩列表 ziplist-quicklist-listpack 双向链表消息队列 |微信点赞
HASH 压缩列表 哈希表 512 64 缓存易变化的对象、购物车 ID 类别、数量
SET 整数集合、哈希 元素为整数、数量小于512 --INTSET 点赞、共同关注、抽奖(去重)
SDS 简单动态字符串 保存文本数据和二进制数据 O(1)时间复杂度,获取长度 可以自动扩容 创建新数组,并复制元素
Redis字符串是怎么实现的? String int (整数) embstr(字符串<=44) raw(字符串长度>44)
在ZIPLIST数据结构下,查询节点个数的时间复杂度是多少? (这个是查询节点个数)
LINKEDLIST编码下,查询节点个数的时间复杂度是多少?
Hash查找某个key的平均时间复杂度是多少?(这个是查询某个KEY 不是节点个数)
Redis中HashTable查找元素总数的平均时间复杂度是多少?
redis (Zset)为什么不使用b+树,而是用跳表 不考虑磁盘IO,跳表实现简单,不需要平衡操作
为什么用跳表而不用平衡树? 内存占用少 范围查询简单 实现简单(不用保持子树平衡)
Redis对象类型
Redis是key-value存储,key和value在Redis中都被抽象为对象,key只能是String对象,而Value支持丰富的对象种类,包括String、List、Set、Hash Set、Sorted Set(Zset)、Stream等。
STRING 缓存对象、分布式锁、共享Session,计数
String类型的底层的数据结构实现主要是SDS(简单动态字符串)。应用场景主要有:
- 缓存对象:例如可以用STRING缓存整个对象的JSON。
- 计数:Redis处理命令是单线程,所以执行命令的过程是原子的,因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
- 分布式锁:可以利用SETNX(SET if Not eXists )命令。
-
- 如果 key 不存在,则将 key 的值设置为 value。
- 如果 key 已经存在,则 SETNX 命令不会对 key 的值进行任何操作,保持原有的值不变
- SETNX 命令返回一个整数值,表示设置成功与否。当 key 不存在时,返回 1 表示设置成功;当 key 已经存在时,返回 0 表示设置失败。
- 共享Session信息:服务器都会去同一个Redis获取相关的Session信息,解决了分布式系统下Session存储的问题。
LIST 512 64 压缩列表 ziplist-quicklist-listpack 双向链表消息队列 |微信点赞
List类型的底层数据结构是由双向链表或压缩列表实现的:
- 如果列表的元素个数小于512个,列表每个元素的值都小于64字节,Redis 会使用压缩列表作为List类型的底层数据结构;
- 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为List类型的底层数据结构;
在 Redis 3.2版本之后, List 数据类型底层数据结构只由 quicklist 实现,替代了双向链表和压缩列表。
在 Redis 7.0 中,压缩列表数据结构被废弃,由listpack 来实现。应用场景主要有:
- 微信朋友圈点赞:要求按照点赞顺序显示点赞好友信息,如果取消点赞,移除对应好友信息。
- 消息队列:可以使用左进右出的命令组成来完成队列的设计。比如:数据的生产者可以通过Lpush命令从左边插入数据,多个数据消费者,可以使用Rpop命令移除列表尾部的数据。
HASH 压缩列表 哈希表 512 64 缓存易变化的对象、购物车 ID 类别、数量
Hash类型的底层数据结构是由压缩列表或哈希表(HashTable)实现的:
- 如果哈希类型元素个数小于512个,所有值小于64字节的话,Redis 会使用压缩列表作为Hash类型的底层数据结构;
- 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为Hash类型的底层数据结构。
在Redis 7.0中,压缩列表数据结构被废弃,交由listpack 来实现。
---------------------------------------------------------------------------------------------------------------------------
应用场景主要有:
缓存对象:一般对象用 String + Json存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash类型存储。
购物车:以用户id为 key,商品id为 field,商品数量为 value,恰好构成了购物车的3个要素。(种类、数量、用户ID)
使用 hash 比使用 string 更便于进行序列化,我们可以将一整个用户对象序列化,然后作为一个 value 存储在 Redis 中,存取更加便捷。
SET 整数集合、哈希 元素为整数、数量小于512 --INTSET 点赞、共同关注、抽奖(去重)
Set类型的底层数据结构是由哈希表或整数集合实现的:
- 如果集合中的元素都是整数且元素个数小于512个,Redis会使用整数集合作为Set类型的底层数据结构;
- 如果集合中的元素不满足上面条件,则Redis使用哈希表作为Set类型的底层数据结构。
应用场景主要有:
- 点赞:key 是文章id, value是用户id。
- 共同关注:Set类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。key可以是用户id, value 则是已关注的公众号的id。
- 抽奖活动:存储某活动中中奖的用户名,Set类型因为有去重功能,可以保证同一个用户不会中奖两次。
Zset 压缩列表 跳表+HashTABLE 元素个数128 64 --ziplist 整体不是有序的 hash计算位置 ziplist有序 排行榜score是个权重 double浮点型 64位,类似雪花算法的ID构成,将时间元素添加进去
Zset 类型的底层数据结构是由压缩列表或跳表+hashtable(范围查询用跳表,等值查询(根据成员来查找分值)用hashtable)实现的:
- 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
可以根据元素的权重来排序,可以自己来决定每个元素的权重值。比如说,可以根据元素插入Sorted Set 的时间确定权重值,先插入的元素权重小,后插入的元素权重大。
应用场景主要有:
- 在面对需要展示最新列表、排行榜等场景时,如果数据更新频繁或者需要分页显示,可以优先考虑使用Zset.
- 排行榜:有序集合比较典型的使用场景就是排行榜。例如学生成绩的排名榜、游戏积分排行榜、视频播放排名、电商系统中商品的销量排名等。
BitMap 数据量大、二值计算 签到、判断用户登录态
bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
可以用于签到统计、判断用户登陆态等操作。
HyperLogLog 基数统计
HyperLogLog用于基数统计,统计规则是基于概率完成的,不准确,标准误算率是0.81%。优点是,在输入元素的数量或者体积非常非常大时,所需的内存空间总是固定的、并且很小。比如百万级网页UV计数等;
GEO
主要用于存储地理位置信息,并对存储的信息进行操作。底层是由Zset实现的,使用GeoHash编码方法实现了经纬度到Zset中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为Zset元素的权重分数。
Stream 为消息队列设计的数据类型
Redis专门为消息队列设计的数据类型。相比于基于 List类型实现的消息队列,有这两个特有的特性:
- 自动生成全局唯一消息ID
- 支持以消费组形式消费数据
方法缺陷:不能持久化,无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息。
Redis底层数据结构
SDS 简单动态字符串 保存文本数据和二进制数据 O(1)时间复杂度,获取长度 可以自动扩容 创建新数组,并复制元素
- SDS不仅可以保存文本数据,还可以保存二进制数据。
- O(1)复杂度获取字符串长度,因为有Len属性。
- 不会发生缓冲区溢出,因为SDS 在拼接字符串之前会检查空间是否满足要求,如果空间不够会自动扩容,所以不会导致缓冲区溢出的问题。
链表
节点是一个双向链表,在双向链表基础上封装了listNode这个数据结构。包括链表节点数量len、以及可以自定义实现的dup(复制)、 free(释放)、match(比较)函数。
- listNode链表节点的结构里带有 prev和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表;
- list结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1);
缺陷:
- 链表每个节点之间的内存都是不连续的,无法很好利用 CPU缓存。能很好利用CPU缓存的数据结构是数组,因为数组的内存是连续的。
- 保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。
压缩列表
- 压缩列表是由连续内存块组成的顺序型数据结构,类似于数组。不仅可以利用 CPU缓存,而且会针对不同长度的数据,进行相应编码,例如,对于较小的整数可以使用整数编码,而对于较长的字符串则使用字节数组编码。这种方法可以有效地节省内存开销。
- 不能保存过多的元素,否则查询效率就会降低;
- 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题。
缺陷:
- 空间扩展操作也就是重新分配内存,因此连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,直接影响到压缩列表的访问性能。
- 如果保存的元素数量增加了,或是元素变大了,会导致内存重新分配,会有连锁更新的问题。
- 压缩列表只会用于保存的节点数量不多的场景,只要节点数量足够小,即使发生连锁更新也能接受。
quicklist
其实 quicklist 就是双向链表+压缩列表组合, quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。
quicklist 解决办法:通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。
listpack
listpack没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当向listpack加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
连锁更新指的是在修改一个节点时,由于需要重新分配内存空间,导致相邻节点的位置发生变化,进而需要更新相邻节点的指针和长度信息。这可能会触发一系列的更新操作,从而带来性能上的开销。
哈希
哈希表是一种保存键值对(key-value)的数据结构。优点在于能以O(1)的复杂度快速查询数据。Redis采用了拉链法来解决哈希冲突,在不扩容哈希表的前提下,将具有相同哈希值的数据串起来,形成链表。
渐进式哈希的过程如下: 用于在计算哈希的同时逐步完成计算,从而提高性能和减少延迟。
Redis定义一个dict结构体,这个结构体里定义了两个哈希表(ht[2])。
- 给ht2分配空间;
- 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将ht1中索引位置上的所有数据迁移到ht2上;
- 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表1」的所有key-value迁移到「哈希表2」,从而完成rehash 操作。
Rehash(重新哈希)是 Redis 中用于扩容哈希表的操作,它发生在当哈希表的负载因子(load factor)超过一定阈值时,为了保持哈希表的性能,需要对哈希表进行扩容。
渐进式哈希的触发条件?
负载因子=哈希表已保存节点数/哈希表大小
- 当负载因子大于等于1,没有执行RDB快照或没有进行 AOF重写的时候,就会进行rehash 操作。
- 当负载因子大于等于5时,此时说明哈希冲突非常严重了,不管有没有有在执行RDB快照或AOF重写,都会强制进行 rehash操作。
跳表
一种多层的有序链表,能快读定位数据。当数据量很大时,跳表的查找复杂度就是O(logN)。
节点同时保存元素和元素的权重,每个跳表节点都有一个后向指针,指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,倒序查找时方便。
跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来。
Zset数组中一个属性是level数组,一个level数组就代表跳表的一层,定义了指向下一个节点的指针和跨度。
跳表的查找过程?
查找一个跳表节点的过程时,跳表会从头节点的最高层开始,逐一遍历每一层。在遍历某一层的跳表节点时,会用跳表节点中的SDS类型的元素和元素的权重来进行判断:
- 如果当前节点的权重小于要查找的权重时,跳表就会访问该层上的下一个节点。
- 如果当前节点的权重等于要查找的权重时,并且当前节点的SDS类型数据小于要查找的数据时,跳表就会访问该层上的下一个节点。
- 如果上面两个条件都不满足,或者下一个节点为空时,跳表就会使用目前遍历到的节点的level数组里的下一层指针,然后沿着下一层指针继续查找。
跳表的相邻两层的节点数量的比例会影响跳表的查询性能:
相邻两层的节点数量最理想的比例是2:1,查找复杂度可以降低到 O(logN)。为了防止插入删除时间消耗,跳表在创建节点的时候,随机生成每个节点的层数。
具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于0.25(相当于概率25%),那么层数就增加1层,然后继续生成下一个随机数,直到随机数的结果大于0.25结束,最终确定该节点的层数。
整数集合
整数集合本质上是一块连续内存空间。
整数集合有一个升级规则,就是当将一个新元素加入到整数集合里面,如果新元素的类型(int32_t)比整数集合现有所有元素的类型(int16_t)都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t)扩展contents 数组的空间大小,然后才能将新元素加入到整数集合里,升级的过程中也要维持整数集合的有序性。
Redis对象的引用计数
redisObject的结构定义,其中有个字段叫refcount,这个refcount就是Redis中的引用计数。
当refcount减少到0,就会触发对象的释放。Redis的引用计数,目前是为整数数 据服务的。
Redis会在初始化服务器时,会创建10000个数值从0到9999的字符串对象。当服务器、新创建的键需要用到值为0到9999的字符串对象时,服务器就会使用这些共享对象,而不是新创建对象。
为什么只做0-9999的字符串对象池呢,关键因素有两点:
1.0-9999的整数,被使用的几率是很大的,复用是有场景的。
2.整数存储空间比较小,而每个redisObject内部结构至少占16字节,这比整数本身占据的空间还大,频繁分配整数是比较大的开销
3.要复用对象,就需要进行数值比较,而整数对象进行比较,成本最低,如果是其它字符串,需要遍历字符串所有字符,而其它如List、ZSet的对比成本就更高了。
综合分析下来,其实就是保留一万个整数,是投入产出比很合适的选择。
String
STRLEN key 返回key存储字符串值的长度
INCR +1
DECR -1
EXISTS key
EXPIRE KEY seconds 设置过期时间
浮点型在String是用什么表示?
要将一个浮点数放入字符串对象里面,需要先将这个浮点数转换成字符串值,然后再保存转换所得的字符串值。
浮点数在字符串对象里面是用字符串值表示的,用Raw还是Embstr编码,取决于转换后字符串的长度
String可以有多大?
一个Redis字符串最大为512MB,官网有明确注明,我看源码里也是直接写死的
最大存储长度可以通过配置项proto-max-bulk-len控制
Redis字符串是怎么实现的? String int (整数) embstr(字符串<=44) raw(字符串长度>44)
Redis字符串底层是String对象,String对象有三种编码方式:INT型、EMBSTR型、RAW型。
如果是存一个整数类型,使用INT编码存储;
如果存字符串,当字符串长度小于等于44byte,使用EMBSTR编码;
如果字符串长度大于44byte,则用RAW编码
SDS有什么用?
主要有三点:
1.SDS包含已使用容量字段,O(1)时间快速返回字符串长度,相比之下,C原生字符串需要O(n)。
2.有预留空间,在扩容时如果预留空间足够,就不用再重新分配内存,节约性能,缩容时也可以将减少的空间先保留下来,后续可以再使用(惰性空间释放)。
3.不再以‘\0’作为判断标准,二进制安全,可以很方便地存储一些二进制数据。
4.相比于普通C语言字符串,SDS增加了占用大小、分配大小的元数据,提升了基于字符串的追加、比较、复制操作的效率
SDS扩容过程
- SDS首先检查要添加的新数据长度是否超过了当前SDS的可用空间。如果可用空间足够,就直接将新数据追加到原有数据的末尾,并更新len字段表示新的长度。
- 如果可用空间不够,SDS就需要重新分配内存。它首先计算出合适的新空间大小,通常是根据当前数据长度和将要添加的新数据长度计算得出。
- SDS 分配一块新的内存空间,并将原有数据复制到这个新空间中。
- SDS释放原有的内存空间。
- 更新SDS的相关字段,包括buf字段指向新的内存空间、len字段表示新的长度、alloc字段表示分配可用空间。
Set一个已有的数据会发生什么?
如果是同种类型,会覆盖原有的值,同时会覆盖或者擦除键的过期时间。
set 命令还可以直接把 key抹除,例如原本有个列表键(key) mylist,可以直接 set mylist abc来覆盖这个列表键。类似隐式的del (或者 unlink) mylist + set, 底层其实就是删除了旧的 key,生成了一个新的 string类型的key
为什么EMBSTR的阈值是44?
Redis是使用jemalloc内存分配器,Redis以64字节为阈值区分大小字符串。所以EMBSTR的边界数值,其实是受64这个阈值影响。
redis对象占用的内存大小由 redisObject 和 sdshdr 这两部分组成,redisObject 16字节,sdshdr中已分配、已申请、标记三个字段固定占了3个字节,"\0'占了一个字节,能存放的数据就是64-(16+4) = 44。
你知道为什么EMBSTR曾经的阈值是39吗?
3.2之后的版本,SDS结构进行了拆分,,EMBSTR用的sdshdr8,总容量和已使用容量字段减少了6个字节,但由于增加了一个flags字段,所以最终节约了5个字节。
你知道EMBSTR和 RAW 的区别吗?
对于EMBSTR类型的字符串的RedisObject和sds在内存中是连续的,而RAW是通过RedisObject中的指针成员指向sds
- EMBSTR 只需要一次malloc, 而RAW 需要两次(分配 RedisObject 和sds),同样前者只需要进行一次free,而后者需要两次free
- EMBSTR读取性能更好(只需寻址一次),内存碎片率更低
- 如果修改EMBSTR (append 操作),那么就会将 EMBSTR 转换成RAWSTRING (重新分配空间,从设计上来说 EMBSTR 用于只读)
List
List对象底层编码方式是什么?
在3.2版本之前,List对象的编码是ZIPLIST和LINKEDLIST。ZIPLIST适用于元素数量较少、且元素都较短(512 、64)的情况,否则用LINKEDLIST。
3.2版本之后,List对象的编码全部由QUICKLIST实现。QUICKLIST是一个压缩列表组成的双向链表,结合了ZIPLIST和LINKEDLIST两者的优点
在后面比较新的版本中ZIPLIST优化为了LISTPACK。
List是完全先入先出吗?
List是双端操作对象,所以不是完全的先入先出,List也可以后入先出。
怎么获得List指定范围内的数据?
LRANGE命令参数为start和stop,比如一个列表有s1, s2, s3三个数据,用LRANGE 0 1就可以得到s1,s2。
List如何移除特定值的数据?时间复杂度是多少
LREM命令可以移除特定值的数据,比如LREM key 1 aaa,就会去移除第一个这个LIST key里面值为aaa的数据,这个操作是遍历去做的,所以时间复杂度是O(n)。
ZIPLIST有什么优点?
压缩列表顾名思义,是用来压缩结构,节约内存,相比于LINKEDLIST链表式设计,压缩列表的内存都是紧凑排列在一起的,这就带来了几个优点,
1.节约内存,2.方便一次性分配,3.遍历时局部性更强
如果单独拉开视角看ziplist的优点
那就是内存利用率很高,也能减少内存碎片
ZIPLIST是怎么压缩数据的?
1.结构头,即header,包括<zlbytes> <zltail> <zllen>字段。
- zlbytes,记录整个压缩列表占用对内存字节数;
- zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
- zllen,记录压缩列表包含的节点数量;(只有两字节)
- zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。
2.数据部分,即entry列表,entry中有<prevlen> <encoding> <entry-data>字段。
- prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历; 这个字段254->1字节, 大于255->5字节
- encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
- data,记录了当前节点的实际数据,类型和长度都由
encoding
决定;
3.结尾标识,即zlend。
压缩列表是一块连续的内存空间,元素之间是紧挨着存储的,一个压缩列表中可以包含多个节点(entry),是在连续的内存空间上实现的双端链表
对于一个普通的双向链表,链表中每一项都占用独立的一块内存,各项之间用地址指针(或引用)连接起来。这种方式会带来大量的内存碎片,而且地址指针也会占用额外的内存。
而ziplist却是将表中每一项存放在前后连续的地址空间内,一个ziplist整体占用一大块内存。
ziplist entry对于不同类型会有不同长度的数据存储,保证尽可能的压缩内存
字符串长度
小于等于 63 字节:以 1 字节存储字符串长度,并紧跟字符串数据。
大于 63 字节:以 5 字节存储字符串长度,并紧跟字符串数据。
列表list、哈希hash、集合set、zset
- 元素数量小于等于 2^16-2:以 2 字节存储列表元素数量。
- 元素数量大于等于 2^16-2:以 5 字节存储列表元素数量。
ZIPLIST下List可以从后往前遍历吗?
可以,List是双端数据结构,无论哪种底层编码,都需要能支持从后往前遍历。ZIPLIST每个节点中都保存了上一个节点的长度,所以可以用当前节点地址减去上一个节点长度来找到上个节点起始位置,进而实现从后往前的遍历。
在ZIPLIST数据结构下,查询节点个数的时间复杂度是多少? (这个是查询节点个数)
在ZIPLIST编码下,查询节点个数的时间复杂度是O(1),因为ZIPLIST中的header定义了记录节点数量的字段zllen。
但是这里也有个限制,记录节点数量的字段只有2字节,也就说如果节点数量超过65535,就失效了,此时只能通过O(n)复杂度的遍历来查节点总数。
LINKEDLIST编码下,查询节点个数的时间复杂度是多少?
LINKEDLIST编码下,查询节点个数的时间复杂度是O(1)。因为LINKEDLIST的表头结构中定义了链表所包含节点数量的字段len。
压缩列表插入的时间复杂度是多少?
从头部插入,因为后面的内存都要移动,整体可以看作O(n),尾部则是O(1),平均而言是O(n)
连锁更新是什么问题?
ZIPLIST每个节点会有个字段记录上一个节点的长度,如果上个节点小于254字节,这个记录字段则是1字节,否则是5。由于更新某个节点,会导致长度变化,如果从小于254变得大于等于254了,则会影响下个节点的长度,依次递推,就像多米诺骨牌一样,这种情况就是连锁更新。
如何解决连锁更新问题?
LISTPACK的思路:
在5.0引入的LISTPACK可以替代ZIPLIST来解决连锁更新问题,核心思路就是不去记录上一个节点的长度,而是记录自身长度,使用LISTPACK的第三部分element-tot-len(backlen)来做特殊化处理(记录encoding和data的长度),以便遍历到前一个节点
ZIPLIST的不足
- 当ziplist的元素个数变得很多之后,查找效率就会下降;
- 因为 ziplist内存是连续的,当其中一个节点需要更新,或者新增节点时,就需要重新分配内存;
- 因为ziplist每一个节点记录了前一个节点的长度,当其中一个节点的长度发生变化时,会导致后面的节点都需要进行更新,引发连锁更新问题
QUICKLIST 的设计是怎样的
为了应对 ziplist 的问题,在3.2版本实现的 quicklist 在 ziplist的基础上,通过链表将 ziplist 串联起来,每一个链表元素都是一个ziplist,这样就可以减少新增 ziplist时的内存分配,而且quicklist 限制了 ziplist 的大小,当一个ziplist 大小过大,就会新增一个quicklist 节点
Set
Set编码方式?
Set使用整数集合和字典(INTSET、HASHTABLE)作为底层编码,当元素都是整数同时元素个数不超过512个,会使用整数集合编码,否则使用字典编码。
SCARD: 查询集合元素个数
SSCAN:指定游标开始范围查询 默认是10个
Set为什么要用两种编码方式?
Set的底层编码是整数集合和字典,当元素数量小于512并且全部是整数的时候,会使用整数集合编码,更加的节约内存。元素数量变大会使用字典编码,查找元素的速度会更快。
Set是有序的吗?
Set的底层实现是整数集合或字典(HASHTABLE),前者是有序的,后者是无序的。整体来看,但是不应该依赖SET的顺序,业务使用适合始终应该按无序来用
Hash
Hash的编码方式是什么?
Hash底层有两种编码结构,一个是ZIPLIST->listpack, 一个是HashTable。ZIPLIST适用于元素较少且单个元素长度较小的情况,这里的阈值分别是元素个数少于512个,值和键长度都小于64字节。
Hash常见操作
创建即产生一个Hash对象,可以使用HSET、HSETNX创建。
查询支持HGET查询单个元素;HGETALL查询所有数据;HLEN查询数据总数; HSCAN进行游标迭代查询。
更新的话,HSET可以用于增加新元素,HDEL删除元素。
至于删除,和其它对象一样,DEL可以删除一个Hash对象。
Hash查找某个key的平均时间复杂度是多少?(这个是查询某个KEY 不是节点个数)
Hash有两种底层结构,ZIPLIST时是O(n), HashTable则是O(1)
Redis中HashTable查找元素总数的平均时间复杂度是多少?
HashTable查找元素总数的平均时间复杂度是O(1),因为HashTable的表头结构中有储存键值对数量的字段,这个字段我记得叫used。
Hash为什么要用两种编码方式?(因地制宜)
采用两种编码方式的原因是ZIPLIST更节约内存,所以在小数据量时使用。而数据多起来了,需要HASHTABLE提高更高的查找性能、更新性能。
一个数据在HashTable中的存储位置,是怎么计算的?
首先会通过哈希函数计算出key的哈希值,然后与哈希掩码做与运算得到索引值,索引值就是这个数据在HashTable中的存储位置。
MurmurHash2哈希算法
哈希掩码其实就是哈希表数组的大小减去1。
HashTable怎么扩容?(渐进式rehash操作)
首先程序会为HashTable的1号表分配空间,空间大小是第一个大于等于0号表大小*2的2^n。
在rehash进行期间,标记位rehashidx从0开始,每次对字典的键值对执行增删改查操作后,都会将rehashidx位置的数据迁移到1号表,然后将rehashidx加1,随着字典操作的不断执行,最终0号表的所有键值对都会被rehash到1号表上。之后,1号表会被设置成0号表,接着在1号表的位置创建一个新的空白表。
rehashidx值为-1时,未进行哈希操作
增加操作在表1进行,查,删,改在两张表上进行
新表大小为第一个大于等于原表2倍used的2次方幂。举个例子,原表如果used=500,2倍就是1000,那第一个大于1000的2次方幂则为1024。此时字典同时持有ht[0]和ht[1]两个哈希表。
HashTable怎么缩容?(渐进式rehash操作)
首先程序会为HashTable的1号表分配空间,新表大小为第一个大于等于原表used的2次方幂。
在rehash进行期间,标记位rehashidx从0开始,每次对字典的键值对执行增删改查操作后,都会将rehashidx位置的数据迁移到1号表,然后将rehashidx加1,随着字典操作的不断执行,最终0号表的所有键值对都会被rehash到1号表上。之后,1号表会被设置成0号表,接着在1号表的位置创建一个新的空白表。
used表示已经使用的节点数量。通过这个字段可以很方便地查询到目前HASHTABLE元素总量.
将原哈希表中的键值对逐个迁移至新的哈希表中。这个过程是逐步进行的,每次只迁移一小部分键值对,而不是一次性将全部键值对迁移完毕。这样可以分散迁移操作的负载,减少对系统性能的影响。
在进行键值对迁移时,会使用新的哈希函数计算键的哈希值,然后将键值对放入相应的新桶中。
什么时候扩容,什么时候缩容 渐进式哈希的触发条件?
HashTable的扩容与缩容由哈希表的负载因子决定,负载因子=哈希表已保存节点数/哈希表大小
- 当负载因子大于等于1,没有执行RDB快照或没有进行 AOF重写的时候,就会进行rehash 操作。
- 当负载因子大于等于5时,此时说明哈希冲突非常严重了,不管有没有执行RDB照或AOF重写,都会强制进行 rehash操作。
- 缩容的话也是负载因子影响,当哈希表的负载因子小于0.1时,程序会自动开始对哈希表进行收缩操作。
服务器进程在执行BGSAVE(RDB)或者BGREWRITEAOF(AOF)命令时,创建了新的子进程; 此时如果我们扩展哈希表,那么相当于向父进程写入数据,同时会导致子进程进行复制操作
rehash 是主进程推进的,主进程推进的话会修改数据,避免写时复制吃系统资源,所以RDB和AOF重写的时候,都不会推进rehash
操作系统为了避免有过多的写入操作,对于负载因子较小的情况下,优先考虑写入操作;但是负载因子较大的时候,哈希表的可用性受到影响,优先考虑哈希表的扩容
ZSet
ZSet底层有几种编码方式?
ZSet就是有序集合对象,ZSet对象的底层有两种编码方式:ziplist 或者 skiplist+HashTable(提高效率)。
如果一个ZSet对象中的所有元素同时满足:
元素数量小于128个以及所有元素成员的长度都小于64字节,那么会使用ziplist编码,否则使用skiplist+字典编码。
----------------------------------------------------------------------------------------------------------------------
字典实现元素到分值的映射关系,元素作为key,分值作为value
跳表实现有序集合,每个元素按照分值大小再跳表中排序
ZSet常见操作
ZADD命令可以用于创建ZSet,也可以用于添加元素,ZREM命令可以移除元素
ZRANGE命令可以按分数从小到大的顺序查找范围,ZREVRANGE则是逆序
ZINCRBY 命令(为有序集合key中元素member的分值加上increment)
Del 阻塞操作,Ulink 异步操作
ZRANGE key start stop [WITHSCORES]
功能:查询从start到stop范围的ZSet数据,WITHSCORES选填,不写输出里就只有key,没有score值
ZREVRANGE key start stop [WITHSCORES]
功能:即reverse range,从大到小遍历,WITHScoRES选填,不写输出里就只有key,没有score值
如何使用ZSet实现24小时的热搜词排行榜,并每小时更新?
使用Redis的ZSet来实现热搜词排行榜是非常合适的。我们可以将热搜词作为键,搜索次数作为分数存入ZSet。每当有新的搜索请求时,我们可以使用ZINCRBY
命令来增加该热搜词的分数。
为了实现每小时更新,我们可以设置一个定时任务,每小时对ZSet进行处理,比如移除分数最低的元素或者重置分数。此外,我们还可以使用滑动窗口算法来保留最近一小时内的热搜数据。
跳表
跳表是什么,和普通的链表有什么区别?
链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。
跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据,实现O(logN)的元素查找效率。
跳表的多层级
Zset 对象要同时保存「元素」和「元素的权重」,对应到跳表节点结构里就是 sds 类型的 ele 变量和 double 类型的 score 变量。每个跳表节点都有一个后向指针(struct zskiplistNode *backward),指向前一个节点,目的是为了方便从跳表的尾节点开始访问节点,这样倒序查找时很方便。
跳表是一个带有层级关系的链表,而且每一层级可以包含多个节点,每一个节点通过指针连接起来,实现这一特性就是靠跳表节点结构体中的zskiplistLevel 结构体类型的 level 数组。
level 数组中的每一个元素代表跳表的一层,也就是由 zskiplistLevel 结构体表示,比如 leve[0] 就表示第一层,leve[1] 就表示第二层。zskiplistLevel 结构体里定义了「指向下一个跳表节点的指针」和「跨度」,跨度时用来记录两个节点之间的距离。
比如,下面这张图,展示了各个节点的跨度。
第一眼看到跨度的时候,以为是遍历操作有关,实际上并没有任何关系,遍历操作只需要用前向指针(struct zskiplistNode *forward)就可以完成了。
跨度实际上是为了计算这个节点在跳表中的排位。具体怎么做的呢?因为跳表中的节点都是按序排列的,那么计算某个节点排位的时候,从头节点到该结点的查询路径上,将沿途访问过的所有层的跨度累加起来,得到的结果就是目标节点在跳表中的排位。
跳表的查找过程
从高级索引往后查找,如果下个节点的数值比目标节点小,继续找,否则不跳过去,而是用下级索引往下找。
跳表编码模式下,查询节点总数的平均时间复杂度是多少?
跳表编码模式下,查询节点总数的平均时间复杂度是O(1),因为跳表的表头结构中定义了一个保存节点数量的字段length,源码中调用查询节点总数的API时会直接返回这个字段。
跳表插入一条数据的平均时间复杂度是多少
跳跃表是一种支持多级索引的结构,查询效率可以媲美二分查找,它插入一条数据也是需要先查找,找到之后会进行索引的重建,整体平均时间复杂度是O(logN)。
跳表的空间复杂度 O(n)
跳表的空间复杂度相对较高,因为它需要额外的索引层来支持快速查找。空间复杂度的计算取决于索引层的数量和元素的数量。在平均情况下,跳表的索引层数量约为log n,其中n是元素的数量。每个索引层需要的额外空间是原始链表的一部分,因此跳表的空间复杂度为O(n)。
跳表插入数据会影响其它节点层高吗?
不会的,节点层高是创建时就确认了,不会被新插入节点影响。
新插入节点只会影响每一层前一跳、后一跳的关联指针。
为什么跳表和HashTable要配合使用
为了结合这两种数据结构各自的优势:
当ZSet要根据成员来查找分值的时候,将使用HashTable来实现,时间复杂度为O(1)。
而当ZSet要执行范围操作时,比如 ZRANK、ZRANGE 等命令时,将使用原本就有序的跳跃表来实现。
ZRANK命令用于获取有序集合中指定成员的排名(即索引位置)。 zrank
ZRANGE命令用于按照排名范围获取有序集合中的成员。zrange
redis (Zset)为什么不使用b+树,而是用跳表 不考虑磁盘IO,跳表实现简单,不需要平衡操作
主要是两者的应用场景不同吧。
B+ tree 更多用于磁盘型数据库,它索引层级低(对应树矮),磁盘IO次数就更少,这个是它的优势。
对于Redis来说,因为它是内存的,层级的高矮就不是啥优劣势,不涉及磁盘IO。跳表实现简单,插入的时候不用像平衡树那样需要做一些保持平衡的操作,插入性能严格来说跳表应该会好一些
为什么用跳表而不用平衡树? 内存占用少 范围查询简单 实现简单(不用保持子树平衡)
从内存占用上来比较,跳表比平衡树更灵活一些:
平衡树每个节点包含2个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为1/(1-p),如果像 Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
在做范围查找的时候,跳表比平衡树操作要简单:在平衡树上,找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。
zset为什么用跳表而不用红黑树? (基本同上一道题)
在 Redis 中,ZSet(有序集合)的实现采用了跳表(Skip List),而没有选择红黑树的原因主要有几点:
1.简单性和可读性:跳表相对于红黑树来说更加简单和直观。实现一个高效且正确的红黑树需要处理多种情况,而跳表的实现相对来说更加清晰和容易理解。这使便得代码更加易于维护和调试。
2.实现复杂度:红黑树的实现相对来说更为复杂,需要处理平衡性和旋转等问题。跳表在实现上相对简单,不需要复杂的平衡操作。这可以减轻开发者的负担,并减少潜在的错误。
3.性能:在实际使用中,跳表在某些操作上的性能可能优于红黑树。对于某些场景,跳表可能更容易优化,并且在一些操作的时间复杂度上可以达到O(log n)。此外,跳表的局部性较好,对于缓存友好,这对于一些读密集的场景是有优势的。
4.随机化:跳表的设计中引入了一定的随机性,这便得跳表的平衡相对容易维护。红黑树在维护平衡时需要更复杂的操作,而跳表通过随机化层的添加,实现了类似平衡的效果。
总体而言,Redis 采用跳表而不是红黑树是为了简化实现,提高可读性,并在某些操作上获得性能上的优势。
跳表中一个节点的层高是怎么决定的?
跳表在插入新节点之前会计算一个随机的层高,具体来说,跳表的每一个节点一开始默认都是1层,然后每增加一层的概率都是25%,最高为32层
自己整理 借鉴很多博主 感谢他们