Redis键值类型及底层原理全解析

1,332 阅读18分钟

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」。

String

常见命令

  1. set :数字写命令

image.png

  1. get 获取数据

image.png

  1. del 数据删除命令

image.png

  1. getset 获取原有value值写入新的value值

image.png

  1. incr,incrby,数据加法计算,incr是+1运算,incrby是+n计算

image.png

  1. decr,decrby,数据减法运算,decr是-1运算,decrby是减n运算

image.png

  1. 获取String长度

image.png

  1. 设置value值并设置过期时间命令setex

image.png

  1. 从指定位置替换赋值,从指定位置开始替换,命令setrange

image.png

  1. redis截取字符串

image.png

  1. 批量获取和批量赋值操作mget,mset

image.png

应用场景

  • 分布式锁

  • 计数功能

  • 全局的序列号:例如用在全局id自增

hash

  1. hset 写入数据

image.png

  1. hget 获取数据

image.png

  1. hdel 删除数据

image.png

  1. hexists key field 查字段是否存在

image.png

  1. HLEN 返回哈希表键 key 中域的数量

image.png

  1. hincrby key field increment 将key中的域field中存储的数字增加 increment

image.png

  1. hmget key field 返回哈希表键key中,一个或多个给定域的值

image.png

  1. hmset key field value,同时为哈希表键key设置一个或多个key-value 键值对

image.png

  1. hkeys key 返回哈希表键key中所有域

image.png

  1. hvals key 返回key所有域的值

image.png

应用场景

存储对象

购物车实现

list

  1. rpush key value 从队列右边入队

image.png

  1. lpush key value 从队列左边入队

image.png

  1. LPOP key 从队列左边出队

image.png

  1. RPOP key 从队列右边出队一个元素

  2. llen key 获取队列长度

image.png

  1. lrange key start stop 从列表中获取指定返回的元素

image.png

  1. lset key index value 设置队列里面一个元素的值

image.png

应用场景

Redis数据结构底层原理

String 底层原理 简单动态字符串(SDS)

底层原理

Redis 没有直接使用C语言传统的字符串表(以空字符结尾的字符数组,)而是自己构建了一个简单动态字符串(SDS)的抽象类型, 并将SDS用作Redis的默认字符串表示。

在Redis里面,C字符串只会作为字符串字面量用在一些无须对字符串值进行修改的地方,例如打印日志。

set msg "hello world"

  • 键值对的键是一个字符串对象,对象底层实现是一个保存着字符串“msg”的SDS。

  • 键值对的值也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world” 的 SDS。

在list命令中

image.png

  • 键值对的键是一个字符串对象,对象底层实现是一个保存着字符串“mypush”的SDS。

  • 键值对的值是一个列表对象,列表对象包含了五个字符串对象,这三个字符串对象分别由五个SDS实现

SDS

SDS是什么呢,先来看张图

image.png

  • len 属性值是5,表示这个SDS保存了一个五个字节长度的字符串

  • free 属性的值为0,表示这个SDS没有分配任何未使用的空间

  • buf属性是个char类型的数组,数组前五个字节分别保存了'R'、‘e’、‘d’、‘i’、‘s’五个字符,而最后一个字节则保存了空字符‘\0’

SDS遵循C字符串以空字符结尾的惯例。保存的空字符的1字节空间不计算在sds的len属性里。

SDS与C字符串的区别

C字符串使用长度为N+1的字符数组表示长度为N的字符串。

image.png

  • C字符串不记录自身长度,所以当计算C字符串长度,就要遍历计算,复杂度为0(N),而SDS记录字符串长度,复杂度为O(1).

  • C字符串不记录自身长度会带来容易造成缓冲区溢出的情况,C字符串通过strcat函数将src字符串的内容拼接到dest字符串的末尾。

image.png

这里举个例子,假设程序又两个内存中紧邻的C字符串s1和s2,s1保存了“Redis”,s2保存了'MongoDB',

image.png

如果决定执行下面这个命令

image.png

将s1内容修改为“Redis Cluster”,但可能在执行strcat前忘了为s1分配足够空间,那么在strcat函数执行后,s1的数据将溢出到s2的所在空间导致s2的内容被意外修改。

与C字符串不同,SDS完全杜绝了发送缓冲区溢出的可能性,当SAS api 需要对SDS进行修改时,会检查SDS空间是否满足修改所需需求,不满足AP会自动扩展空间,修改所需大小之后再进行实际的修改。

  • 减少修改字符串带来的内存重分配次数

在C字符串中,C字符串的长度总是N+1个字符长的数组,因此每次增长或者缩短一个C字符串都需要对C字符串数组进行一个内存重分配操作,如果是频繁的修改这样会对性能造成影响

SDS会通过未使用空间解除字符串长度和底层数组之间的关联,buf数组的长度不一定极速字符数量加一,数组里会包含未使用字节,这些未使用字节由free属性记录,通过未使用空间,实现空间预分配和惰性空间释放两种优化策略。

