Redis 数据结构使用

Redis 数据结构使用

1. String的内存空间消耗问题

1.1 String在保存数据时内存空间消耗较多

  • String类型除了实际记录的数据,还需要额外的内存空间记录数据长度,空间使用等信息,这些信息被称为元数据
  • 当实际保存的数据比较小的时候,元数据的空间开销就会比较大
  • 当保存64位有符号整数的时候
    • String类型会将其保存为一个8字节的Long类型整数
    • int编码方式
  • 当保存的数据中包含字符的时候
    • String 用简单动态字符串 Simple Dynamic String , 共有三部分组成
      • buf
        • 字节数组,保存实际数据
        • 为了表示字节数组的结束,会在数组最后加一个\0. 这里会额外占用一个字节的开销
      • len
        • 占四个字节,表示buf的已用长度
      • alloc
        • 占四个字节,表示buf的实际分配长度,一般来说会大于len
    • 故而在上述的分析当中,len 和alloc就是元数据,带来了一部分的额外开销

1.2 RedisObject 的结构

  • Redis本身支持多种数据类型,而不同数据类型都会有一些相同的元数据需要记录

    • 最后一次访问的时间
    • 被引用的次数等
  • 因此Redis会用一个RedisObject结构体来统一记录这些元数据,同时指向实际数据

  • 另外出于节省内存空间的考虑

    • 当保存的是Long类型整数时,RedisObject中的指针就直接赋值为整数数据了,这样就不用额外的指针再指向整数,节省了指针的空间开销

    • 当保存的是字符串数据,并且字符串小于等于44个字节,RedisObject中的元数据,指针的SDS是一块连续的内存区域,来避免内存碎片

    • 当保存的数据量大于44字节的时候,SDS的数据量就会变多,Redis就不再把SDS和RedisObject布局在一起了,会给SDS分配独立的空间,并且用指针指向SDS结构

      Redis Object

  • 在计算总共消耗的内存的时候,值得注意的是除了使用RedisObject本身,Redis还维护了一个全局哈希表来保存所有键值对,这个结构体有三个8字节的指针,共24字节。Redis使用的是jemalloc内存分配库,会根据申请的字节数N,找一个比N大,但是最接近N的2的幂次数作为分配的空间,来减少频繁分配的次数

RedisObject Entity

2. 压缩列表

  • 压缩列表的构成

    压缩列表构成

    • 表头

      • zlbytes — 列表长度
      • zltail — 列表尾
      • zllen — 列表entry个数
    • 表尾

      • zlend — 列表结束
    • 表entry

      • 是连续的entry
        • 因为是挨着来进行放置的,所以不需要再使用额外的指针进行连接,就可以节省指针所占用的空间了
      • 包括以下几部分:
        • prev_len — 前一个entry的长度
        • len — 自身长度 4字节
        • encoding — 编码方式 1字节
        • content — 保存实际数据
    • Redis Hash类型底层有两种实现结构

      • 压缩列表
      • 哈希表
    • 通过阈值确定应该使用哪一种来保存数据

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

3. 集合的使用

  • 使用场景
    • 一个key对应一个数据集合
      • 比如手机app的用户登录信息 — 一天对应一系列用户ID或者移动设备ID
      • 电商商品用户评价列表 — 一个商品对应一系列评论
    • 在这样子的场景当中,除了记录信息,我们还需要对集合当中的数据进行统计,而我们选用的数据类型必须能够高效的统计这些数据

3.1 聚合统计

  • 定义

    • 指统计多个集合元素的聚合结果
  • 例子

    • 统计多个集合的共有元素
    • 将两个集合相比,统计其中一个集合独有的元素
    • 统计多个集合的所有元素
  • 当你需要对多个集合进行聚合计算时,Set 类型会是一个非常不错的选择。

  • 这里有一个潜在的风险。Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。

    • 你可以从主从集群中选择一个从库,让它专门负责聚合计算
    • 或者是把数据读取到客户端,在客户端来完成聚合统计,这样就可以规避阻塞主库实例和其他从库实例的风险了。

3.2 排序统计

  • E.G
    • 电商网站提供最新评论列表,需要使得集合当中的元素可以按序排列
      • List和Sorted Set属于有序集合
      • List按照元素进入List的顺序进行排序,而Sorted Set根据元素的权重来排序
  • 使用list来做排序的问题在于是通过元素在List当中的位置来排序,新元素插入会改变原有的顺序,都会顺次后移
  • Sorted Set相对应的是根据元素的实际权重来排序和获取数据
    • Sorted Set 的 ZRANGEBYSCORE 命令就可以按权重排序后返回元素。这样的话,即使集合中的元素频繁更新,Sorted Set 也能通过 ZRANGEBYSCORE 命令准确地获取到按序排列的数据。

3.3 二值状态统计

  • 集合元素的取值只有0和1两种
    • 可以对于数据结构进行优化,使用Bitmap
    • Bitmap本身是使用String类型作为底层数据结构来实现的一种统计二值状态的数据类型,可以将其理解为一个bit数组
  • 查看签到情况的基本操作
// 记录8.3 签到
SETBIT uid:sign:3000:202008 2 1
// 查询8.3是否签到
GETBIT uid:sign:3000:202008 2
// 统计该用户20年8月份的签到次数
BITCOUNT uid:sign:3000:202008
  • 统计应用当中10天连续签到的用户数量
    • Bitmap支持使用BITOP命令来对多个Bitmap按位做与,或,异或,的操作,操作结果会保存到一个新的bitmap当中
    • 在统计 1 亿个用户连续 10 天的签到情况时,你可以把每天的日期作为 key,每个 key 对应一个 1 亿位的 Bitmap,每一个 bit 对应一个用户当天的签到情况。接下来,我们对 10 个 Bitmap 做“与”操作,得到的结果也是一个 Bitmap。在这个 Bitmap 中,只有 10 天都签到的用户对应的 bit 位上的值才会是 1。最后,我们可以用 BITCOUNT 统计下 Bitmap 中的 1 的个数,这就是连续签到 10 天的用户总数了。

3.4 基数统计

  • 统计一个集合中不重复的元素个数 — 统计网页的UV
    • 需要去重 — 一个用户一天内的多次访问只能算一次
    • 使用set 或者hash的话都会需要将不同的id记录下来,最后看整个数据结构内元素的数量,会很大程度上占用内存
  • HyperLogLog
    • 用于统计基数的数据集合
      • 当集合元素数量非常多的时候,计算基数所需的空间是固定的,而且比较小
      • 有一点需要你注意一下,HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的,标准误算率是 0.81%。这也就意味着,你使用 HyperLogLog 统计的 UV 是 100 万,但实际的 UV 可能是 101 万。虽然误差率不算大,但是,如果你需要精确统计结果的话,最好还是继续用 Set 或 Hash 类型。

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 stone2paul@gmail.com

文章标题:Redis 数据结构使用

文章字数:2k

本文作者:Leilei Chen

发布时间:2021-02-09, 12:30:04

最后更新:2021-03-02, 12:46:14

原始链接:https://www.llchen60.com/Redis-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BD%BF%E7%94%A8/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