Redis面试——数据结构

一、SDS如何防止缓冲区溢出?

Redis 的 String 类型通过 SDS(Simple Dynamic String)来防止缓冲区溢出,具体机制如下:

  1. Redis 的 String 类型底层采用 SDS 实现,即 Simple Dynamic String
  2. SDS 底层维护的数据结构包含多个关键部分:
    1. len:表示当前 SDS 字符串已经使用的字节数
    2. alloc:代表为 SDS 字符串预先分配的总字节数,alloc - len 即为空闲空间的大小
    3. flags:用于标识 SDS 的类型,不同类型的 SDS 在内存分配和存储方式上有所不同,以适应不同长度的字符串存储需求
    4. buf:实际存储字符串内容的字符数组,其长度为 alloc,并且遵循 C 字符串以 \0 结尾的惯例,不过 \0 不包含在 len 统计范围内
  3. 当对 SDS 字符串执行 append 等操作时,会先进行检查。计算添加新内容后所需的总字节数,如果该总字节数超过了当前 alloc 的字节数,就会触发扩容操作。扩容规则如下
    1. 如果当前 alloc 的字节数小于 1MB,采用两倍扩容策略,即将原来 alloc 的字节数乘以 2
    2. 如果当前 alloc 的字节数大于或等于 1MB,每次扩容 1MB,也就是在原来 alloc 的字节数基础上加上 1MB

二、跳表插入流程

(1)跳表的基本结构

跳表是在有序链表的基础上,通过增加多层索引来提高查找效率。每一层都是一个有序链表,并且上层链表是下层链表的子集。每个节点可能会跨越多个层级,节点的层级是在插入时随机确定的

(2)插入步骤演示

每次插入一个元素之前,采用抛硬币的方式记录层数,最开始的层数从 0 开始,每次抛硬币,如果正面向上,就加 1,如果反面向上,就不加,而且会立刻停止抛硬币。然后最终得到的层数,就是这个元素开始插入的层。从该层起,在每层找到最后一个比这个元素小的元素,把这个元素插入到该元素之后,接着依次向下,直到第 0 层都完成插入操作

三、渐进式Rehash的过程

(1)初始状态

  1. 电商网站使用 Redis 存储商品信息,每个商品信息以键值对形式存在,键为商品 ID,值为商品的详细信息(如商品名称、价格等)
  2. 初始时,Redis 中的哈希表 ht[0] 大小为 8,当前存储了 8 个商品信息,负载因子为 1(已使用桶数量 / 哈希桶总数 = 8 / 8 = 1),ht[1] 为空
    ht[0]:
    | 0 | -> (1001, {name: "手机", price: 3999})
    | 1 | -> (1002, {name: "电脑", price: 7999})
    | 2 | -> (1003, {name: "相机", price: 5999})
    | 3 | -> (1004, {name: "耳机", price: 299})
    | 4 | -> (1005, {name: "手表", price: 1999})
    | 5 | -> (1006, {name: "键盘", price: 499})
    | 6 | -> (1007, {name: "鼠标", price: 199})
    | 7 | -> (1008, {name: "音箱", price: 399})ht[1]:
    | 0 | -> NULL
    | 1 | -> NULL
    | 2 | -> NULL
    | 3 | -> NULL
    | 4 | -> NULL
    | 5 | -> NULL
    | 6 | -> NULL
    | 7 | -> NULLrehashidx = -1

(2)触发Rehash

  1. 当新添加商品 1009(商品 ID 为 1009,名称为 “路由器”,价格为 199)时,ht[0] 的负载因子变为 9 / 8 > 1,触发扩容操作
  2. Redis 为 ht[1] 分配新的内存空间,大小为 ht[0] 的 2 倍,即 16。同时,将 rehashidx 设置为 0,表示从 ht[0] 的第 0 个哈希桶开始进行 Rehash。由于处于 Rehash 过程中,新添加的商品 1009 会直接被添加到 ht[1] 中。假设商品 1009 计算得到的哈希值对 16 取模结果为 11,则:
    ht[0]:
    | 0 | -> (1001, {name: "手机", price: 3999})
    | 1 | -> (1002, {name: "电脑", price: 7999})
    | 2 | -> (1003, {name: "相机", price: 5999})
    | 3 | -> (1004, {name: "耳机", price: 299})
    | 4 | -> (1005, {name: "手表", price: 1999})
    | 5 | -> (1006, {name: "键盘", price: 499})
    | 6 | -> (1007, {name: "鼠标", price: 199})
    | 7 | -> (1008, {name: "音箱", price: 399})ht[1]:
    | 0  | -> NULL
    | 1  | -> NULL
    | 2  | -> NULL
    | 3  | -> NULL
    | 4  | -> NULL
    | 5  | -> NULL
    | 6  | -> NULL
    | 7  | -> NULL
    | 8  | -> NULL
    | 9  | -> NULL
    | 10 | -> NULL
    | 11 | -> (1009, {name: "路由器", price: 199})
    | 12 | -> NULL
    | 13 | -> NULL
    | 14 | -> NULL
    | 15 | -> NULLrehashidx = 0