空间预分配就是当字符串进行增长操作,除了会分配修改所需要的必要空间,还会为SDS分配额外的未使用空间,这样就可以减少连续执行增长操作所需的内存重分配次数。

惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序不会立即使用内存重分配来回收多出来的字节,而是使用free属性将这些字节数量记录起来,等待将来使用。

image.png

image.png

list的底层原理 链表

list 底层是用链表来存储数据,这个链表是一个双向链表

来看下内部结构

image.png

链表每个节点都是由listNode组成,前置节点,后置节点和节点值

整个链表的组成内部结构如下图

image.png

list结构提供了头节点,尾节点,以及链表长度len,而dup、free、match用于实现多态链表所需类型的函数

dup函数用于复制链表节点所保存的值

free函数用于释放链表节点所保存的值

match函数用于对比链表节点所保存的值和另一个输入值是否相等

链表特性总结

  • 双端:链表节点有prev何next指针,所以是双向链表

  • 无环:表头节点prev和表尾节点指针都指向null

  • 带表头指针和表尾指针

  • 带链表长度计数器

  • 多态:可以通过dup,free,match为节点值设置类型特定函数

image.png

字典表

字典是一种用于保存键值对的抽象数据结构

在字典中,一个键(key)可以和一个值进行关联,这些关联的键和值就称为键值对。

字典中每个键都是独一无二的,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值删除值等。

Redis自己构建了字典的实现,字典在redis的应用很广泛,例如Redis的数据库就是使用字典来作为底层实现。

例如 image.png 键是msg,value是 hello,这个键值对就是保存在代表数据库的字典里面

字典还是哈希键的底层实现之一,当哈希键包含的键值对比较多,又或者键值对中元素都是比较长的字符串,Redis会使用字典作为哈希键的底层实现。

例如 website是包含10086个键值对的哈希键,这个websiet键的底层实现就是一个字典,字典中包含了10086个键值对。

字典底层实现原理

Redis的字典使用哈希表作为底层实现,一个哈希表可以有多个哈希表节点,每个哈希表节点保存了字典中的一个键值对

哈希表

image.png

  • table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对

  • size属性记录了哈希表的大小,也就是table数组的大小

  • used属性记录了哈希表目前已有节点(键值对)的数量。

  • sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。

image.png

哈希表节点

哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对

image.png

key属性保存着键值对中的键,val属性保存着键值对中的值。

next属性指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一起,用来解决键哈希冲突的问题。

image.png

字典

image.png

  • type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置。

type属性是一个指向dicttype结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数,而privdata属性则保存了需要传给那些类型特定函数的可选参数。

image.png

  • ht属性是一个包含两个项的数组,数组中每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。

  • rehashidx,它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1.

下图展示了一个普通的字典完整的结构

image.png

哈希算法

当要将一个新的键值对添加到字典中时,程序需要先根据键值对的中的键计算出哈希值和索引值,然后在根据索引值将包含新键值对的哈希表放到哈希表数组的指定索引上面。

计算哈希值和索引值方法如下

image.png

例如要将一个键值对k0和v0添加到字典里面,那么程序会先使用语句:

hash=dict->type->hashFunction(k0);

计算出hash值后,接着计算索引值

index=hash&dict->ht[0].sizemask=8&3=0;

计算出键k0的索引值是0,那么键值对k0和v0的节点就会被放置在哈希数组的索引0的位置上

image.png

解决键冲突

当有两个或两个以上数量的键被分配到哈希表数组的同一个索引上面时,我们称这些键发生了冲突。

Redis哈希表使用链地址法来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上多个节点就可以用这个单向链表连接,从而解决键冲突问题。

当发生冲突时总是将新节点添加到链表表头位置,所以这里使用的是头插法。

image.png

总结

image.png

跳跃表

跳跃表是一种有序的数据结构,他通过在每个节点维持多个指向其他节点的指针,从而达到快速访问节点的目的,跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

在Redis中,跳跃表只会用在两个地方,一个是作为有序集合的底层数据结构,一个是作为集群节点内部数据结构。

跳跃表的实现

Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,例如节点数量,以及指向表头节点和表尾节点的指针等。

跳跃表结构

image.png

image.png

图中展示了跳跃表zskiplist的结构

  • head:指向跳跃表的表头节点

  • tail:指向跳跃表的表尾节点

  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)

  • length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)

跳跃表节点结构

image.png

image.png

跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,层的数量越多,访问其他节点的速度就越快。程序会根据幂次定律,随机生成介于1到32之间的值作为level数组的大小,这个大小就是层的高度。

  • 前进指针

每个层都有一个指向表尾方向的前进指针,用于从表头节点向表尾方向访问节点。

image.png

图中虚线标书出程序从表头向表尾方向,遍历跳跃表中所有节点的路径

  • 跨度

层的跨度用于记录两个节点之间的距离。

  • 后退指针

节点的后退指针用于从表尾向表头方向访问节点,每个节点只有一个后退指针,所以不能向前进指针一样可以跳过多个节点,后退节点每次只能后退至前一个节点。

  • 分值和成员

节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。

节点的成员对象obj 是一个指针,指向一个字符串对象,而字符串对象保存着一个SDS值。

