Redis 的数据结构解析

Redis 的数据结构

底层数据结构

简单动态字符串SDS:

  • 用于实现字符串,SDS 还被用作缓冲区:AOF 缓冲区,客户端状态中的输入缓冲区。

  • C语言字符串的区别和SDS(Simple Dynamic String)的数据结构(需要注意的是字符串最大长度为 512M)

    1. 常数复杂度获得字符串长度:C语言:获取长度需要遍历整个数组,O(n)复杂度;SDS只需要访问len属性就能得到字符串的长度,复杂度为O(1)。

    2. 杜绝缓冲区溢出:对于字符串的拼接、复制等操作,C语言开发者必须确保目标字符串的空间足够大,不然就会出现溢出;SDS由于有len,对字符串修改时候先根据len检查内存空间是否满足需 求,不满足则进行空间扩展不会发生溢出。

    3. 减少修改字符串长度所需的内存重分配次数:

      • 空间预分配:可以减少连续执行字符串增长操作所需的内存分配次数。

        • 空间预分配策略:sds存储的数据未超过1MB,长度扩容成原来的两倍;超过1MB,长度扩容1MB(避免空间浪费)
      • 惰性空间释放:当需要缩短字符串时,不是立刻回收空间,而是用free字段记录未使用的空间(可用的长度大小)

    4. 二进制安全:C语言的String无法存储图片这样的(二进制数据),因为会对空格字符解释成字符串的结尾符;SDS额外实现了添加字符的API,不是沿用C语言的String API,不会对空格字符解释成字符串的结尾符。

链表:

  • 用于实现列表键,还有发布、订阅、慢查询、监视器都用到了链表。

  • Redis的链表是双向链表

  • 无环,表头节点的prev和表尾节点next指向null

  • 带链表长度计数器

  • 多态。使用 void* 指针来保存节点值,并通过 list 结构的 dup、free。match 三个属性为节点值设置 类型特定函数。

字典:

  • 用于实现哈希键,还有数据库底层。

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

  • 使用链地址法解决哈希冲突

  • 在堆哈希表扩展或者收缩时,程序需要将所有键值对rehash到新哈希表上,并且这个过程不是一次性的,而是渐进式完成的。

    【渐进式 rehash】

    • 维持一个索引计数器变量 rehashidx ,表示 rehash 工作正式开始, 并将它的值设置为 0 ,在每次增删改查时都会触发rehash 。

    • 在 rehash 进行期间, 每次对字典执行增删改查, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有entry键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。

    • 在 rehash 过程中,新增操作,则直接写入 ht[1],查询、修改和删除则会在dict.ht[0] 和 dict.ht[1] 依次查找并执行(数据要么在ht0要么在ht1)。这样可以确保 ht[0] 的数据只减不增,随着 rehash 最终为空。

    • 完成后程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

跳表:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vmg5O6CM-1683526867321)(images/image-20221020013841584.png)]

  • 用于实现有序集合。
  • 跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。跳表在原有的有序链表上增加了多级索引,通过索引来实现快速查询。
  • 跳跃表支持平均O(logN)、最坏O(N)的查找。
  • 查找过程:最高层开始遍历找到第一个节点 (最后一个比「我」小的元素,也就是第一个比「我」大的元素的前一个),然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最后一个比我「小」的元素)。
  • 每个跳跃节点的层高都是1~32的随机数。
  • 跳跃表中的节点按照分值大小排序,当分值相同时,按照成员对象的大小排序。
  • 跳表与平衡树、哈希表的比较:
    • 跳表和各种平衡树(如 AVL、红黑树、B 树等)的元素是有序排列的,而哈希表不是有序的。因 此,在哈希表上只能做单个 key 的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小 在指定的两个值之间的所有节点。
    • 跳表属于一种以空间换时间的数据结构,从内存占用上来说,一般情况下,平衡树相较于跳表空间 复杂度更低。,并且平衡树稳定,跳表不稳定(最坏时间复杂度O(n))。
    • 1、跳表的实现更加简单,不用旋转节点,相对效率更高
      2、跳表在范围查询的时候的效率是高于红黑树的,因为跳表是从上层往下层查找的,上层的区域范围更广,可以快速定位到查询的范围(我认为是最重要的)
      3、平衡树的插入和删除操作可能引发子树的调整、逻辑复杂,而跳表只需要维护相邻节点即可
      4、查找单个key,跳表和平衡树时间复杂度都是O(logN)

整数集合:

  • 用于实现集合键

  • 整数集合的底层实现为数组,这个数组以有序、不重复的方式保存集合元素,在有需要的时候,程序会根据新添加元素的类型,改变整个数组的类型。

  • 升级操作是为了节约内存。整数集合只支持升级,不支持降级。

    【升级添加新元素】

    • 根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间;
    • 把数组现有的元素都转换成新元素的类型,并将转换后的元素放到正确的位置,且要保持数组的有序性;
    • 添加新元素到底层数组;