(3)渐进式迁移

3.3.1第一次 Rehash

  1. 当有用户查询商品 ID 为 1010 的商品信息时,由于处于 Rehash 过程中,会先将商品 1010 插入到 ht[1] 中。假设商品 1010 计算得到的哈希值对 16 取模结果为 4,插入后如下:
    ht[0]:
    | 0 | -> (1001, {name: "手机", price: 3999})
    | 1 | -> (1002, {name: "电脑", price: 7999})
    | 2 | -> (1003, {name: "相机", price: 5999})
    | 3 | -> (1004, {name: "耳机", price: 299})
    | 4 | -> (1005, {name: "手表", price: 1999})
    | 5 | -> (1006, {name: "键盘", price: 499})
    | 6 | -> (1007, {name: "鼠标", price: 199})
    | 7 | -> (1008, {name: "音箱", price: 399})ht[1]:
    | 0  | -> NULL
    | 1  | -> NULL
    | 2  | -> NULL
    | 3  | -> NULL
    | 4  | -> (1010, {name: "新商品", price: 888})
    | 5  | -> NULL
    | 6  | -> NULL
    | 7  | -> NULL
    | 8  | -> NULL
    | 9  | -> NULL
    | 10 | -> NULL
    | 11 | -> (1009, {name: "路由器", price: 199})
    | 12 | -> NULL
    | 13 | -> NULL
    | 14 | -> NULL
    | 15 | -> NULLrehashidx = 0
  2. 接着,系统会将 ht[0] 中索引为 0 的桶中的商品键值对 (1001, {name: "手机", price: 3999}) 重新计算哈希值。假设新的哈希值对 16 取模的结果为 3,则将该键值对插入到 ht[1] 的第 3 个哈希桶中,然后将 rehashidx 的值增 1,变为 1
    ht[0]:
    | 0 | -> NULL
    | 1 | -> (1002, {name: "电脑", price: 7999})
    | 2 | -> (1003, {name: "相机", price: 5999})
    | 3 | -> (1004, {name: "耳机", price: 299})
    | 4 | -> (1005, {name: "手表", price: 1999})
    | 5 | -> (1006, {name: "键盘", price: 499})
    | 6 | -> (1007, {name: "鼠标", price: 199})
    | 7 | -> (1008, {name: "音箱", price: 399})ht[1]:
    | 0  | -> NULL
    | 1  | -> NULL
    | 2  | -> NULL
    | 3  | -> (1001, {name: "手机", price: 3999})
    | 4  | -> (1010, {name: "新商品", price: 888})
    | 5  | -> NULL
    | 6  | -> NULL
    | 7  | -> NULL
    | 8  | -> NULL
    | 9  | -> NULL
    | 10 | -> NULL
    | 11 | -> (1009, {name: "路由器", price: 199})
    | 12 | -> NULL
    | 13 | -> NULL
    | 14 | -> NULL
    | 15 | -> NULLrehashidx = 1

3.3.2后续 Rehash

后续每次对商品信息执行操作(如查询、更新、删除等)时,都会顺带将 ht[0] 中 rehashidx 位置的键值对迁移到 ht[1] 中,并将 rehashidx 加 1。例如,当有用户更新商品 1002 的信息时,会把 ht[0] 中索引为 1 的 (1002, {name: "电脑", price: 7999}) 迁移到 ht[1] 中,假设新位置为 7:

