Redis系列(四)

1、redis经典五种数据类型及底层实现

1.1、redis分布式锁的实现,其他方式了解吗,对比redis、zk实现分布式锁,这三个从实现方式上和锁竞争上有什么不同?

  1. redis按照一个key是否过期+lua脚本实现以及官网推荐redlock算法的落地产品redisson
  2. zookeeper按照一个zk里面只可以有且仅有一个znode节点,加锁成功就是建立一个节点,时间到期使用完了,自动删除该节点
  3. 两个为了避免单点故障,一般3台机器,zk集群是全体同步才返回消息,redis集群异同通知,容易出现mater宕机后,slave上位但锁丢失的情况。

2、redis的底层实现

REmote DIctionary Server(Redis)是一个由Salvatore Sanfilippo写的 key-value存储系统,是跨平台的非关系型数据库。Redis是一个开源的使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存、分布式、可选持久性的键值对(Key-Value)存储数据库,并提供多种语言的API。

在这里插入图片描述

2.1、底层代码

2.1.1、Redis基本的数据结构(骨架)
  • 简单动态字符串:sds.c
  • 整数集合:intset.c
  • 压缩列表:ziplist.c
  • 快速链表:quicklist.c
  • 字典:dict.c
  • Streams的底层实现结构:listpack.c,irax.c
2.1.2、Redis数据类型的底层实现
  • Redis对象:object.c
  • 字符串:t_string.c
  • 列表:t_list.c
  • 字典:t_hash.c
  • 集合及有序集合:t_set.c和t_zset.c
  • 数据流:t_stream.c
2.1.3、Redis数据库的实现
  • 数据库的底层实现:db.c
  • 持久化:rdb.c和aof.c
2.1.4、Redis服务端和客户端实现
  • 事件驱动:ae.c和ae_epoll.c
  • 网络连接:anet.c和networking.e
  • 服务端程序:server.c
  • 客户端程序:redis-cli.c
2.1.4、其他
  • 主从复制:replication.c
  • 哨兵:sentinel.c
  • 集群:cluster.c
  • 其他数据结构:如hyperloglog.c. geo.c等
  • 其他功能:如pub/sub、Lua脚本

2.2、redis的KV键值对到底是什么?

redis是key-value存储系统,其中key类型一般为字符串,value 类型则为redis对象(redisObject),每个键值对都会有一个dictEntry。
在这里插入图片描述
dictEntry的构成
在这里插入图片描述

  • bitmap:实质string
  • hyperLogLog:实质string
  • geo:实质zset
2.2.1、C语言的struct结构体语法简介

在这里插入图片描述
在这里插入图片描述

redisObject +Redis数据类型+Redis 所有编码方式(底层实现)三者之间的关系
在这里插入图片描述

在这里插入图片描述

1.3、当set k1 v1 时,发生了什么?

以set hello word为例,因为Redis是KV键值对的数据库,每个键值对都会有一个dictEntry(源码位置:dict.h),里面指向了key和value的指针,next 指向下一个dictEntry。key是字符串,但是 Redis没有直接使用C的字符数组,而是存储在redis自定义的SDS中。value既不是直接作为字符串存储,也不是直接存储在SDS 中,而是存储在redisObject中。实际上五种常用的数据类型的任何一种,都是通过redisObject来存储的。

在这里插入图片描述

  • 看看类型
  • 看看编码
  • debug结构在这里插入图片描述

在这里插入图片描述

1.4、redisobject结构的作用

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

  • 4位的type表示具体的数据类型
  • 4位的encoding表示该类型的物理编码方式见下表,同一种数据类型可能有不同的编码方式。(比如String就提供了3种:int、embstr、raw)
  • Iru字段表示当内存超限时采用LRU算法清除内存中的对象。
  • refcount表示对象的引用计数。
  • ptr指针指向真正的底层数据结构的指针。

在这里插入图片描述

比方说 set age 17

type:类型
encoding:编码,此处是数字类型
lru:最近被访问的时间
refcount:等于1,表示当前对象被引用的次数
ptr:指针,指向的value值是多少,当前就是17

1.5、数据类型以及数据结构的关系

在这里插入图片描述

1.5.1、String数据类型
1.5.1.1、3大编码格式
  • int :保存long型(长整型)的64位(8个字节)有符号整数9223372036854775807,数字最多19位,并且只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值(也就是说直接是embstr),然后再保存

