Redis数据结构
Redis数据存储原理:Redis使用全局哈希表来保存所有键值对,实现从键到值的快速访问。哈希桶中的值是指向具体值的指针。

全局哈希表提供了快速访问的能力,通过key快速找到哈希桶位置,从而访问相应的实际键值。实际复杂度是O(1)。
RedisObject:Redis数据类型很多,不同数据类型都有些相同的元数据记录(被引用的次数等),采用了一个RedisObject来统计记录元数据。该结构体包含8字节的元数据和8字节指针,指针指向具体数据结构所在的内存地址。

String存储底层:
- 保存64位有符号整数时,String类型会将数据保存为8字节的Long类型整数
- 保存数据包含字符时,就会用SDS结构保存数据。SDS结构主要是int len(保持字符串长度)、int free(buf数组中未使用字节的数量)、char buf[](字节数组,保存字符串)三个变量
Long整数和SDS内存布局优化设计:
- int编码方式:保存Long类型,RedisObject中指针直接赋值为整数数据,这样不需要额外指针指向整数,节省指针空间开销。
- embstr编码方式:保存字符串数据时,当小于等于44字节,RedisObject的元数据、指针、SDS是一块连续内存,这样避免内存碎片。
- raw编码方式:当字符串大于44字节,SDS数据量开始变多,RedisObeject和SDS就会拆开,单独给SDS分配独立空间,并用指针指向SDS结构

String实际内存:
Redis有全局哈希表保存所有键值对,每一项都是dictEntry结构,这个结构中有三个指针分别指向key、value及下一个dictEntry,共24字节。但是在内存分配时,Redis使用的分配库jemalloc会根据申请的字节数N,找一个大于N,但是最接近N的2次幂的数作为空间大小,减少频繁分配次数。所以实际的dictEntry结构就占用了32字节。所以当存一个16字节的整数时,实际内存占用要8+8+16+32=64字节,这就是为什么有时候String类型内存占用比实际存储要大很多的原因
为何不用C的字符串实现而是用SDS?
- 长度获取的复杂度:len属性方便了获取SDS字符串的长度,时间复杂度为O(1),而C中获取字符串长度是通过遍历计数的,时间复杂度为O(N)
- 缓冲区溢出:在C中使用函数进行字符串拼接,一旦没有分配足够的内存空间会造成缓冲区溢出。而SDS会先根据len检查内存空间是否满足,不满足会先扩展后操作,避免缓冲区溢出。
- C不记录字符串的长度,修改字符串需要先释放再申请内存。而SDS利用len和free属性实现了空间预分配和惰性空间释放两种策略。空间预分配:对字符串进行空间扩展时,扩展内存比实际需要的多,可以减少连续执行字符串增长操作所需要重新分配内存的次数。惰性空间释放:对字符串进行缩短操作,程序利用free将字节数量记录下来,等待后续使用,而不是立即内存重分配。
- C字符串以空字符作为结束标识,对于二进制文件内容包括空字符串,C无法正确存取,SDS以len属性长度来判断字符串是否结束,保证了二进制安全
hash底层存储:
可以使用ziplist和哈希表(redis的dict)。当hash对象同时满足下面两个条件时,哈希对象使用ziplist编码。
- hash-max-ziplist-entries:压缩列表保存时最大元素个数,默认512个。
- hash-max-ziplist-value:用压缩列表保存时单个元素最大长度,默认64字节
哈希表存在冲突和rehash的,这两个问题会带来操作阻塞,偶尔操作会变慢。Redis的全局哈希表处理冲突是采用链式哈希,冲突链上的元素只能通过指针逐一查找再操作,当冲突链过长,会导致查找元素耗时长,效率降低。因此Redis会对哈希表进行rehash操作。
dict:包含哈希表数据组(dictEntry数组)、大小、大小掩码(计算索引值)、已有节点的数量。
Rehash:rehash是为了增加现有的哈希桶数量,让更多的entry元素在更多桶之间分散保存,降低单个桶的元素量。默认有两个全局哈希表表1和表2,开始使用时默认使用表1,表2并未分配空间。让数据逐步增多开始执行rehash:给表2分配空间,是表1的两倍、将表1的数据重新映射并拷贝到表2、释放表1空间。过程中涉及大量数据拷贝,如果一次性迁移完表1数据,会造成Redis线程阻塞,所以Redis采用了渐进式rehash。
- 扩容:如果哈希表ht[0]中保存的key个数与哈希表大小的比例已经达到1:1,即保存的节点数已经大于哈希表大小且redis服务当前允许执行rehash,或者保存的节点数与哈希表大小的比例超过了安全阈值(默认值为5)。则将哈希表大小扩容为原来的两倍,设置字典的rehashindex为0,表示之后需要进行rehash操作。
- 缩容:字典使用容量不足总空间10%触发,也会设置rehashindex为0。
渐进式rehash:
- Redis的扩容和缩容不是一次性,集中式完成的。而是分多次、渐进式完成。当键值对数量非常大,一次性rehash会造成redis服务器一段时间内无法进行别的操操作。
- 执行定时任务或客户端的hset、hdel指令时,判断rehashindex来决定是否要rehash。触发后调用dictRehash函数,将旧hash元素添加到新hash中。操作完后,清空旧hash表,设置rehashindex为1.
- 渐进式rehash时,字典的删除查找更新等操作会在两个哈希表进行,第一个哈希表没有则去第二个找,新增操作一定是在新哈希表操作。
ziplist(压缩列表):
- ziplist由ziplist和zipEntry组成。ziplist包含header(开始)、entry、end(结束)三个模块。zipEntry由prevlen(前面entry长度)、encoding$length(编码字段长度和实际value长度)、value(真正数据)组成。
- 每个key/value存储结果中key用一个zipEntry存储,value用一个ZipEntry存储
- 压缩列表定位第一个和最后一个是O(1),查找其他元素就是O(n)。压缩列表分配的是一块连续内存,将所有元素紧挨着存储,数据结构本身没有实际复杂度的优势,但是节省空间且能避免一些内存碎片