ht[0]:
| 0 | -> NULL
| 1 | -> NULL
| 2 | -> (1003, {name: "相机", price: 5999})
| 3 | -> (1004, {name: "耳机", price: 299})
| 4 | -> (1005, {name: "手表", price: 1999})
| 5 | -> (1006, {name: "键盘", price: 499})
| 6 | -> (1007, {name: "鼠标", price: 199})
| 7 | -> (1008, {name: "音箱", price: 399})ht[1]:
| 0  | -> NULL
| 1  | -> NULL
| 2  | -> NULL
| 3  | -> (1001, {name: "手机", price: 3999})
| 4  | -> (1010, {name: "新商品", price: 888})
| 5  | -> NULL
| 6  | -> NULL
| 7  | -> (1002, {name: "电脑", price: 7999})
| 8  | -> NULL
| 9  | -> NULL
| 10 | -> NULL
| 11 | -> (1009, {name: "路由器", price: 199})
| 12 | -> NULL
| 13 | -> NULL
| 14 | -> NULL
| 15 | -> NULLrehashidx = 2

(4)完成 Rehash

当 rehashidx 变为 8 时,意味着 ht[0] 中的所有键值对都已迁移到 ht[1] 中。此时程序将 rehashidx 属性的值设为 -1,表示 Rehash 操作已完成。之后,电商网站对商品信息的操作就只在新的哈希表 ht[1] 上进行了

ht[0]:
| 0 | -> NULL
| 1 | -> NULL
| 2 | -> NULL
| 3 | -> NULL
| 4 | -> NULL
| 5 | -> NULL
| 6 | -> NULL
| 7 | -> NULLht[1]:
| 0  | -> (1008, {name: "音箱", price: 399})
| 1  | -> (1007, {name: "鼠标", price: 199})
| 2  | -> (1006, {name: "键盘", price: 499})
| 3  | -> (1001, {name: "手机", price: 3999})
| 4  | -> (1010, {name: "新商品", price: 888})
| 5  | -> (1005, {name: "手表", price: 1999})
| 6  | -> (1004, {name: "耳机", price: 299})
| 7  | -> (1002, {name: "电脑", price: 7999})
| 8  | -> (1003, {name: "相机", price: 5999})
| 9  | -> NULL
| 10 | -> NULL
| 11 | -> (1009, {name: "路由器", price: 199})
| 12 | -> NULL
| 13 | -> NULL
| 14 | -> NULL
| 15 | -> NULLrehashidx = -1

四、压缩列表

(1)压缩列表的基本结构

Redis 的压缩列表(ziplist)是一种为了节省内存而设计的顺序型数据结构,它被广泛应用于列表键和哈希键中,当列表元素较少或者列表元素都是小整数值或短字符串时,以及哈希键中键值对数量较少且键值都是小整数值或短字符串时,Redis 会使用压缩列表来存储数据

(2)压缩列表的结构

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,其整体结构包含以下几个部分:

  1. zlbytes
    1. 类型:4 字节无符号整数
    2. 作用:记录整个压缩列表所占用的内存字节数,通过这个字段,Redis 可以快速定位到压缩列表的末尾
  2. zltail
    1. 类型:4 字节无符号整数
    2. 作用:记录压缩列表尾节点相对于压缩列表起始地址的偏移量,借助这个字段,Redis 能够在不遍历整个列表的情况下直接访问尾节点
  3. zllen
    1. 类型:2 字节无符号整数
    2. 作用:记录压缩列表中节点的数量。不过,当节点数量超过 65535 时,这个字段的值会固定为 65535,此时需要遍历整个压缩列表才能获取准确的节点数量
  4. entryX
    1. 类型:可变长度
    2. 作用:压缩列表中的节点,每个节点可以存储一个字节数组或者一个整数值
  5. zlend
    1. 类型:1 字节特殊值
    2. 取值:固定为 0xFF(十进制的 255)
    3. 作用:标记压缩列表的结束

(3)节点的结构

每个压缩列表节点由以下三个部分组成:

  1. prevlen
    1. 类型:长度可变,1 字节或者 5 字节
    2. 取值及作用:
      1. 如果前一个节点的长度小于 254 字节,那么 prevlen 用 1 字节来存储该长度
      2. 如果前一个节点的长度大于等于 254 字节,prevlen 则用 5 字节来存储,其中第一个字节固定为 0xFE(十进制的 254),后面 4 个字节存储实际的长度值
      3. 通过这个字段,Redis 可以从后向前遍历压缩列表
  2. encoding
    1. 类型:长度可变
    2. 作用:记录节点所保存的数据的类型以及长度。不同的编码方式会使用不同的字节数来表示数据类型和长度
  3. data
    1. ​​​​​​​类型:长度可变
    2. 作用:节点实际保存的数据