在这里插入图片描述

  • embstr:代表embstr格式的SDS(Simple Dynamic String简单动态字符串),保存长度小于44字节的字符串,EMBSTR顾名思义即: embedded string,表示嵌入式的String。
  • raw:保存长度大于44字节的字符串,但是有一个字符串的最大长度限制,为512M

在这里插入图片描述
在这里插入图片描述

1.5.1.2、SDS简单动态字符串

在这里插入图片描述

Redis没有直接复用C语言的字符串,而是新建了属于自己的结构----SDS,在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。

在这里插入图片描述

在这里插入图片描述
Redis中字符串的实现,SDS有多种结构( sds.h) :
sdshdr5、(2^5=32byte)
sdshdr8、(2^8=256byte)
sdshdr16、(2^ 16=65536byte=64KB)
sdshdr32、(2 ^ 32byte=4GB)
sdshdr64,2的64次方byte=17179869184G用于存储不同的长度的字符串。

  • len表示SDS的长度,使我们在获取字符串长度的时候可以在O(1)情况下拿到,而不是像C那样需要遍历一遍字符串。
  • alloc可以用来计算 free就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。
  • buf[]表示字符串数组,存真实数据的。

1.5.1.3、Redis为什么重新设计一个SDS数据结构?

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

C语言char[]与SDS的对比
在这里插入图片描述

1.5.1.4、三大编码格式的空间分配?
1.5.1.4.1、编码格式为 int

命令示例: set k1 123
当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应OBJ_ENCODING_INT编马类型。内部的内存结构表示如下:
set k1 123
set k2 123
在这里插入图片描述
Redis启动时会预先建立10000个分别存储0~9999的redisObject变量作为共享对象,这就意味着如果set字符串的键值在0到10000
之间的话,则可以直接指向共享对象而不需要再建立新对象,此时键值不占空间!
set k1 100
set k2 100
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

1.5.1.4.2、编码格式为 embstr

redis快的一个原因及时极致的压缩内存空间,尽量少占用内存空间,减少碎片化,并连续。

在这里插入图片描述

1.5.1.4.2、编码格式为 raw

当字符串的键值为长度大于44的超长字符串时,Redis则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与oBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了

在这里插入图片描述
字符串append之后,明明没有超过阈值,为什么变成raw了?

在这里插入图片描述
对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改。因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。

在这里插入图片描述
在这里插入图片描述

总结:
只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值,然后再保存,embstr与 raw类型底层的数据结构其实都是SDS(简单动态字符串,Redis 内部定义sdshdr一种结构)。

  1. int:Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。
  2. embstr:当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含redisObject与 sdshdr两个数据结构,让元数据、指针和SDS是块连续的内存区域,这样就可以避免内存碎片。
  3. raw:当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含redisObject结构,而另一块用于包含sdshdr 结构、

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择较优化的内部编码格式,而这一切对用户完全透明!

1.5.2、Hash数据类型
1.5.2.1、hash的两种编码格式
  • ziplist

  • hashtable

  • hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。

  • hash-max-ziplist-value:使用压缩列表保存哈希集合中单个元素的最大长度。

Hash类型某个键的元素个数小于hash-max-ziplist-entries(默认大小512)并且该键下的键值对中每个字段名(键值对的键)和字段值(键值对的值)的长度都小于 hash-max-ziplist-value(默认大小64)时,Redis才会使用OBJ_ENCODING_ZIPLIST来存储该键,前述条件任意一个不满足则会转换为OBJ_ENCODING_HT(hashtable)的编码方式。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

ziplist升级到hashtaible可以,反过来降级不可以,一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存而不会再转回压缩列表了。在节省内存空间方面哈希表就没有压缩列表高效了。

在这里插入图片描述

1.5.2.1.1、ziplist

Ziplist压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率,因此只会用于字段个数少,且字段值也较小的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。当一个hash对象只包含少量键值对,且每个键值对的键和值要么就是小整数,要么就是长度比较短的字符串,那么它用ziplist作为底层实现。

ziplist的结构:

ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

1.5.2.1.2、明明有链表了,为什么出来一个压缩链表?
  1. 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失
    ziplist是一个特殊的双向链表没有维护双向指针,prev和next,而是存储上一个entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。
  2. 链表在内存中一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。
    普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。
  3. 头节点里同时还有一个参数len,时间复杂度是O(1)。
    和string类型提到的SDS类似,这里是用来记录链表长度的,因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是O(1)。
    在这里插入图片描述

