为了便于操作,Redis采用redisObjec结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObjec中定义了type和encoding字段对不同的数据类型加以区别。简单地说,redisObjec就是string、hash、list、set、zset的父类,可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObjec结构来到达同样的目的。



String类型
为什么采用简单动态字符串作为redis的默认字符串?
C语言没有Java里面的String类型,只能是靠自己的char[]来实现,字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 ‘\0’ 为止。所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。


3大物理编码方式: int 、embstr 、raw
int :保存long型的64位有符号整数
注:只有整数才会使用int,如果是浮点数,redis内部讲浮点数转化为字符串值再保存。
embstr:嵌入式的String,代表embstr格式的SDS(简单动态字符串),保存长度小于44字节的字符串。
raw:保存长度大于44字节的字符串

Hash数据结构
Java中,JDK8之前采用的是数组+链表的方式,JDK8之后采用的是数组+链表+红黑树的方式。HashMap默认初始化大小为16,之后每次扩容,容量变为原来的2倍。JDK1.8之后HashMap当链表长度大于阈值(默认8),将链表转化为红黑树(此时要求当前数组的长度大于64,若小于则优先选择数组扩容)
ConcurrentHashMap和HashTable:
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
JDK1.7 ConcurrentHashMap对整个数组进行了分段分割,每把锁只锁容器中其中一部分数据,JDK1.8ConcurrentHashMap直接使用Node数组+链表+红黑树实现,并发控制采用synchronized和CAS操作。
Segment继承了ReenTrantLock,所以是一种可重入锁。Java 8 中,采用node+cas+synchronized保证线程安全,锁粒度更细,synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。
JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。
java的hash封装了一个Node类型的节点,因为对于存在hash冲突的节点是使用链表或者红黑树保存的。

Redis中
Redis6:压缩列表(ziplist+hashtable)
Hash类型键的字段个数 小于 hash-max-ziplist-entries 并且每个字名和字段值的长度 小于 hash-max-ziplist-value 时,
Redis才会使用 OB] ENCODING ZIPLIST来存储该键,前述条件任意一个不满足则会转换为 OBJ ENCODING HT的编码方式
hash-max-ziplist-entries 使用压缩列表保存时哈希集合中最大元素的个数
hash-max-ziplist-value 使用压缩列表保存哈希集合中单个元素的最大长度
1.哈希对象保存的键值对数里小于 512 个;
2.所有的键值对的健和值的字符串长度都小于等于 64byte(一个英文字母一个字节)时用ziplist,反之用hashtable
ziplist可以升级到hashtable,反过来不可以。
hash
hashtable被称为字典,它是一个数组+链表的结构。每个键值对都有一个dictEntry

ziplist
为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组
ziplist是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面。


Ziplist压缩节点zlentry的构成

为什么要设计一个压缩链接?
1 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist 是一个特殊的双向链表没有维护双向指针:previous next;而是存储上一个 entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。
2 链表在内存中一般是不连续的,遍历相对比较慢而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
备注:sizeof实际上是获取了数据在内存中所占用的存储空间,以字节为单位来计数。
3 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)
总结:
- ziplist为了节省内存,采用了紧凑的连续存储。
- ziplist是一个双向链表,可以在时间复杂度为 O(1) 下从头部、尾部进行 pop 或 push。
- 新增或更新元素可能会出现连锁更新现象(致命缺点导致被listpack替换)。
- 不能保存过多的元素,否则查询效率就会降低,数量小和内容小的情况下可以使用
listpack
hash-max-listpack-entries: 使用压缩列表保存时哈希集合中的最大元素个数
hash-max-listpack-value::使用压缩列表保存时哈希集合中单个元素的最大长度。
连锁更新:
搜索
140%
便笺
压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
案例说明:压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患
第一步:现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:

因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值,一切OK,O(∩_∩)O哈哈~
第二步:这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:

因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。
第三步:连续更新问题出现

entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点…一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」
listpack



ziplist和listpack的对比

和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过,为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项不再像ziplist列表项那样保存其前一个列表项的长度。