在同一个跳跃表内,各个节点保存的成员对象必须是唯一的,但多个节点保存的分值可以是相同的。

分值相同的节点将按照成员对象大小来来进行排序

例如

image.png

这里面的三个跳跃表节点分值相同,因为o1成员对象最小,o2次之,o3最大,所以他们的排序规则是o1<=o2<=o3

总结

  • 跳跃表是有序集合的底层实现之一。

  • Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成,其中zskiplist用于保存跳跃表信息,zskiplistNode用于表示跳跃表节点。

  • 每个跳跃表节点的层都是1至32之间的随机数

  • 在同一个跳跃表中,多个节点key包含相同的分值,但每个节点成员对象是唯一的。

  • 跳跃表的结点按照分值大小进行排序,分值相同时,按照成员对象大小来进行排序。

整数集合

整数集合(intset)是集合键底层实现之一,当一个集合只包含整数值元素,并且结合元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

举个简单的例子

image.png

这里向集合中存储了6个数字,查了下采用底层结构,就是整数集合。

整数集合的实现

整数集合可以保存int16_t、int32_t或者int_64_t的整数值,这是啥意思呢,就是整数集合可以保存16位,32位,64位的整数,并且会保证集合不会出现重复元素。

image.png

  • contents数组保存的是整数集合里的具体元素值,并且是按照值的袋熊有序排列,并且不包含重复项。

  • length: 记录整数集合包含的元素数量,也就是contents数组的长度。

  • encoding:encoding包含了编码方式,实际上也是决定contents数组里元素的类型,如果encoding属性的值是int16,那么contents就是一个int16_t类型的数组,数组里每个项都是一个int16_t类型的整数值,

encoding如果是int32,那么contents就是int32_t的数组。

升级

如果当前contents数组里的元素都是int16_t类型,这时进来一个int32_t类型的元素,那么整个集合的所有元素都会被转换成int64_t,具体是如何升级的,接着往下看。

升级整数集合大致分为三步

  • 根据新元素类型,扩展整数集合底层数组空间大小,并为新元素分配空间

  • 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确的位置上,放置时同时也要保住有序性不变。

  • 将新元素添加到底层数组里。

因为每次向整数集合添加新元素都有可能会引起升级,所以添加新元素的时间复杂度为O(N)。

升级可以提升整数集合的灵活性,另一个是尽可能的节约内存。

降级

整集合不支持降级操作,一旦升级,就会保持升级的状态。

总结

  • 整数集合是集合键的实现之一

  • 整数集合的底层实现为数组

  • 升级操作为整数集合带来了操作的灵活性,并节约了内存

  • 整数集合只支持升级操作,不支持降级操作

压缩列表

压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量的列表项,并且每个列表项要么是小整数值,要么是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。而当一个哈希键只包含少量键值对,并且每个键值对和值要么是小整数值,要么是长度比较短的字符串,Redis也会使用压缩列表来作为哈希键的底层实现。

这里举例说明,来验证上述的情况

image.png

image.png

压缩列表构成

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。

image.png

  • zlbytes:记录整个压缩列表占用的字节数

  • zltail: 记录压缩列表尾节点距离压缩列表的起始地址有多少字节。

  • zllen:记录了压缩列表包含的节点数量

  • entryX:压缩列表包含的各个节点,节点的长度由节点保存的内容决定

  • zlend:用于标记压缩列表的末端

压缩列表节点的构成

每个压缩列表节点可以保存一个字节数组或者一个整数值

字节数组可以是三种长度的其中一种:

长度小于等于63字节的字节数组

长度小于等于2的14次方-1的字节数组

长度小于等于2的32次方减1字节的字节数组

整数值则是6种长度的其中一种:

4位长的整数

1字节长

3字节长

int16_t类型

int_32_t类型

int_64_t类型

压缩列表节点构成是由三部分组成

image.png

  • previous_entry_length

记录了压缩列表前一个节点的长度hang'd性长度可以是1字节或者5字节

如果前一节点长度小于254字节,那么previous_entry_length长度为1字节

如果前一节点长度大于等于254字节,那么属性长度为5字节

  • encoding

  • 节点encoding记录了节点的content属性所保存数据的类型及长度

image.png

  • content

content负责保存节点的值,节点值可以是字节数组或者整数

image.png

连锁更新

如果在一个压缩列表,有多个连续的、长度介于250字节到253字节之间的节点e1至eN

image.png

这些节点长度都小于254字节,所以记录这些节点的长度只需要1字节长的previous_entry_length属性

这时,如果我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点。

image.png

因为e1的previous_entry_length 属性仅长1字节,它没办法保存新节点new的长度,所以程序将压缩列表执行空间重分配操作,并将e1节点的previous_entry_length属性从原来1字节变为5字节,但是当变为5字节,e1长度就超过了254,那么e2的provious_entry_lengt就要从原来1字节变为5字节,这样就会往后影响到e3,e4以及之后的节点,造成连锁更新,同样的删除节点也会造成连锁更新

总结

image.png

image.png