压缩列表是Redis为节约空间而实现的一系列特殊编码的连续内存块组成的顺序型数据结构,本质上是字节数组在模型上将这些连续的数组分为3大部分,分别是header+entry集合+end,其中header由zlbytes+zltail+zllen组成。

entry是节点,zlend是一个单字节255(11111111),用做zipList的结尾标识符。见下:压缩列表结构:由zlbytes、zltail、zllen、entry、zlend这五部分组成

  • zlbytes 4字节,记录整个压缩列表占用的内存字节数。
  • zltail 4字节,记录压缩列表表尾节点的位置。
  • zllen 2字节,记录压缩列表节点个数。
  • zlentry列表节点,长度不定,由内容决定。
  • zlend 1字节,OxFF标记压缩的结束。

压缩列表zlentry节点结构:每个zlentry由前一个节点的长度、encoding和entry-data三部分组成
在这里插入图片描述

前节点:(前节点占用的内存字节数)表示前1个zlentry的长度,prev_len有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节。
enncoding:记录节点的content保存数据的类型和长度。
content:保存实际数据内容
在这里插入图片描述

在这里插入图片描述
压缩列表的遍历:

通过指向表尾节点的位置指针p1,减去节点的previous_enty_length,得到前一个节点起始地址的指针。如此循环,从表尾遍历到表头节点,遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的
previous_entry_length属性程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。

ziplist存取情况
在这里插入图片描述

1.5.2.1.3、Redis 中,hashtable被称为字典( dictionary),它是一个数组+链表的结构

OBJ_ENCODING_HT这种编码方式内部才是真正的哈希表结构,或称为字典结构,其可以实现O(1)复杂度的读写操作,因此效率很高。在Redis内部,从OBJ_ENCODING_HT类型到底层真正的散列表数据结构是一层层嵌套下去的,组织关系见面图:
在这里插入图片描述

在这里插入图片描述
dictEntry:哈希条目
dictht:哈希表
dict:字典
在这里插入图片描述

1.5.3、List数据类型

在这里插入图片描述

  • ziplist压缩配置:list-compress-depth 0
    表示一个quicklist两端不被压缩的节点个数。这里的节点是指quicklist双向链表的节点,而不是指ziplist里面的数据项个数参数
    list-compress-depth的取值含义如下:

    • 0:是个特殊值,表示都不压缩。这是Redis的默认值。
    • 1:表示quicklist两端各有1个节点不压缩,中间的节点压缩。
    • 2:表示quicklist两端各有2个节点不压缩,中间的节点压缩。
    • 3:表示quicklist两端各有3个节点不压缩,中间的节点压缩。
    • 依此类推…
  • ziplist中entry配置: list-max-ziplist-size -2
    当取正值的时候,表示按照数据项个数来限定每个quicklist节点上的ziplist长度。比如当这个参数配置成5的时候,表示每个quicklist节点的ziplist最多包含5个数据项。当取负值的时候,表示按照占用字节数来限定每个quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,
    每个值含义加下:

    • -5:每个quicklist节点上的ziplist大小不能超过64 Kb。(注:1kb => 1024 bytes)
    • -4:每个quicklist节点上的ziplist大小不能超过32 Kb。
    • -3:每个quicklist节点上的ziplist大小不能超过16 Kb。
    • -2:每个quicklist节点上的ziplist大小不能超过8 Kb。(-2是Redis给出的默认值>)
    • -1:每个quicklist节点上的ziplist大小不能超过4Kb。
1.5.2.1、list的一种编码格式

list用quicklist来存储,quicklist是由多个quicklistnode节点首尾彼此指向,形成一个双向链表结构,每个节点都是一个ziplist
在这里插入图片描述

  • 在低版本的Redis中,list采用的底层数据结构是ziplist+linkedList;
  • 高版本的Redis中底层数据结构是quicklist(它替换了ziplist+linkedList),而quicklist也用到了ziplist

quicklist:是ziplist和linkedlist的结合体,quicklist实际上是zipList和 linkedList的混合体,它将 linkedList按段切分,每一段使用zipList来紧凑存储,多个zipList之间使用双向指针串接起来。
在这里插入图片描述
quicklist.h,head和ltail指向双向列表的表头和表尾,quicklistNode中的*zl指向一个ziplist,一个ziplist可以存放多个元素。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.5.4、set数据类型

set的两种编码格式是intset或hashtable,Redis用intset或hashtable存储set。如果元素都是整数类型,存储的个数不超过512(默认值),就用intset存储,如果不是整数类型或者存储的个数超过512,就用hashtable(数组+链表的存来储结构),key就是元素的值,value为null。
在这里插入图片描述

