一个人内耗,说明他活在过去;一个人焦虑,说明他活在未来。只有当一个人平静时,他才活在现在。
日常
1、起床6:00
2、健身1.5h
3、LeetCode刷了1题
- Dijkstra堆优化(稀疏图用邻接表 )
- 对于稠密图(边多顶点少),使用邻接矩阵存储并进行朴素版的Dijkstra可以在O(V2)内完成;但对于稀疏图(边少顶点多),此时朴素版就会很慢,故此时可以从边开始选择到源点最短的边加入 ,此时可以在O(ElogE)内完成,使用堆进行优化
- 思路是使用邻接表来存放图 ,要注意对于每个顶点,不仅要存放其所连的边的顶点 ,还要存放边的权值 ,故要使用
List<Map<Integer, Integer>>[] graph
来实现,即每个顶点所连的边的相连顶点和权值均要保存 ,使用邻接表存放有向图,不仅要存放每个顶点可以到达的顶点,还要保存权值,故使用List<Map<Integer, Integer>>[]
结构来定义邻接表,其中Map的key是可以到达的节点,value是这条边的权值,此时就可以定义邻接表,使用小顶堆来存放边,刚开始从源点开始,遍历所有相连的边,将边加入小顶堆,然后当小顶堆不为空时,取出权值最小的边,然后判断目标顶点是否遍历过,如果未遍历过则遍历,然后遍历所有相连的边,更新到源点的最短距离,在当前节点的距离的基础上进行更新,然后再加入小顶堆 - 此时首先遍历源点的所有边 ,将所有边根据权值大小加入到小顶堆中 ,然后当堆不为空时,每次选择顶堆的边 ,如果对应顶点未访问时,将对应顶点标记 ,然后根据源点到该顶点的权值(距离),遍历该顶点相连的所有边,将这些边的权值更新为到源点的距离 ,然后加入到小顶堆中 ,然后继续从小顶堆取出权值最小的边,如果取出的边的顶点遍历过,则直接将该边从小顶堆中删除 ,也可以先从堆顶取出权值最小的边(pop),然后判断是否遍历过,如果遍历过则直接continue ,否则根据当前边的权值来将其所连的所有边的权值更新为到源点的距离然后加入小顶堆,当小顶堆不为空时,取出堆顶的边pop,然后判断顶点是否遍历过,如果遍历过则继续取,如果未遍历过,则先遍历,然后遍历所有相连的边,更新到源点的最短距离然后加入小顶堆
- Dijkstra堆优化适用于边少顶点多的稀疏图 ,其时间复杂度为O(ElogE),只与边的数量有关,与顶点个数无关,根据边的权值来选择顶点,且使用小顶堆实现边的权值的排序
- Dijkstra算法要求所有边的权值均为正数 ,要求所有边的权值均为正数,只适用于所有边的权值均为正数的单源最短路径 ,对于带负权值的单源最短路径要使用Bellman_ford算法
- Dijkstra算法只适用于权值全为正的图的单源最短路径,对于负权值,则不可以使用dijkstra,要使用bellman_ford算法,对所有的边进行n-1次松弛,松弛是指对于边 from to value ,如果 minDist(from) + value < minDist(to) 则对minDist(to)进行更新
- Dijkstra算法只可求非负权值的单源最短路径,对于带负权值的单源最短路径不可使用Dijkstra,要使用Bellman_ford算法
- Bellman_ford算法:求带负权值 的单源最短路径
- 对所有的边进行 n-1 次松弛操作 即可求出从源点开始经过最多 n-1 条边到达其他顶点的最短距离 ,可以求带负权值的单元最短路径
- 松弛操作 是指对于有权值边 (from, to, value) ,如果
minDist[to] < minDist[from] + value
则对minDist[to]
进行更新,一次松弛操作是指对所有的边均进行一次上述操作 ,对所有边松弛一次,相当于计算 起点 到达 与起点一条边相连的节点 的最短距离,一次松弛操作指多所有的边均进行一次上述操作,使用minDist数组存放源点到顶点的最短距离,对所有的边经过 n-1 次松弛操作后就可以求出源点到所有顶点最多n-1条边所需要的最短路径 - 仍要使用 minDist 数组记录到所有顶点的最短距离 ,对所有的边进行 k 次松弛操作后的 minDist 数组 ,相当于计算 起点到达与起点最多k条边相连的点的最短距离 ,而两个顶点最多 n-1 条边相连 ,故对所有的边进行 n-1 次松弛 即可,此时 minDist 数组就表示从源点出发经过最多 n-1 条边到达所有顶点的最短距离
4、复盘
不复盘等于白学!!!
学习和感想
Redis学习
1. Redis五大经典数据类型源码及底层数据结构
- 面试题
- Redis数据类型的底层数据结构
- Redis五大经典类型:String、List、Hash、SET、ZSET
- GEO底层使用的是ZSET,以经纬度对key进行打分,Bitmap底层使用的是string来存放bit数组,HyperLogLog底层也是使用string实现
- Redis源码
- Redis是基于C语言进行开发
- 在/src下的.c文件就是redis的源码实现,.h文件是一些定义(接口)
- 或者去github看redis源码
- Redis源码核心
- Redis基本数据结构
- 只看一些基本数据结构,五大经典数据类型都是基于基本数据结构来实现
- Redis数据库实现
- 数据库底层实现 db.c
- 数据库持久化 rdb.c aof.c
- Redis服务端和客户端实现
- 其他功能源码
- 考什么就看什么,对文件进行分类
- Redis基本数据结构
- Redis是字典数据库存放KV键值对
- 说明
- Redis既是KV键值对(dict.c)又是内存数据库(db.c)
- db.c启动数据库 ,然后dict.c形成字典 ,然后在字典中存放KV键值对,每个key都抽象为object.c对象
- dict.c形成字典,字典就是KV键值对 ,先生成db数据库 ,然后生成字典存放KV键值对
- 每个key都是string类型,每个value都被抽象为 redis-object 类型
- 如何使用KV键值对在字典dict.c存储数据
- 先使用db.c生成数据库,然后使用dict.c生成字典 ,在字典中保存KV键值对,所有的key都是String类型,value都被抽象为object类型
- redis所有的key都是string类型 ,所有的value都是redis-object类型 ,其中object被具体实现为string、list、hash、set、zset等
- 十大类型:大多都是基于五大经典数据类型实现
- 五大经典类型:String、List、Hash、Set、Zset![[Pasted image 20241102203034.png]]
- 五大新类型:GEO(ZSET)、HyperLogLog(STRING)、Bitmap(STRING)、Bitfield(STRING)、Stream,底层大多也是使用五大经典数据类型实现
- 上帝视角启动redis服务器
- redis-server启动后先根据db.c创建数据库 ,再根据dict.c在数据库中创建字典 ,在字典中以dictHash的形式存放KV键值对 ,内部有一个keys表,每个key均指向一个object抽象类型的value ,并具体实现不同的数据类型
- redisObject抽象结构体
- KV键值对从dictEntry -> redisObject
- Redis使用字典来存放KV键值对 ,每一个KV键值对都对应一个dictEntry ,每个dictEntry中都有一个key和value指针 ,分别指向redisObject对象 ,暴露给用户的是Hash\string\list等数据类型 ,但底层是SDS、压缩列表和哈希表等具体实现
- KV键值对底层是dictEtry ,但暴露给用户是redisObject
- Redis每个KV键值对(对象)都有一个dictEntry (类似HashMap的Entry),存放一个key和一个value ,每一个key和value都指向一个redisObject
- redis中每个key的value都是一个object结构体
- 将KV键值对的value抽象为Object结构体对象,不同数据类型分别具体实现
- redis是字典数据库,字典中存放KV键值对,每个key都是字符串,其对应的value都是redsiObject结构体对象
- C语言struct结构体
- struct type_name {属性定义} object_name(可选定义变量);
- typedef关键字定义结构体 ,此时定义变量时不需要加struct关键字
- KV键值读取流程
- Redsi底层使用字典dict存放KV键值对,每个KV键值对都有一个dictEntry来存放key和value,每个key和value都指向一个redisObject对象并暴露给用户表示具体的数据类型 ,而每个redisObject对象内部都有一个指针指向具体类型的实现,即实现数据类型的数据结构
- 每个KV键值对都对应一个dictEntry存放key和value,其中每个key和value都指向一个redisObject对象,在redisObject对象中通过type表明数据类型,并通过指针指向具体实现的数据结构
- KV键值对从dictEntry -> redisObject
- Redis存放KV键值对具体实现
- 当redis-server启动时会先根据db.c创建一个数据库,然后在数据库中根据dict.c创建一个字典,并创建hashtable,然后每添加一个KV键值对,就在dicthashtable中创建一个dictEntry来存放当前KV键值对的key和value,且每个key和value均指向一个redisObject对象,在redisObject对象中标明当前数据类型type,并指向具体的实现结构
- 说明
- 五大经典数据类型 底层源码以及底层数据结构
- redis数据类型 与底层数据结构 总纲
- redis底层使用dictEntry 保存KV键值对,内部有一个key和一个value,每个key和value都指向一个redsiObject,并在redisObject中通过type指明数据类型 ,然后通过ptr指向具体实现的数据结构
- 总体数据结构 大纲:SDS动态字符串、双向链表、压缩列表ziplist(7之后就不再使用,为了兼容性继续保留) 、哈希表hashlist、跳表skiplist、整数集合intset、快速列表quicklist、紧凑列表listpack
- Redis7之后就不在使用zipList压缩列表 ,而是引入了listpack紧凑列表 ,此时String(SDS动态字符串) 、SET(哈希表+整数数组) 、ZSET(跳表+紧凑列表) 、Hash(哈希表+紧凑列表) 、List(快表)
- 从 set hello world 开始
- 当redis-server启动时,会加载server.h,在期内定义了很多常亮和方法,然后调用 db.c 创建redis内部的数据库,然调用dict.c创建字典,并在字典中创建hashtable来存放KV键值对,每个键值对都有一个dictEntry存放到ht中,其内存放了KV键值对的key和value,且key是string类型的,指向一个SDS对象 (Redis没有使用C语言的字符数组,而是自定义SDS存放字符串 ),value指向一个redisObject对象 ,在redisObject对象中通过type指明该对象的数据类型,用encoding指明数据结构,然后通过 ptr指针指向该数据类型的具体实现数据结构
- 每一个KV键值对的dictEntry中均存放着一个key和一个value,其中key是string类型的,指向一个SDS对象(Redis没有使用C语言的字符数组存放字符串,而是自定义了SDS) ,而value均指向redisObject对象 ,用type表明value的数据类型 ,用encoding表明该类型底层数据结构 ,用ptr指向具体实现的数据结构
- key是string类型,指向一个SDS对象,每个KV键值对都有一个dictEntry,当根据key获得value时,先判断key是不是指定的key ,如果是则返回该dictEntry中的value所指向的RedisObject对象 ,当key匹配时,返回value所指向的RedisObject对象
- RedisObject对象 的结构
- encoding表示底层存储的编码类型,同一个数据类型的底层编码可能不同 ,如 int、raw、emstr,均是string类型value的编码方式,其中 int 编码方式指该string类型的value中存放的是数字
- 使用 type key 查看键的value的数据类型,使用 OBJECT ENCODING key 查看该对象数据类型的编码方式
- 每个KV键值对的value都是一个redisObject对象,用type表明具体的数据类型 ,用encoding表明具体的数据结构 ,然后用ptr指向具体数据结构实现 ,refcount表示对象的引用计数 ,LRU表示使用LRU缓存淘汰策略时的计数,表示最后一次访问的时间,24bit
- 五大数据类型底层数据结构解析
- 各个数据类型的数据结构编码映射encoding
- DEBUG ONJECT key
- redis的每个键值对的value都对应一个redisObejct对象 ,内部存放该键值对的所有有关信息,如type数据类型 、encoding编码方式 、refcount引用次数 、lru最近使用时间、ptr指向具体的实现![[Pasted image 20241104081749.png]]
- 默认该命令无法在客户端使用 ,可以通过在配置文件中对 enable-debug-comman 命令进行配置为 local 来开启,要重启服务器
- 对KEY进行调试的命令,存在时返回key的信息
- 当开启后就可以使用 DEGUG OBJECT key 来查看该 key 的相关信息:地址、引用次数 、物理编码类型(同一个数据类型的编码方式可能不同 )、序列化后的长度(不是真正长度) 、最近使用的时间戳(lru缓存淘汰策略)、空闲时间
- String数据结构:SDS +三大编码
- 通过 SET/GET 来设置的数据为 String 类型 ,string类型的数据有3大物理编码ENCODING ,根据string类型数据的具体值来设置
- **3大物理编码Encoding:根据value的值来设置
- embstr:SDS简单动态字符串 (redis没有使用C语言的字符数组来存放字符串,而是使用SDS),保存长度小于44字节(一个字符占用一个字节)的字符串,嵌入式的String编码![[Pasted image 20241104082439.png]]
- int:保存long型的64位有符号整数 (不超过long,长度小于20的整数,长度大于等于20使用embstr编码 ),即只有string类型数据的value是整数且不超过long且长度小于20时才用 int 编码 ,浮点数是字符串
- raw:保存长度大于44字节的字符串,即长度大于44个字符的字符串
- redis的SDS字符串结构
- 说明
- C语言中字符串使用字符char数组来保存,并设置结束符 \0
- redis没有使用C语言的字符串,而是新建了SDS简单动态字符串结构来保存字符串
- 在redis中所有的键key都是有SDS实现 ,所有string类型的value也是使用SDS实现(只不过有3大编码)
- SDS
- redis的SDS字符串相当于在C语言的字符串外套了一层 ,记录字符串的长度len 、类型flags 、占用空间大小alloc 以及字符buf数组存放真正的值
- SDS有多种结构sdshdr8/13/32/64,用来存放不同长度的字符串 ,通过flags来标识sdshdr8/13/32/64,sdshdr5没有被使用只是做个测试
- 为什么要设置SDS
- redis没有使用C语言的字符数组存放字符串 ,而是自定义了SDS结构来存放字符串,C语言字符串存在二进制不安全问题 ,因为其以\0作为结束符,而存放的字符可能是\0,而且C语言字符串获得长度为O(N)
- SDS相当于对字符数组的封装 ,添加了len、alloc等属性,可以O(1)直接获得字符串的长度 ,而且便于空间分配,且是二进制安全的
- 源码分析
- 当使用set key value 创建一个string类型的键值对时,会先根据value获得字符串的三大编码类型 ,然后根据编码类型来创建字符串
- int编码:享元模式
- 当设置的字符串类型数据可以转换为long型数据进行存储时 ,就会使用int编码方式,直接指向具体的数字,且会使用享元模式,对于0~10000直接指向内存中的数字
- redis在启动时会预先建立10000个分别存放0~10000的redisObject共享变量 ,此时当string类型的value在10000以内时就不需要建立新对象 ,直接指向已经建立好的对象,此时键值不占空间 (享元模式)
- 当设置的KV键值对是string类型,且value是long型数据时 ,则就是int编码方式,且当value的值小于10000时,就不会新建对象,而是使用已经建立好的对象
- embstr编码 :SDS与redisObject内存连续
- 当字符串value的值不是long型数据 ,即不是整数 或者超过long的范围时且长度小于44位时就会使用embstr编码 ,只要不是long类型(可能不是整数也可能长度超过范围)且长度小于44字节,则使用embstr编码 ,embstr编码就是让SDS字符串存放地址与redisObject相邻
- embstr编码其SDS字符串(C语言字符数组的封装)地址紧凑着redisObject对象 ,相当于嵌入redisObject对象中,embstr编码的SDS与redisObject地址相邻
- raw编码:SDS与redisObject内存不连续
- 所有的value都指向一个redisObject对象,在其中指明数据类型type、引用次数refcount、编码方式encoding、数据结构ptr、以及缓存淘汰策略lru
- 当value的长度超过44字节时 ,就会使用raw编码,此时创建的SDS字符串就与redisObject对象的内存不再连续了 ,而是为SDS新分配一块空间
- 对于embstr是只读的 ,在对embstr修改后都会转换为raw编码 ,无论是否达到了44字节,都会变为raw编码
- 说明
- 当设置一个string类型的数据时 ,先根据value的值判断编码类型,如果是整数且符合long,则用int编码类型进行存储 ,ptr直接指向整数数据 ,且使用享元模式节省内存,如果不是整数且长度小于44则用embstr编码类型 ,其使用SDS字符串格式 ,且对象的redisObject与数据结构SDS在内存上相邻 ,如果字符串长度大于44字节,则在redisObject中设置为raw编码方式 ,然后申请空间创建一个SDS字符串,并指向
- 当embstr编码被修改时,会直接变为raw编码 ,不管是否超过44字节,redis会自动选择合适的编码格式进行优化,不需要用户选择 ,当embstr编码被修改时就会变为raw编码
- Hash数据结构:listpack(少)+哈希表(多)
- 版本优化:HashTable + ziplist->listpack
- redis6的Hash使用哈希表+压缩列表ziplist
- redis7的Hash使用哈希表+紧凑列表listpack
- redis6:HashTable + ziplist
- 使用Hash类型时,默认ziplist的最大字段个数是512,单个字段元素长度最大是64 ,如果没有超过最大,则使用ziplist结构进行存放(默认也是ziplist) ,如果超过任一个,则使用hashtable结构进行存放 ,从ziplist升级到hashtable可以 ,但不可以降级
- 在节省空间方面hashtable没有ziplist高效 ,只有当键的字段个数和元素长度不超过设置的值时,使用ziplist进行存储,否则使用hashtable存储,初始时默认为ziplist存储
- Hash类型的数据整体使用ziplist+hashtable结构 ,但其内部的key和value仍然是string类型
- 在创建hash类型数据时,默认是ziplist存储 ,如果字段个数大于设置的hash-ziplist-max-entries则会转换为hashtable类型 ,如果没有则遍历每个字段的value,hash类型的每个field和value都是string类型 ,判断长度是否超过了设置的最大长度 ,如果超过了则用hashtable结构进行存储,否则仍用ziplist结构进行存储![[Pasted image 20241104130732.png]]
- hashtable才是真正的结构 ,一层层嵌套,内部创建dict,并创建dicthashtable,然后为每个字段创建一个dictEntry
- ziplist.c
- 说明
- ziplist是一种紧凑编码格式 ,以连续内存存放的双向链表,以时间换空间,有极高的内存空间利用率 ,当hash类型数据的键值对很少并且每个元素长度很少时 ,使用ziplist结构进行实现 ,以提高内存空间的利用率 ,会对数据压缩和解压 ,故空间利用率高但耗时多
- ziplist结构使用连续的内存空间 ,查找时遍历O(n)的方式 ,以时间换空间 ,有极高的内存利用率,但只适用于小规模使用,当规模很大时使用hashtable来存放hash类型数据的键值对
- 整体结构
- ziplist压缩列表是一种用连续数组实现的特殊的双向链表 ,每个节点不保存前后指针 ,而是保存上一个节点的长度,记录了总节点的个数以及首尾节点的偏移量(快速定位尾节点),并包含了节点列表
- zlEntry节点构成
- hash数据的每个field和每个value都单独作为一个entry保存到ziplist中
- ziplist存取时,根据当前entry中存放的数据,可以快速得到下一个entry的地址
- 为什么要有ziplist
- ziplist是特殊的双向链表 ,其使用连续的内存进行存放 ,不存放前后指针(内存利用率高) ,所有的entry存放在连续的内存空间,且通过节点长度可以快速找到下一个节点的地址 ,故其遍历速度极快 ,且通过参数记录entry的个数 ,O(1)就可以直接得到len
- 因为ziplist的每个entry的长度不同,故不可以直接使用数组存放
- 总结
- 对于普通链表,每个节点都要有指向前后节点的指针,占用大量的空间,空间利用率低
- 而ziplist是用连续内存实现的双向链表 ,每个entry只记录上一个节点的大小,故内存利用率高 ,但遍历时要通过计算得到下一个节点的地址 ,故是用时间换空间
- 且ziplist通过len属性存放总的entry个数 ,可以在O(1)快速得到节点个数
- 而且因为每个节点的长度不同,故不可以使用数组
- ziplist是使用连续内存实现的特殊的双向链表 ,相比于普通链表记录前后节点的指针 ,其只记录上一个节点的大小,故空间利用率高 ,但因为要计算地址,故遍历效率不高,不可以保存太多元素 ;且是双向链表,故可以快速在首尾进行增加和删除,但因为使用连续内存进行存储,故更新删除节点时可能会导致大量节点移动,所有在redis7之后被listpack所替代
- ziplist新增或者更新元素可能会出现连锁更新现象 ,故被listpack替代,因为每个节点的大小可能不同,故不能使用数组
- 说明
- redis7:HashTable + listpack
-
配置和ziplist类似
- redis7之后使用listpack结构来替换ziplist结构 ,以解决ziplist结构连锁更新问题 ,且为了兼容和过渡,依旧保留ziplist的配置项 ,且会同时修改listpack和ziplist的配置
- 当hash类型数据的字段个数很少(小于hash-max-listpack-entries) 且每个字段元素长度很少(小于 hash-max-listpack-value)时 ,使用listpack结构, 否则使用hashtable结构,不可以降级
- 默认使用listpack结构 进行存储,当任一个不满足时就会使用HT进行存储
-
源码分析
- redis7引入listpack来替代ziplist ,其余均为改变,只不过将ziplist的数据结构改为listpack的数据结构
-
为什么用listpack替代ziplist
- ziplist结构使用连续地址实现双向链表,每个entry均连续存放(内部有一个属性记录前一个节点的长度,大小为1字节或者5字节) ,若某个entry更新后大小变大,则后面节点中存放上一个节点长度的属性就会变大,导致当前节点变大,故可能会更新后面的所有节点,故存在连锁更新问题 ,因为要保存前一个节点的长度字段,故可能会导致连锁更新问题
- 故引入了listpack紧凑列表结构 来解决ziplist出现的连锁更新问题,listpack通过记录自己节点的长度且放在节点尾部来解决连锁更新
-
listpack
- 整体结构
- listpack通过记录自己节点的长度且放在节点尾部来解决连锁更新
-
ziplist布局与listpack布局
- ziplist的每个entry通过存储上一个节点的大小来进行遍历 (可以得到上一个节点的开始地址 ),但因为要存放上一个节点的大小,当小时用1字节大时用5字节,故可能存在连锁更新问题
- listpack通过在每个entry中只存放当前节点的大小且存放在节点尾部来进行遍历 ,此时可以快速得到上一个节点尾部的地址 ,通过根据上一个节点尾部得到上一个节点的大小来计算可以得到上一个节点的地址,然后进行遍历,此时就解决了连锁更新问题(只记录自己的长度,别人的变化不影响)
-
- 版本优化:HashTable + ziplist->listpack
- List数据结构:quicklist(双端链表+ziplist/listpack)
- redis6:quickList (双向链表+ziplist)
-
配置
- list-compress-depth 配置表示quicklist两端不被压缩的节点个数 ,默认为0表示都不压缩 ,当为k时表示两端各有k个不被压缩
- list-max-ziplist-size 表示每个quicklist节点中ziplist的最大长度 ,为正数时表示通过数据项限制ziplist长度 ,为负数时表示通过大小限制ziplist,默认为-2
-
结构
-
quicklist就是双向链表+压缩列表ziplist (紧凑列表listpack),quicklist就是双向链表加上ziplist ,双向链表分段,每一段都是一个ziplist
-
list数据类型通过quicklist双向链表来实现,而每个节点都是一个ziplist
-
ziplist内存利用率极高 ,但会出现连锁更新问题 ,双向链表linkedlist可以极快的删除新增更新,但内存利用率低
-
-
源码分析
- quicklist
- List数据类型底层使用quicklist进行实现 ,quicklist就是双向链表+ziplist ,每个quickNode节点都是一个ziplist
- quicklist
-
- redis7:quickList (双向链表+listpack )
- 用listpack替换ziplist
- redis7的List数据类型就是将quicklist底层的ziplist替换为listpack
- 均使用quicklist来实现List数据类型,但底层数据结构不一样,redis7引入listpack来替代ziplist
- redis6:quickList (双向链表+ziplist)
- Set数据结构:哈希表+整数集合
- Set两种编码格式
- intset(整数集合):当Set集合中元素都是整数long类型且个数不超过 set-max-intset-entries(默认512) 时,使用intset结构编码 ,否则使用hashtable结构编码
- hashtable(哈希表):用数组加链表来实现,key就是集合元素的值,value为null
- Set两种编码格式
- ZSet数据结构:listpack(少)+skiplist(多)
- redis6:skiplist + ziplist
- redis7:skiplist + listpack:用listpack替代ziplist
- 配置
- redis7就是用listpack替代ziplist ,且配置项会同步修改
- zset-max-ziplist(listpack) -entries(128) /value(64) :设置ziplist/listpack结构时最大的entry元素个数以及单个元素的最大长度
- 当上面配置全部满足时,则使用ziplist/listpack来作为ZSet的底层实现 ,否则使用skiplist作为底层实现
- skiplist 数据结构
- 对于ZSet数据类型,当元素个数很少(<zset-max-listpack-entries=128)并且每个元素的长度都小于zset-max-listpack-value=64时,就会使用listpack作为底层数据结构 ,否则就是要skiplist作为底层数据结构
- 普通的单链表,当遍历查找元素时必须从前向后依次进行遍历,此时为O(N),为了解决查找效率低 ,可以使用空间换时间 的方法,给链表加一个索引,两两相邻取首作为索引即可 ,可以加多层索引
- skiplist面试题
- skiplist是可以实现二分查找的有序链表 ,即为链表加上多层索引以加快链表的遍历 :跳表 = 链表 + 多级索引 ,为每个节点添加索引,两两取首作为索引 ,可以实现二分查找的有序链表为跳表 ,用空间(额外索引)换时间(O(logn))
- skiplist以空间换时间 ,为每个节点添加索引,两两取首作为索引 ,时间复杂度是O(logn) ,空间复杂度为O(n)
- skiplist是redis中实现ZSet数据类型的数据结构 ,当元素很多时使用skiplist进行存储
- 优缺点:skiplist以空间换时间(T(N)=O(logn),S(N)=O(N)) ,为每个节点添加索引 ,可以实现二分查找的有序链表 ,但要保证链表的有序性 ,故插入时必须先遍历再插入
- 小总结
- 同一个数据类型可以有多种不同的编码结构
- string类型的数据有三大编码:int、embstr、raw
- Set类型数据底层编码有 intset(少512且为整数)、hashtable
- Hash类型底层编码有 ziplist/listpack(少512 64)、hashtable
- ZSet类型底层编码有 ziplist/listpack(少)、skiplist
- List类型底层编码有 双端链表 + (ziplist/listpack)
- ziplist是压缩列表以时间换空间 ,相对于传统的链表要存储前后节点指针,其只存放上一个节点的大小 ,空间利用率高 ,但是会存在连锁更新;skiplist是以空间换时间,为每个顶点加一个索引,两两取首作为索引,时间复杂度为O(logn)
- 为了解决ziplist的连锁更新问题 ,redis7引入了listpack ,其不存放上一个节点的大小,而是存放当前节点的大小并放在末尾 ,此时不会出现连锁更新问题 ,故redis7之后用listpack替代ziplist,所有底层使用ziplist均会被listpack替代
- redis数据类型 与底层数据结构 总纲