Redis数据结构

Redis开发

API及底层实现

全局命令

keys *:查看所有的键 O(n)

dbsize:键总数 O(1)

exists key :检查键是否存在

del key:删除键

expire key seconds:键过期

ttl : 返回键的剩余时间 (

  • >=0: 剩余时间
  • -1:没设置过期时间
  • -2:键不存在

)

type key:键的类型

object encoding:查询内部编码

数据结构和内部编码:

每种数据结构都有两种以上的内部编码实现,好处:

  1. 可以改进内部编码,而对外的数据结构和命令没有影响
  2. 多种内部编码实现可以在不同场景下发挥各自的优势

单线程架构

为什么单线程还能这么快?

  1. 纯内存访问
  2. 非阻塞I/O,redis使用epoll作为I/O多路复用技术的实现
  3. 单线程避免了线程切换和竞态产生的消耗

缺点:如果某个命令执行时间过长,会造成其他命令的阻塞。

字符串

字符串类型的值可以是普通字符串,也可以是复杂字符串(Json, Xml), 数字(整数、浮点数),二进制的图片、音频、视频,但值最大不能超过512MB

字符串api

  1. set key value [ex seconds] [px milliseconds] [nx | xx]

    • ex seconds : 为键设置秒级过期时间
    • px milliseconds : 为键设置毫秒级过期时间
    • nx : 键不存在才设置成功,用于添加
    • xx : 键存在才设置成功,用于更新
  2. get key

  3. mset key value [key value ……]

  4. mget key [key……]

  5. incr key 自增

    • 值不是整数,返回错误
    • 值是整数,返回自增后的结果
    • 键不存在,按照值为0自增,返回结果为1
  6. decr、incrby(自增指定数字)、decrby、incrbyfloat

  7. append key value 向字符串尾部追加值

  8. strlen key 字符串长度

    每个中文占用3个字节

  9. getset key value 设置并返回原值

  10. setrange key offset value 设置指定位置的字符

  11. getrange key start end 获取部分字符串,下标从0开始,包括右下标

字符串内部编码

有3种编码:

  • int:8个字节的长整型
  • embstr:小于等于39个字节的字符串
  • raw:大于39个字节的字符串

通过object encoding key 查看内部编码

使用场景

  • 缓存功能
  • 计数:视频播放数
  • 共享session:出于分布式服务考虑,已不适用
  • 限速:防盗刷

哈希

api

  • hset key field value 设置值
  • hget key field 获取值
  • hdel key field 删除filed
  • hlen key 计算field的个数
  • hmget key field [filed ……]
  • hmset key filed value ……
  • hexists key field 判断field是否存在
  • hkeys key 获取所有field
  • hvals key 获取所有value
  • hgetall key 获取所有的f-v,如果个数过多,会造成阻塞
  • hincrby key field
  • hincrbyfloat key field
  • hstrlen key field 计算value的长度

内部编码

有2种:

ziplist (压缩列表):当元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节),redis使用ziplist作为内部编码。ziplist 使用紧凑的结构实现多个元素的连续存储,在节省内存方面比hashtable更加好。

hashtable (哈希表):当上述条件无法满足时,redis使用hashtable,因为此时ziplist 的读写效率会下降,而hashtable 的读写时间复杂度为O(1)

使用场景

缓存:用户的id作为键后缀,多对f-v对应每个用户的属性

列表

一个列表最多可以存储232-1 个元素,列表中的元素是有序的且可以重复的

api

lpush key value 从左插入元素

rpush key value 从右插入元素

lrange 0 -1 从左到右获取列表中的所有元素

linsert key before | after pivot value 从列表中找到等于pivot的元素,插入

lrange key start end 获取指定索引范围的所有元素,索引下标:从左到右是0 - N-1,从右到左是-1 - -N;lrange中的end元素包括自身。

lindex key index 获取指定索引下标

llen key 获取列表长度

lpop key 从左弹出元素

lrem key count value

  • count > 0, 从左到右,最多删除count个value值的元素
  • count < 0, 从右到左,最多删除count绝对值的元素
  • count = 0,删除所有。

ltrim key start end 按照索引范围剪列表

lset key index newValue 修改指定索引下标的元素