ziplist优劣:
- ziplist可以节省内存,但是查询指定元素需要遍历,所以获取到内存性能时,查询性能会下降很多。
- 尽量存储int数据,内存占用小,每个元素都存储了上一个元素的长度,所以修改其中一个元素超过一定大小时,会引发多个元素的重新排列位置,分配内存,引发性能问题。
- 在用Hash或Sorted Set存储时,虽然节省了内存,但是设置过期困难,无法控制每个元素的过期,只能控制整个key。
- 选用hash或Sorted Set存储时,类似于将Redis当成数据库,需要保证Redis可靠性(数据备份、主从),防止实例宕机导致数据丢失。采用String存储时,设置maxmemory和淘汰策略,控制整个实例内存上限。同时要保证数据库和缓存的一致性
list底层存储:list底层采用ziplist或linkedlist两种结构存储,首先以ziplist存储,不满足条件时转换为linkedlist。当列表对象保存字符串元素长度小于64字节且对象保存元素数量小于512时,使用ziplist。
链表:链表包含:头结点、尾节点、链表所包含节点数量、节点值复制函数(复制链表节点所保存的值)、释放函数(释放链表节点所保存的值)、对比函数(对比链表节点所保存的值和另一个输入值是否相等)。每个链表节点由listNode结构组成,包含前置节点、后置节点、节点值。多个listNode通过两个指针组成双端链表。
Set(集合):内部键值对是无序且唯一的,由intset(整数集合)或hash(普通哈希表)组成。在对象保存元素都是整数值且对象保存元素数量不超过512个时,采用intset存储,否则使用hashtable
- intset包含:编码方式、数组包含元素数量、实际存储用的数组。存储数据有序, 查找数据采用二分查找实现。
- hash存储时,元素value作为了key,而实际value存的是NULL。
sorted set(zset):底层存储包括ziplist或skiplist。当保存元素数量小于128个且元素长度小于64字节使用ziplist,其他时候使用skiplist。使用ziplist时,每个集合元素使用两个相邻的zipEntry保存,第一个节点保存元素,第二个节点元素的分值。且必须按score排序,所以插入元素会先根据score找到对应位置,然后插入member和score,插入性能比hash低。使用skiplist时,使用skiplist按序保存元素及分值,使用dict保存元素和分值的映射关系
skiplist(跳表)原理:
有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点
跳表由zskiplistNode和zskiplist两个结构定义。zskiplistNode表示跳跃表节点,zskiplist保存跳表节点信息
zskiplist节点信息包含:指向跳表的表头/表尾节点、目前跳表内层数最大的节点层数(表头节点层数不在内)、跳表长度(除表头节点外,目前包含节点数量)
zskiplistNode包含:
- 层(标记节点的各个层,每个层都有两个属性。前进指针:用于访问位于表尾方向的其他节点。跨度:记录前进指针指向节点和当前节点的距离。程序从表头向表尾遍历,访问会沿着层的前进指针进行)
- 后退指针:节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。
- 分值:各个节点中的分值,节点按各自:节点中用BW字样标记节点的后腿指针,指向位于当前所保存的分值从小到大排列
- 成员对象:各个节点所保存的成员对象。

特点:
- 跳表的最底层拥有所有的元素
- 跳表每一层都是一个链表,除了最底层是原始链表,层次逐渐往上可分别划分为一级索引层、二级索引层
- 跳表插入元素时,会先随机生成出一个层次数字。然后元素会插入到这个层次的所有底层,直到原始链表层
- 如果一个元素存在与某个索引层,那么这个元素也会存在于低于它的所有索引下层,如元素在第99索引层,那么由上到下从99索引层直到原始链表层都会存在该元素
- 空间换时间,跳表查找变快了,但是要存储许多索引层,故空间开销变大了
- 每个新节点插入,都会调用随机算法分配一个合理的层数:每层晋升的概率是50%,即1层是50%,2层是25%,3层是12.5%,层层降低50%。默认允许的最大层数为32,可容纳2^64个元素。
- 查找节点时,从高索引层往低索引层查找。要查找的目标元素在该层的某两个相邻元素之间,就会往下跳到下层的同一个位置,继续从同一位置向链表尾方向遍历查询、
本文来自博客园,作者:难得,转载请注明原文链接:https://www.cnblogs.com/zhangbLearn/p/18829274

浙公网安备 33010602011771号