List数据类型
java中List===> ArrayList<> =>Object[] LinkedList 双端链表
redis list
redis6 ==》quicklist+ziplist

采用的底层数据结构是ziplist压缩列表+linkedList双向链表
然后在高版本的Redis中底层数据结构是quicklist(替换了ziplist+linkedList),而quicklist也用到了ziplist
结论:quicklist就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表

quicklist 实际上是zipList 和 linkedList 的混合体,它将 linkedList按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间
使用双向指针串接起来。

redis7 ==》quicklist+listpack
listpack紧凑列表
是用来替代 ziplist 的新数据结构,在 7.0 版本已经没有 ziplist 的配置了(6.0版本仅部分数据类型作为过渡阶段在使用)
Set
set的两种编码方式 intset 、hashtable
Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。
如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。
ZSet
当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表作为有序集合的底层实现。
否则会使用ziplist作为有序集合的底层实现.
总结:
Redis6:
- 字符串
int:8个字节的长整型。
embstr:小于等于44个字节的字符串。
raw:大于44个字节的字符串。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。 - 哈希
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64 字节)时,
Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的 结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀。
hashtable(哈希表):当哈希类型无法满足ziplist的条件时,Redis会使 用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O(1)。 - 列表
ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置 (默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时 (默认64字节),
Redis会选用ziplist来作为列表的内部实现来减少内存的使 用。
linkedlist(链表):当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist作为列表的内部实现。quicklist ziplist和linkedlist的结合以ziplist为节点的链表(linkedlist) - 集合
intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时,Redis会用intset来作为集合的内部实现,从而减少内存的使用。
hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。 - 有序集合
ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist- entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配 置(默认64字节)时,
Redis会用ziplist来作为有序集合的内部实现,ziplist 可以有效减少内存的使用。
skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作 为内部实现,因为此时ziplist的读写效率会下降。


数据类型和时间复杂度

跳表skiplist
skiplist是一种以空间换取时间的结构。
由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表。总的来说,跳表=链表+多级索引
跳表的时间复杂度
跳表查询的时间复杂度分析,如果链表里有N个结点,会有多少级索引呢?
按照我们前面讲的,两两取首。每两个结点会抽出一个结点作为上一级索引的结点,以此估算:
第一级索引的结点个数大约就是n/2,
第二级索引的结点个数大约就是n/4,
第三级索引的结点个数大约就是n/8,依次类推…也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)

跳表的空间复杂度
第一步:首先原始链表长度为n,
第二步:两两取首,每层索引的结点数:n/2, n/4, n/8 … , 8, 4, 2 每上升一级就减少一半,直到剩下2个结点,以此类推;如果我们把每层索引的结点数写出来,就是一个等比数列。

这几级索引的结点总和就是n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是O(n) 。也就是说,如果将包含n个结点的单链表构造成跳表,我们需要额外再用接近n个结点的存储空间。
第三步:思考三三取首,每层索引的结点数:n/3, n/9, n/27 … , 9, 3, 1 以此类推;
第一级索引需要大约n/3个结点,第二级索引需要大约n/9个结点。每往上一级,索引结点个数都除以3。为了方便计算,我们假设最高一级的索
引结点个数是1。我们把每级索引的结点个数都写下来,也是一个等比数列

通过等比数列求和公式,总的索引结点大约就是n/3+n/9+n/27+…+9+3+1=n/2。尽管空间复杂度还是O(n) ,但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半的索引结点存储空间。
所以空间复杂度是O(n);
总结
优点:
跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的
缺点:
维护成本相对要高,在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1)
新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(log n)
Redis中的五种基本数据类型包括String、Hash、List、Set和ZSet。String类型默认使用简单动态字符串SDS,有int、embstr、raw三种编码方式。Hash数据结构在特定条件下使用ziplist或hashtable编码。Redis6后引入了listpack作为ziplist的替代,以减少连锁更新的问题。List数据类型在Redis6中采用quicklist(ziplist+linkedList)结构。Set和ZSet分别使用intset和hashtable或skiplist编码。Redis通过这些编码方式在内存使用和性能之间取得平衡。
6万+

被折叠的 条评论
为什么被折叠?