blpop/brpop key timeout 阻塞弹出

  • key 可以多个

  • timeout 如果列表为空,则等待timeout时间返回。timeout = 0时,则一直等待。

  • 如果列表不为空,立即返回

    注意:

    • 如果有多个键,则从左到右遍历键,有一个键可以返回就立即返回。
    • 如果多个客户端对同一个键执行blpop,则最先执行命令的客户端返回,其他客户端阻塞。

内部编码

ziplist:当列表元素个数小于list - max - ziplist - entries(默认512个),同时列表中每个元素的值小于list - max - ziplist - values(默认64字节)

linkedlist:无法满足ziplist条件,则用linkedlist

使用场景

  • 消息队列:用rpush + blpop 实现阻塞队列。多个消费者客户端使用blpop阻塞“抢”列表尾部元素,多个客户端保证了消费的负载均衡和高可用性。
  • 文章列表:每个用户有自己的文章列表,需要分页展示文章列表
    • rpush user:1:articles 文章
    • lrange user:1:articles 0 9 (ps:取出前10条)

集合

集合不允许有重复元素,且元素是无序的,不能通过索引获取。

api

sadd key element[...] 添加元素

srem key element[...] 删除元素

scard key 计算元素个数

sismember key element 判断元素是否在集合中,返回1存在,0不存在

srandmember key 【count】 随机返回集合中指定个数的元素

spop key 随机弹出

smembers key 获取所有元素,可能会造成阻塞

sinter key [key...] 求交集

sunion key [key...] 求并集

sdiff key [key...] 求差集

sinterstore / sunionstore / sdiffstore destination key [key...] 求结果并保存

内部编码

intset(整数集合):元素都是整数并且元素个数小于 set - max - intset - entries (默认512个),减少内存的使用

hashtable(哈希表):无法满足上述,则用hashtable

使用场景

  • 标签:比如用户对娱乐、体育、历史等比较感兴趣,可以得到喜欢同一个标签的人,以及用户的共同喜好标签。
    • 给用户添加标签和给标签添加用户,需要在一个事务中进行
    • 删除用户下的标签和删除标签下的用户,也需要在一个事务中进行
    • 计算用户共同感兴趣的标签:sinter user:1:tage user2:tag
  • 抽奖

有序集合

没有重复元素,给每个元素设置一个分数,作为排序的依据;提供了获取指定元素分数和元素范围查询、计算成员排名等功能

api

zadd key score member [....] 添加成员

zcard key 计算成员个数

zscore key memeber 计算某个成员分数

zrank key memeber 从低到高排名

zrevrank key member 从高到底排名

zrem key member [....] 删除成员

zincrby key increment member 增加成员分数

zrange key start end [withscores] 返回指定排名范围成员

zrevrange key start end [withscores] 返回指定排名范围成员

zrangebyscore key min max 返回指定分数范围成员

zcount key min max 返回指定分数范围成员个数

zremrangebyrank key start end 删除指定排名范围内的元素

zremrangebyscore key min max 删除指定分数范围内的元素

zinterstore destination numkeys key [key...] [weights [weight ...]] [aggregate sum|min|max] 求交集

  • destination : 保存到目标集合
  • numkeys :键的个数
  • weights:每个键的权重,默认是1
  • aggregate :聚合运算,默认是sum

zunionstore......

内部编码

ziplist:元素个数小于zset - max - ziplist - entries(默认128个),值小于zset - max - ziplist - value(默认64字节)

skiplist:不满足上述条件

使用场景

  • 排行榜:比如对用户上传的视频做排行榜,可以按照时间、播放量、赞数排行。

    比如赞数:

    • 添加用户赞数

      zadd ...

      之后再获得一个赞:zincrby ....

    • 取消用户赞数

      zrem ...

    • 展示获取赞数最多的10个用户

      zrevrangebyrank ...

压缩列表底层实现

压缩列表是列表键和哈希键的底层实现之一,键值要么是小整数值,要么是短字符串

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

压缩列表组成:

属性 类型 长度 用途
zlbytes uint32_t 4字节 记录整个压缩列表占用的字节数:进行内存重分配或者计算zlend位置时使用
zltail uint32_t 4字节 记录表尾节点距离压缩列表的起始地址有多少字节:可以无须遍历就确定尾节点地址
zllen uint16_t 2字节 记录列表包含的节点数量:当小于65535时,节点数量;否则,只能遍历算出数量
entryX 列表节点 不定 压缩列表节点,节点的长度由节点内容决定
zlend uint8_t 1字节 特殊值OxFF(十进制255),用于标记压缩列表的末端

节点组成:

每个节点可以保存一个字节数组或一个整数值:

字节数组

  • 长度小于等于26 - 1的字节数组
  • 小于等于214 - 1
  • 小于等于232 - 1

整数值

  • 4位长,0-12的无符号整数
  • 1字节长的有符号整数
  • 3字节长的有符号整数
  • int16_t 类型整数
  • int32_t 类型整数
  • int64_t 类型整数

previous_entry_length:以字节为单位,可以是1字节或5字节,记录前一个节点的长度

  • 如果前一个节点的长度小于254字节,该值为1字节

  • 如果前一个节点的长度大于等于254字节,该值为5字节,第一字节会被设置为0xFE(254),后4个字节表示前一节点的长度

    作用:可以根据当前节点的起始地址计算前一节点的起始地址,压缩列表从表尾到表头的遍历操作就是这样实现的。

encoding:记录content属性保存的数据类型及长度

  • 字节数组:1字节、2字节或5字节长,值最高位为00,01或者10

    编码 编码长度 content属性保存的值
    00bbbbbbb 1字节 长度小于等于26 - 1字节的字节数组
    01bbbbbbb xxxxxxxx 2字节 长度小于等于214 - 1字节的字节数组
    10___ aaa…… 5字节 长度小于等于232 - 1字节的字节数组
  • 整数:1字节长,值的最高位以11开头

    编码 编码长度 content属性保存的值
    11000000 1字节 int16_t类型的整数
    11010000 1字节 int32_t类型的整数
    11100000 1字节 int64_t类型的整数
    11110000 1字节 1字节
    11111110 1字节 8位有符号整数
    1111xxxx 1字节 没有content属性,本身包含了0-12的值

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

连锁更新:压缩列表恰有好多个连续的、长度介于250字节至253字节的节点,当添加一个长度大于等于254字节的节点时,可能会导致后续的节点的previous_entry_length属性从1字节扩展为5字节。删除节点也可能产生更新。时间复杂度O(n2)

整数集合底层实现

整数集合是集合键的底层实现之一,集合只包含整数值元素并且元素数量不多。

encoding:contents数组中存储值的类型

  • int16_t
  • int32_t
  • int64_t

length:集合包含的元素个数

contents:按照从小到大的顺序保存元素

升级:当添加一个新元素到整数集合时,并且新元素的类型比整数集合中所有元素类型都要长时,要先进行升级,然后才将新元素添加到整数集合中。

​ 升级整数集合的三步骤:

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

  • 将底层数组现有的所有元素都转换成与新元素相同的类型,然后将元素放到正确的位置上,需要维持底层数组的有序性

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

    每次升级都需要对底层数组的元素进行类型转换,因此向整数集合添加新元素的时间复杂度为O(N)

升级后新元素的位置:

​ 引发升级的新元素的长度总是比整数集合所有的元素长度大,

  • 新元素小于所有现有元素时,新元素会被放置在底层数组的最开头
  • 新元素大于所有现有元素时,新元素会被放置在底层数组的最末尾

升级的好处:提升整数集合的灵活性,尽可能节约内存

​ 提升灵活性:整数集合通过自动升级底层数组来适应新元素,因此可以随意将不同类型的整数添加到集合中,灵活性较好。

​ 节约内存:整数集合既能保存三种不同类型的值,又能确保升级操作只会在有需要的时候进行,可以尽量节省内存。

整数集合不支持降级,一旦升级,就会一直保持升级后的状态

跳跃表底层实现

跳表是一种有序的数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的

跳表是有序集合键的底层实现之一,当有序集合中包含的元素个数较多或者元素的成员是比较长的字符串时

redis用到跳表的地方:有序集合键、集群节点的内部数据结构