(4)示例说明

假设我们有一个简单的压缩列表,用于存储一个包含三个元素的列表:["apple", 100, "banana"]。下面是这个压缩列表的结构示例:

+--------+
| zlbytes|  29(4 字节无符号整数,记录整个压缩列表所占用的内存字节数)
+--------+
| zltail |  20(4 字节无符号整数,记录压缩列表尾节点相对于压缩列表起始地址的偏移量)
+--------+
| zllen  |   3(2 字节无符号整数,记录压缩列表中节点的数量)
+--------+
| entry1 |
|        | prevlen |  0(1 字节,表示前一个节点长度,因为是第一个节点,所以为 0)
|        | encoding|  0b10000101(1 字节,高 2 位 10 表示字符串,低 6 位 000101 表示字符串 "apple" 的长度 5)
|        | data    |  "apple"(5 字节,实际存储的字符串内容)
+--------+
| entry2 |
|        | prevlen |  7(1 字节,前一个节点 "apple" 长度为 5 字节,加上 prevlen 和 encoding 各 1 字节,共 7 字节)
|        | encoding|  0b00000011(1 字节,表示存储的是 1 字节整数)
|        | data    |  100(1 字节,实际存储的整数 100)
+--------+
| entry3 |
|        | prevlen |  3(1 字节,前一个节点总长度为 3 字节,即 prevlen 1 字节 + encoding 1 字节 + data 1 字节)
|        | encoding|  0b10000110(1 字节,高 2 位 10 表示字符串,低 6 位 000110 表示字符串 "banana" 的长度 6)
|        | data    |  "banana"(6 字节,实际存储的字符串内容)
+--------+
| zlend  |  0xFF(1 字节,固定值,标记压缩列表结束)
+--------+

五、Set数据结构

(1)存储小数据时采用的结构:整数集合(intset)

  1. 适用场景:当集合中的元素全部为整数并且元素数量不超过 set-max-intset-entries(默认值为 512)时,Redis 会采用整数集合来存储集合元素
  2. 结构特点
    1. ​​​​​​​内存紧凑:整数集合是一块连续的内存区域,其存储效率较高,能有效节省内存空间
    2. 有序存储:集合中的元素按照从小到大的顺序排列,这样可以利用二分查找来快速定位元素,时间复杂度为 O(logn)
    3. 自动升级:整数集合支持不同的编码方式,如 INTSET_ENC_INT16、INTSET_ENC_INT32 和 INTSET_ENC_INT64。当插入的新元素无法用当前编码表示时,整数集合会自动升级编码方式,以适应新元素的存储需求
  3. 示例说明:假设集合中存储的元素为 {1, 2, 3, 4, 5},且元素数量未超过 set-max-intset-entries,Redis 会使用整数集合来存储这些元素。存储结构如下
    +------------+-----------------+
    | encoding   | 表示当前的编码方式,如 INTSET_ENC_INT16
    +------------+-----------------+
    | length     | 集合中元素的数量,这里为 5
    +------------+-----------------+
    | contents   | 依次存储元素 1, 2, 3, 4, 5
    +------------+-----------------+

(2)存储大数据时采用的结构:哈希表(hashtable)

  1. 适用场景:当集合中的元素不全部为整数或者元素数量超过 set-max-intset-entries 时,Redis 会使用哈希表来存储集合元素
  2. 结构特点
    1. ​​​​​​​高效查找:哈希表的查找、插入和删除操作的平均时间复杂度为 O(1),这使得 Redis 在处理大量元素时能保持高效的性能
    2. 键值对存储:哈希表以键值对的形式存储元素,其中键为集合中的元素,值统一为 NULL。这样可以利用哈希表的特性来确保元素的唯一性
    3. 渐进式 rehash:当哈希表的负载因子过高时,Redis 会进行 rehash 操作,将元素从旧的哈希表迁移到新的哈希表中。为了避免一次性迁移大量元素导致的性能问题,Redis 采用了渐进式 rehash 的方式,在每次操作时逐步迁移一部分元素
  3. 示例说明:假设集合中存储的元素为 {"apple", "banana", "cherry", "date"},由于元素为字符串,Redis 会使用哈希表来存储这些元素。存储结构如下
    +-----------------+-----------------+
    | 哈希表数组      | 存储多个哈希桶
    +-----------------+-----------------+
    | 哈希桶 1        | 链表,存储键值对 ("apple", NULL)
    +-----------------+-----------------+
    | 哈希桶 2        | 链表,存储键值对 ("banana", NULL)
    +-----------------+-----------------+
    | 哈希桶 3        | 链表,存储键值对 ("cherry", NULL)
    +-----------------+-----------------+
    | 哈希桶 4        | 链表,存储键值对 ("date", NULL)
    +-----------------+-----------------+