压缩列表:

  • 用于实现列表键和哈希键

  • 压缩列表是redis为了节约内存开发的,由一系列特殊编码的连续内存块组成的顺序型数据结构。

  • 可以在任意一端进行压入、弹出操作,并且该操作的时间复杂度为O(1)。

  • 压缩原理:1. 节约指针开销prev_len; 2. ,每个节点不同数据类型不同编码,长度不同。

  • 连锁更新:添加节点或者删除节点,可能会引发连锁更新问题,但出现几率并不高,后续可以用quickList解决。

    每个节点都有prev_len属性记录了前一个节点的长度。根据前一个节点的长度大小分配空间不一样。

    添加元素:new_255 e1_253 e2_253 e3_253 …连锁更新

    删除元素:big_255 samll_253 e2_253 e3_253 …连锁更新

    【quicklist】

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

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ivyuXaL-1683526867322)(images/164e3b0b953f2fc7tplv-t2oaga2asx-zoom-in-crop-mark3024000.webp)]

对象:

  • Redis数据库中每个键值对的键和值都是一个对象,共有五种对象。

  • Redis 可以在执行命令之前, 根据对象的类型来判断一个对象是否可以执 行给定的命令。 使用对象的另一个好处是, 我们可以针对不同的使用场景, 为对象设置多种不同的数 据结构实现, 从而优化对象在不同场景下的使用效率。

    • 字符串对象:SDS

    • 列表对象:ziplist或者linkedlist

    • 哈希对象:ziplist或者hashtable

    • 集合对象:intset或者hashtable

    • 有序集合对象:ziplist或者skiplist

redis数据类型

  • String:String :String 是最常用的一种数据类型,采用 k-v 形式,一个 key 对应一个 value;string 类型是 二进制安全的。意思是 Redis 的 string 可以包含任何数据。如 jpg 图片或者序列化的对象 ;

    使用场景:常规 key-value 缓存应用。常规计数:微博数, 粉丝数。

  • Hash:hash 是一个键值 (key => value) 对集合。Redis hash 是一个 string 类型的 field 和 value 的映 射表,hash 特别适合用于存储对象。

    使用场景:常用于存储对象:用户信息,商品信息

hmset user:info age 30 name guanam page 50//批量获取hash key的一批field对应的值 O(n)
hmger user:info age name//O(n)
-"30"
-"guanam"
  • List:它的底层实际是个双向链表。

    使用场景:

    • 比如微博的关注列表, 粉丝列表, 消息列表

    • 异步队列:使用list结构作为队列,rpush生产消息,lpop消费消息。可是如果队列空了,客户端就会陷入 pop 的死循环,不停地 pop,没有数据,接着再 pop,又没有数据。这就是浪费生命的空轮询。当lpop没有消息的时候,要适当sleep一会再重试。

      可不可以不用sleep呢?list还有个指令叫blpop/brpop,在没有消息的时候,它会阻塞住直到消息到来。

  • Set:无序集合(内部使用值为空的哈希表),它通过计算hash的方式来快速去重,它能以 O(1) 的复杂度快速查询数据。

    使用场景:

    • 某些需要去重的列表,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
    • 聚合计算(并集、交集、差集)场景,共同好友,共同关注。
  • ZSet:zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员;每个元 素都会关联一个 double 类型的分数;redis 正是通过分数来为集合中的成员进行从小到大的排序。 zset 的成员是唯一的,但分数(score)却可以重复。

    使用场景:

    • 排行榜相关:ZADD leaderboard 。 得到前100名高分用户很简单: ZREVRANGE leaderboard 0 99。
    • 存储粉丝列表,value是用户id,score是关注时间。
    • 延时队列:拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

    ZSet由跳表+字典实现:

    需要跳表:方便范围查找,zrange、zrank

    需要字典:记录value和score关系,可以O(1)复杂度查找成员分值

  • Bitmap

    • 底层:Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身 。

    • 应用场景:需要保存状态信息(0/1 即可表示)的场景:用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视频)

  • HyperLogLog

    • 底层:用来做基数统计的算法,在输入元素的数量或体积非常大时,计算基数所需的空间总是固定的,并且是很小的(12k)。HyperLogLog 只会根据输入元素来计算基数,而不会存储输入元素本身

    • 应用场景:数量量巨大(百万、千万级别以上)的计数场景:热门网站每日/每周/每月访问 ip 数统计、热门帖子 uv 统计、

  • Geospatial:主要用于存储地理位置信息,常用于定位附近的人,打车距离的计算等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Guanam_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值