表头节点的结构:表头节点也有后退指针、分值和成员对象

  • header:指向跳跃表的表头节点
  • tail:指向跳跃表的表尾节点
  • level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计)
  • length:跳跃表的长度

跳表节点的结构

  • level数组:level数组可以包含多个元素,每个元素都包含前进指针和跨度。

    ​ 前进指针用于从表头向表尾访问其他节点,跨度则记录了前进指针所指向节点和当前节点的距离。

    ​ 层的数量越多,访问其他节点的速度就更快。

    ​ 每次创建一个新节点时,会随机生成一个1-32之间的值作为level数组的大小。

    ​ 跨度实际上是用来排位的,而不是遍历:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计得到的结 果就是目标节点在跳表中的排位。

  • 后退指针:指向位于当前节点的前一个节点,可以用于从表尾向表头遍历时使用,每次只能后退至前一个节点

  • 分值:在跳表中,节点按各自所保存的分值从小到大排列

  • 成员对象:节点所保存的成员对象

    各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,如果分值相同,则按照成员对象的字典序来排序,较小的排前面。

字典底层实现

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

字典组成

  • type:一个指向dictType结构的指针,每个dictType保存了一组用于操作特定类型键值对的函数

    dictType包含的函数:计算hash值;复制键;复制值;销毁键;销毁值;对比键

  • privdata:传给函数的可选参数

  • ht:包含两个项的数组,每个项都是一个哈希表,一般只是用ht[0],ht[1]只会在对ht[0]哈希表进行rehash使用

  • rehashidx:rehash索引,记录当前rehash的进度,如果当前没有进行rehash,值为-1

哈希表组成:

  • table:哈希数组,数组中的每一个元素指向一个entry结构,每个entry保存一个键值对
  • size:数组的大小
  • sizemask:等于size - 1,用hash值&sizemask算出键应该放在哪个索引上
  • used:目前哈希表的键值对数量

哈希表节点组成:

  • key:键
  • v:值,可以是一个指针或者uint64_t或者是int64_t
  • next:指向另一个节点的指针,将多个哈希值相同的键值对连接起来

哈希算法:

使用MurmurHash2算法,调用type中的计算哈希值的函数算出hash,然后和sizemask进行按位&运算

键冲突:

采用链地址法,为了速度考虑,采用头插法,总是将新节点添加到链表的表头

rehash:

当保存的键值对太多或太少,需要通过rehash对哈希表的大小进行相应的扩展或者收缩,让负载因子维持在一个合理范围内。

rehash步骤:

  • 为哈希表ht[1] 分配空间,空间的大小取决于要执行的操作以及ht[0]当前包含的键值对数量:
    • 如果是扩展操作,ht[1] 大小为第一个大于等于ht[0].used * 2 的 2n
    • 如果是收缩操作,ht[1] 大小为第一个大于等于ht[0].used 的 2n
  • 将ht[0] 的所有键值对rehash到ht[1]
  • 当ht[0] 的所有键值对迁移到ht[1]后,释放ht[0],将ht[1] 设置为ht[0],并为ht[1] 新建一个空白的哈希表,为下一次rehash做准备

哈希表的扩展与收缩:

扩展:

  • 当前没有在执行 BGSAVEBGREWRITEAOF ,并且负载因子大于等于1

  • 当前有在执行 BGSAVEBGREWRITEAOF,并且负载因子大于等于5

    负载因子 = (哈希表已保存的节点数量)/ 哈希表大小

    在执行BGSAVEBGREWRITEAOF 时,需要创建子进程,大多数操作系统都是采用写时复制来优化子进程使用效率,所以在子进程存在期间,提高执行扩展所需负载因子,避免子进程存在期间进行扩展操作,避免不必要的内存写入,最大限度节约内存。

收缩:负载因子小于0.1时,执行收缩操作。

渐进式rehash:

rehash并不是一次性完成,而是分多次、渐进式完成,如果键值对数量比较多,要一次性将全部键值对rehash到ht[1],可能会导致服务器在一段时间内停止服务

rehash步骤:

  • 为ht[1] 分配空间
  • 维持索引计数器rehashidx,并将值设为0,表示rehash开始
  • rehash期间,每次对字典执行增删改查时,顺带将ht[0] 在rehashidx 索引上的所有键值对rehash到ht[1],rehash完成后,rehashidx加1
  • 当ht[0] 全部的键值对rehash完成后,将rehashidx设置为-1,表示完成