(3)结构转换

当集合从满足使用整数集合的条件转变为不满足时,Redis 会自动将整数集合转换为哈希表。例如,当向一个原本使用整数集合存储的集合中插入一个非整数元素,或者元素数量超过 set-max-intset-entries 时,Redis 会触发转换操作

六、Zset数据结构

(1)存储小数据时采用的结构:压缩列表(ziplist)

  1. 适用场景:当有序集合同时满足元素数量少于 zset-max-ziplist-entries(默认值为 128),且每个元素的成员长度和分数长度都小于 zset-max-ziplist-value(默认值为 64 字节)时,Redis 使用压缩列表存储 Zset 数据
  2. 结构特点
    1. 内存紧凑,是连续的内存块,通过特殊编码存储数据节省内存
    2. 元素按分数从小到大顺序存储,每个元素的成员和分数依次存于压缩列表,一个元素由两个连续节点表示
  3. 示例说明:假设有序集合存储 {"apple": 1.0, "banana": 2.0, "cherry": 3.0} 且满足使用压缩列表条件,其存储结构如下
    +--------+
    | zlbytes|  记录整个压缩列表占用字节数
    +--------+
    | zltail |  尾节点相对于起始地址的偏移量
    +--------+
    | zllen  |  压缩列表中节点数量
    +--------+
    | entry1 |
    |        | prevlen |  前一节点长度,首节点为 0
    |        | encoding|  编码表示 "apple" 为字符串及长度
    |        | data    |  "apple"
    +--------+
    | entry2 |
    |        | prevlen |  entry1 的长度
    |        | encoding|  编码表示 1.0 分数格式
    |        | data    |  1.0
    +--------+
    | entry3 |
    |        | prevlen |  entry2 的长度
    |        | encoding|  编码表示 "banana" 为字符串及长度
    |        | data    |  "banana"
    +--------+
    | entry4 |
    |        | prevlen |  entry3 的长度
    |        | encoding|  编码表示 2.0 分数格式
    |        | data    |  2.0
    +--------+
    | entry5 |
    |        | prevlen |  entry4 的长度
    |        | encoding|  编码表示 "cherry" 为字符串及长度
    |        | data    |  "cherry"
    +--------+
    | entry6 |
    |        | prevlen |  entry5 的长度
    |        | encoding|  编码表示 3.0 分数格式
    |        | data    |  3.0
    +--------+
    | zlend  |  固定值 0xFF 标记结束
    +--------+