1.5.5、zset数据类型

ZSet的两种编码格式:ziplist 和 skiplist。
当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries的值(默认值为128),或者有序集合中新添加元素的 member的长度大于服务器属性server.zset_max_ziplist_value的值(默认值为64)时,redis会使用跳跃表作为有序集合的底层实现,否则会使用ziplist作为有序集合的底层实现。
在这里插入图片描述
在这里插入图片描述

1.5.6、总结:
类型编码对象
REDIS_STRINGREDIS_ENCODING_INT使用整数值实现的字符串对象
REDIS_STRINGREDIS_ENCODING_EMBSTR使用embstr编码的简单动态字符串实现的字符串对象
REDIS_STRINGREDIS_ENCODING_RAW使用简单动态字符串实现的字符串对象
REDIS_LISTREDIS_ENCODING_ZIPLIST使用压缩列表实现的列表对象
REDIS_LISTREDIS_ENCODING_LINKEDLIST使用双端链表实现的列表对象(低版本是LINKEDLIST,高版本是QUICKIST)
REDIS_HASHREDIS_ENCODING_ZIPLIST使用压缩列表实现的哈希对象
REDIS_HASHREOIS_ENCODING_HT使用字典实现的哈希对象
REDIS_SETREDIS_ENCODING_INTSET使用整数集合实现的集合对象
REDIS_SETREDIS_ENCODING_HT使用字典实现的集合对象
REDIS_ZSETREDIS_ENCODING_ZIPLIST使用压缩列表实现的有序集合对象
REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳跃表和字典实现的有序集合对象
  1. 字符串
    int:8个字节的长整型,存储小于等于19位的整数,embstr:小于等于44个字节的字符串。raw:大于44个字节的字符串,Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
  2. 哈希
    ziplist(压缩列表)当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比hashtable更加优秀,hastable(哈希表)当哈希类型无法满足ziplist的条件时,Redis会使用hashtable作为哈希的内部实现,因为此时ziplist的读写效率会下降,而hashtable的读写时间复杂度为O⑴.
  3. 列表
    ziplist(压缩列表)当列表的元素个数小于list-max-ziplist-entrie配置(默认512个),同时列表中每个元素的值都小于list-max-ziplist-value配置时(默认64字节),Redis会选用ziplist来作为列表的内部实现来减少内存的使用。linkedlist (链表)当列表类型无法满足ziplist的条件时,Redis会使用 linkedlist作为列表的内部实现。quicklist是ziplist和linkedlist的结合,以ziplist为节点的链表(linkedlist)
  4. 集合
    intset(整数集合)当集合中的元素都是整数且元素个数小于set-max-intset-entrie配置(默认512个)时,Redis会选用intset来作为集合的内部实现,从而减少内存的使用。hashtable(哈希表):当集合类型无法满足intset的条件时,Redis会使用hashtable作为集合的内部实现。
  5. 有序集合
    ziplist(压缩列表)当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个)同时每个元素的值都小于zset-max-ziplist-value配置默认(64字节时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。skiplist(跳跃表).当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。

在这里插入图片描述

3、跳表

跳表是什么:跳表是可以实现二分查找的有序链表,skiplist是一种以空间换取时间的结构。由于链表,无法进行二分查找(无法保证有序),因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找。提取多层关键节点,就形成了跳跃表。

总结来讲跳表=链表+多级索引。
在这里插入图片描述
在这里插入图片描述
跳表的时间复杂度

3.1、跳表查询的时间复杂度分析

首先每一级索引我们提升了2倍的跨度,那就是减少了2倍的步数,所以是n/2、n/4、n/8以此类推;第k级索引结点的个数就是n/(2^k);假设索引有h级,最高的索引有2个结点;n/(2h)=2,从这个公式我们可以求得h = log2(N)-1;所以最后得出跳表的时间复杂度是O(logN)

3.2、跳表查询的空间复杂度分析

首先原始链表长度为n,如果索引是每2个结点有一个索引结点,每层索引的结点数: n/2, n/4, n/8 … ,8,4,2以此类推;或者所以是每3个结点有一个索引结点,每层索引的结点数: n/3, n/9, n/27… ,9,3,1以此类推;所以空间复杂度是O(n); |

3.3、优缺点:

跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较肴限的,但是维护成本相对要高-新增或者删除时需要把所有索引都更新一遍;最后在新增和删除的过程中的更新,时间复杂度也是O(log n)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值