rehash期间操作:

  • 增:新添加操作只被保存到ht[1],ht[0]不再进行任何添加操作,保证ht[0]键值对的数量只增不减,最终变成空表
  • 删、改、查:先在ht[0]里进行,如果没有操作成功,再到ht[1]进行。

链表底层实现

链表是列表键的底层实现之一,除此之外,发布与订阅、慢查询、监视器等功能也用到了链表,Redis本身也使用了链表保存多个客户端的状态信息

链表组成:

  • head:表头节点
  • tail:表尾节点
  • len:节点的数量
  • dup:复制链表节点所保存的值
  • free:释放链表节点所保存的值
  • mathch:对比链表节点所保存的值和另一个输入值是否相等

节点组成:

  • prev:前驱节点
  • next:后置节点
  • value:节点的值

特性:

  • 双向无环链表
  • 带表头和表尾指针
  • 带链表长度计数器
  • 多态

简单动态字符串

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

字符串值的键值对都是SDS实现的,SDS还可以被用作缓冲区:AOF缓冲区、客户端输入缓冲区

  • len:字符串的长度

  • free:未使用的字节数量

  • buf:字节数组,用于保存字符串

    SDS遵循C字符串以空字符结尾的惯例,空字符的1字节空间不计算在len中,好处:可以直接重用一部分C字符串函数。

SDS与C字符串的区别:

  • 常数复杂度获取字符串长度

    C字符串不记录自身的长度,要获取长度,必须遍历,O(N);SDS只要反问len属性就可以知道长度,即使对一个非常长的字符串键反复执行获取长度的命令,也不会对系统造成任何影响。

  • 杜绝缓冲区溢出

    C字符串如果没有分配足够的内存空间,在执行修改字符串时可能会溢出。SDS杜绝了缓冲区溢出的可能性:当SDS修改时,会先检查SDS空间是否满足修改所需的要求,不满足自动将SDS扩展至执行修改所需的大小,然后执行实际修改操作。

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

    每次增长或缩短一个C字符串,都要进行一次内存重分配操作:

    • 如果是增长操作,需要先通过内存重分配来扩展底层数组,忘了这一步会产生缓冲区溢出

    • 如果是缩短操作,需要先通过内存重分配来释放不再使用的空间,忘 了这一步会产生内存泄漏

      内存重分配是一个耗时的操作,如果每次修改字符串都要进行内存重分配,在修改频繁的情况下,会对性能造成影响。

    SDS的空间预分配和惰性空间两种优化策略:

    1. 空间预分配:每次修改并需要对SDS进行扩展时,除了分配修改所需要的空间,还需要分配额外未使用的空间

      • 如果修改之后,SDS的长度小于1MB,将分配和len属性同样大小的未使用空间,这时buf实际长度等于len字节+free字节+1字节

      • 如果修改之后,SDS的长度大于等于1MB,将分配1MB未使用空间,buf实际长度等于lenMB+1MB+1字节

        通过空间预分配,可以减少连续执行字符串增长操作所需的内存重分配次数,和C字符串对比,将连续增长N次所需的内存重分配次数从必定的N次降低为最多N次

    2. 惰性空间释放:优化缩短SDS操作,需要缩短SDS时,不是立即用内存重分配来回收缩短后多出来的字节,而是使用free记录这些字节的数量,并等待将来使用。

      通过惰性空间释放,SDS避免了缩短字符串所需的内存重分配操作,并为将来可能有的增长操作提供了优化

  • 二进制安全

    C字符串除了字符串末尾的空字符外,里面不能包含空字符,使得只能保存文本数据,不能保存图片、音频、视频等二进制文件。

    SDS的buf属性是保存二进制数据,SDS会以处理二进制的方式处理buf数组,数据写入时什么样,读出时就什么样。

  • 重用部分C字符串函数

    SDS遵循C字符串以空字符结尾,可以让保存文本数据的SDS重用一部分C字符串函数。

posted @ 2020-12-06 10:53  西凉马戳戳  阅读(240)  评论(0编辑  收藏  举报