(2)存储大数据时采用的结构:跳跃表(skiplist)与哈希表(hashtable)结合

  1. 适用场景当有序集合不满足使用压缩列表的条件时,采用跳跃表和哈希表的组合结构存储
  2. 结构特点
    1. 跳跃表(skiplist)
      1. 能高效排序与查找,通过节点维护多个指向其他节点的指针,平均时间复杂度 O(logn) 完成插入、删除和查找
      2. 节点高度随机生成,可在不同数据分布下保持较好性能
    2. 哈希表(hashtable)
      1. 能快速查找元素,以元素成员为键、分数为值,平均时间复杂度 O(1) 完成查找
    3. 两者共享元素的成员和分数信息,操作时同时更新保证数据一致
  3. 示例说明
    1. ​​​​​​​​​​​​​​跳跃表
                +--------+--------+--------+--------+--------+
      Level 2   |  Head  | "cat"  | "elephant" |        |  Tail  |+--------+--------+--------+--------+--------+|        |        |        |        |        ||        |        |        |        |        |
      Level 1   |  Head  | "cat"  | "dog"    | "elephant" | "fox" |  Tail  |+--------+--------+--------+--------+--------+--------+|        |        |        |        |        |        ||        |        |        |        |        |        |
      Level 0   |  Head  | "cat"  | "dog"    | "elephant" | "fox"  |  Tail  |+--------+--------+--------+--------+--------+--------+节点详情:
      - Head 节点:- 各层前进指针分别指向对应层的下一个节点
      - "cat" 节点:- 成员: "cat"- 分数: 2.5- 后退指针: 无- 层数: 2- 第 0 层前进指针: 指向 "dog" 节点- 第 1 层前进指针: 指向 "dog" 节点- 第 2 层前进指针: 指向 "elephant" 节点
      - "dog" 节点:- 成员: "dog"- 分数: 3.5- 后退指针: 指向 "cat" 节点- 层数: 1- 第 0 层前进指针: 指向 "elephant" 节点- 第 1 层前进指针: 指向 "elephant" 节点
      - "elephant" 节点:- 成员: "elephant"- 分数: 4.5- 后退指针: 指向 "dog" 节点- 层数: 2- 第 0 层前进指针: 指向 "fox" 节点- 第 1 层前进指针: 指向 "fox" 节点- 第 2 层前进指针: 指向 Tail 节点
      - "fox" 节点:- 成员: "fox"- 分数: 5.5- 后退指针: 指向 "elephant" 节点- 层数: 1- 第 0 层前进指针: 指向 Tail 节点- 第 1 层前进指针: 指向 Tail 节点
      - Tail 节点:- 无成员和分数
    2. 哈希表
      +-----------------+
      | 哈希表数组      |
      | 大小: 4         |
      +-----------------+
      | 哈希桶 0        |
      | 链表: ("cat", 2.5)
      +-----------------+
      | 哈希桶 1        |
      | 链表: ("dog", 3.5)
      +-----------------+
      | 哈希桶 2        |
      | 链表: ("elephant", 4.5)
      +-----------------+
      | 哈希桶 3        |
      | 链表: ("fox", 5.5)
      +-----------------+
  4. 查找过程说明

(3)结构转换

当有序集合元素数量或元素长度超出压缩列表使用条件,Redis 自动将压缩列表转换为跳跃表和哈希表的组合结构,且此转换不可逆

七、总结

Redis 一共有五种基本数据结构:

  1. String:键对应的值为 String 类型,其底层实现是简单动态字符串(SDS)。SDS 不仅能存储普通字符串、数字,还能存储二进制数据。存储的数据存放在 SDS 的 buf 数组部分,同时 SDS 通过额外的元数据(如长度信息等)来优化字符串操作性能,相比传统 C 字符串,SDS 在追加、修改等操作时能更高效地管理内存,避免缓冲区溢出等问题
  2. List:列表类型,底层实现有压缩列表双向链表两种。当数据量较少且每个元素的长度较短时,Redis 会选择压缩列表来存储。每次向列表中添加一个数据,该数据就会成为压缩列表中的一个节点。压缩列表是紧凑的连续内存结构,适合从头部或尾部遍历,并且可以通过 zllen 字段快速获取元素数量。当数据量较多时,会采用双向链表存储。双向链表在插入和删除操作上具有优势,无需移动大量元素,时间复杂度为 O(1),但在内存占用和随机访问性能上不如压缩列表
  3. Hash:底层基于哈希表结构。当插入一个键值对时,先对键进行哈希计算,然后对哈希表的大小取模,得到对应的哈希桶索引,将键值对存储到该哈希桶中。哈希桶中可能存在多个键值对(通过链表解决哈希冲突)。随着数据量的增加,哈希表可能会出现负载因子过高的情况,此时会触发 rehash 操作,Redis 采用渐进式 rehash 来避免一次性大量数据迁移带来的性能问题,即在每次对哈希表进行操作时,顺带迁移一部分数据到新的哈希表中
  4. Set:对于小数据量且元素全为整数的情况,使用整数集合存储。整数集合是紧凑的有序结构,能有效节省内存。当数据量较大或元素类型不全为整数时,采用哈希表存储。向 Set 中添加数据时,如果使用哈希表存储,数据会以键值对形式存储在哈希桶中,其中键为数据本身,值统一为 NULL,利用哈希表的特性来保证元素的唯一性
  5. Zset:小数据量时采用压缩列表存储。压缩列表中,每个元素的成员和分数依次存储,元素按分数从小到大排列。当数据量较大时,采用跳跃表和哈希表结合的方式存储。跳跃表基于节点的多层指针结构,在范围查询(如按分数范围查找成员)上具有 O(logn) 的时间复杂度优势。哈希表则用于通过成员快速查找对应的分数,因为哈希表以成员为键、分数为值,查找时间复杂度平均为 O(1),二者结合能高效地满足 Zset 各种操作需求

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/76136.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Doris的向量化执行如何支撑分布式架构和复杂查询

Doris 的向量化执行能力与其 分布式架构 和 复杂查询优化 深度结合,通过 批处理 列式计算 分布式调度 的协同设计,解决传统分布式数据库在复杂查询场景下的性能瓶颈。以下是具体原理展开: 一、向量化如何适配分布式架构? Doris…

DataInputStream 终极解析与记忆指南

DataInputStream 终极解析与记忆指南 一、核心本质 DataInputStream 是 Java 提供的数据字节输入流,继承自 FilterInputStream,用于读取基本数据类型和字符串的二进制数据。 作用:1.专门用来读取使用DataOutputStream流写入的文件 注意:读取的顺序要和写入的顺序一致(…

云转型(cloud transformation)——不仅仅是简单的基础设施迁移

李升伟 编译 云转型不仅仅是迁移基础设施,更是重塑企业运营、创新及价值交付的方式。它具有战略性、持续性,并影响着人员、流程和平台。 ☁️ 云转型涉及以下内容: 🔄 应用现代化——从单体架构转向微服务架构。 ⚙️ 运营自动…

Java HTTP Client API详解

Java HTTP Client API详解 Java的HTTP客户端API经历了多次演进,从早期的HttpURLConnection到第三方库如Apache HttpClient,再到Java 11引入的标准HttpClient。本文将全面解析Java中主要的HTTP客户端API,包括特性对比、使用方法和最佳实践。 …

如何深入理解引用监视器,安全标识以及访问控制模型与资产安全之间的关系

一、核心概念总结 安全标识(策略决策的 “信息载体) 是主体(如用户、进程)和客体(如文件、数据库、设备)的安全属性,用于标记其安全等级、权限、访问能力或受保护级别,即用于标识其安全等级、权限范围或约束…

京东3D空间视频生成技术探索与应用

1. 背景 近年来,随着社交媒体、流媒体平台以及XR设备的快速发展,沉浸式3D空间视频的需求迅猛增长,尤其是在短视频、直播和电影领域,正在重新定义观众的观看体验。2023年,苹果公司发布的空间视频技术为这一趋势注入了新…

惊爆!Cursor 限制多设备登录,网友疯狂吐槽,退订潮汹涌来袭,直呼:没理由再给它掏钱!

大家好,我是小程程。 吃瓜吃瓜,知名 AI 编程工具 Cursor 惹事了! ① 遭遇强制登出 前几天有 Cursor 用户发现,自己要是从多台设备登录,就会被强制下线。 比方说,你正在台式电脑上干活,中途换到笔…

React JSX 语法深度解析与最佳实践

本文系统梳理 JSX 语法的完整知识体系。通过原理剖析、代码示例和开发警示&#xff0c;帮助开发者建立严谨的 JSX 使用认知。 一、JSX 本质解析 1.1 编译机制 JSX 通过 Babel 转换为 React.createElement 调用&#xff0c;以下为转换对照&#xff1a; // 原始 JSX <MyCo…

若依改用EasyCaptcha验证码

若依自带的验证码样式比较单一&#xff0c;所以想改用EasyCaptcha验证码&#xff0c;另外EasyCaptcha算术验证码可能会有负数&#xff0c;输入时需要写负号&#xff0c;比较麻烦&#xff0c;所以使用一个简单的方法过滤掉负数结果 原本的验证码依赖和代码可删可不删&#xff0c…

趣味编程之go与rust的爱恨情仇

声明:此篇文章利用deepseek生成。 第一章&#xff1a;出身之谜 Go&#xff08;江湖人称"高小戈"&#xff09;是名门之后——谷歌家的三少爷。生来就带着"简单粗暴"的家族基因&#xff0c;口号是**“少写代码多搬砖&#xff0c;并发处理赛神仙”**。它爹Ro…

【cocos creator 3.x】速通3d模型导入, 模型创建,阴影,材质使用,模型贴图绑定

1、右键创建平面&#xff0c;立方体 2、点击场景根节点&#xff0c;shadows勾选enabled3、点击灯光&#xff0c;shadow enabled勾选 4、点击模型&#xff0c;勾选接收阴影&#xff0c;投射阴影&#xff08;按照需要勾选&#xff09; 5、材质创建 6、选中节点&#xff0c;找…

告别昂贵语音合成服务!用GPT-SoVITS生成你的个性化AI语音

文章目录 前言1.GPT-SoVITS V2下载2.本地运行GPT-SoVITS V23.简单使用演示4.安装内网穿透工具4.1 创建远程连接公网地址 5. 固定远程访问公网地址 前言 今天给大家介绍一款AI语音克隆工具——GPT-SoVITS。这款由花儿不哭大佬开发的工具是一款强大的训练声音模型与音频生成工具…

Doris FE 常见问题与处理指南

在数据仓库领域&#xff0c;Apache Doris 凭借其卓越性能与便捷性被广泛应用。其中&#xff0c;FE&#xff08;Frontend&#xff09;作为核心组件&#xff0c;承担着接收查询请求、管理元数据等关键任务。然而&#xff0c;在实际使用中&#xff0c;FE 难免会遭遇各类问题&#…

Unity编辑器扩展之项目资源查找工具

一、需要实现的效果如下: 二、在项目的Asset目录下新增Editor目录,新增AssetSearchWindow和EditorDefine和EditorTools这三个C#脚本,并复制以下的代码保存好之后,就可以实现上述功能啦。 -------------------------------------------EditorTools脚本Begin----------------…

《Java 泛型的作用与常见用法详解》

大家好呀&#xff01;&#x1f44b; 今天我们要聊的是Java中一个超级重要但又让很多初学者头疼的概念——泛型(Generics)。带你彻底搞懂它&#xff01;&#x1f4aa; 准备好你的小本本&#xff0c;我们开始啦&#xff5e;&#x1f4dd; 一、为什么需要泛型&#xff1f;&#x…

USB(TYPE-C)转串口(TTL)模块设计讲解

目录 一 、引言 二、方案设计 三、USB TYPE-C介绍 1、TYPE-C接口定义 1、24P全引脚描述 2、Type C 接口 VBUS/GND 作用 3、Type C 接口 D/D- 作用 1、数据传输&#xff1a; 2、设备识别&#xff1a; 3、充电协议协商&#xff1a; 4、Type C 接口 CC1/CC2 作用 1、主从设备区…

v-model进阶+ref+nextTick

一、v-model进阶 复习 v-model v-model: 双向数据绑定指令 数据 <-> 视图: 数据和视图相互影响, 因此被称为双向数据绑定指令 1> 数据变了, 视图也会跟着变 (数据驱动视图) 2> 视图变了, 数据也会跟着变 1. v-model 原理 v-model只是一个语法糖, 比较好用, …

Sentinel源码—4.FlowSlot实现流控的原理二

大纲 1.FlowSlot根据流控规则对请求进行限流 2.FlowSlot实现流控规则的快速失败效果的原理 3.FlowSlot实现流控规则中排队等待效果的原理 4.FlowSlot实现流控规则中Warm Up效果的原理 3.FlowSlot实现流控规则中排队等待效果的原理 (1)实现排队等待流控效果的普通漏桶算法介…

2025华中杯数学建模B题完整分析论文(共42页)(含模型、数据、可运行代码)

2025华中杯大学生数学建模B题完整分析论文 目录 一、问题重述 二、问题分析 三、模型假设 四、 模型建立与求解 4.1问题1 4.1.1问题1解析 4.1.2问题1模型建立 4.1.3问题1样例代码&#xff08;仅供参考&#xff09; 4.1.4问题1求解结果&#xff08;仅供参考&am…

Project ERROR: liblightdm-qt5-3 development package not found问题的解决方法

问题描述&#xff1a;使用make命令进行ukui-greeter-Debian构建时出现Project ERROR: liblightdm-qt5-3 development package not found错误&#xff0c;具体如图&#xff1a; 问题原因&#xff1a;缺乏liblightdm-qt5-3 development软件包 解决方法&#xff1a;安装liblightd…