Set 的编码方式
Redis 中的 Set 集合有两种底层编码方式:整数集合(intset) 和 字典(hashtable)。具体使用哪种编码取决于集合元素的数据类型和集合的大小。
-
整数集合(intset):
- 当集合的所有元素都是整数(例如
1, 2, 3
),并且集合中元素的个数不超过 512 个时,Redis 会使用整数集合(intset)作为底层实现。 - 整数集合是有序的:整数集合存储的整数是有序排列的,因此支持高效的查找和范围查询操作。
- 当集合的所有元素都是整数(例如
-
字典(hashtable):
- 当集合中有非整数类型的元素,或者元素个数超过 512 个时,Redis 会使用字典(hashtable)作为底层实现。
- 字典是无序的:字典的底层实现是哈希表,因此它不保证元素的顺序。
Set 是有序的吗?
Redis 中的 Set 集合既可以是有序的,也可以是无序的,具体取决于底层编码的类型。如果 Set 的底层编码是 整数集合(intset),那么 Set 是有序的;如果底层编码是 字典(hashtable),那么 Set 是无序的。因此,在实际业务中,不应该依赖 Set 的顺序,始终应当按照无序来使用 Set。
Set 为什么要用两种编码方式?
Redis 中的 Set 集合使用两种编码方式:整数集合(intset) 和 字典(hashtable)。采用这两种编码方式的主要原因是为了在不同的场景下优化内存使用和查询性能。
-
内存优化(intset):
-
内存效率高:当 Set 集合中的所有元素都是整数类型,并且元素的数量较少(不超过 512 个),Redis 使用整数集合(intset)作为底层实现。这种编码方式在内存的使用上更加节约,因为它使用一个连续的有序数组来存储整数,并且数组的编码格式会根据存储的整数范围自动升级(从
int16
到int32
,再到int64
),以适应更大的整数值。 -
适合小数据量的集合:整数集合(intset)通过这种可变长度的编码方式(编码格式和元素数量的灵活调整),能够在较小的数据量情况下节省大量内存。它在数据量不大时,查找操作的时间复杂度为 O(logn)O(\log n)O(logn),通过二分查找来快速定位元素。
-
-
查询性能(hashtable):
-
更高效的查找:当 Set 中的元素数量较多(超过 512 个)或包含非整数类型的元素时,Redis 则使用字典(hashtable)作为底层编码。这是因为哈希表提供了 O(1)O(1)O(1) 平均时间复杂度的查找性能,适合处理大量数据。
-
适合大数据量和复杂数据类型的集合:字典编码更适合处理较大的数据集合,或者当元素不仅仅是整数时,字典编码能提供更高效的查找速度和灵活性。
-
Hash 的编码方式是什么?
Redis 中的 Hash(哈希)有两种底层编码方式:ZIPLIST(压缩列表) 和 HASHTABLE(哈希表)。具体使用哪种编码方式,取决于 Hash 中元素的数量和每个元素的大小。
-
ZIPLIST(压缩列表):
- 适用场景:元素个数少且每个元素的大小较小的情况。
- 条件:当 Hash 中的元素个数少于 512 个,并且键和值的长度都小于 64 字节时,Redis 使用 ZIPLIST 作为底层数据结构。
- 优点:ZIPLIST 编码的主要优点是节省内存,适用于小型数据集。
-
HASHTABLE(哈希表):
- 适用场景:元素个数多或单个元素的大小较大的情况。
- 条件:当 Hash 中的元素个数超过 512 个,或者任意一个键或值的长度超过 64 字节时,Redis 则会使用 HASHTABLE 作为底层数据结构。
- 优点:HASHTABLE 编码的主要优点是查找速度快,即使在较大的数据集上也能保持高效的查找性能。
Hash 查找某个 key 的平均时间复杂度是多少?
在 Redis 中,Hash 的底层编码有两种,查找某个 key 的时间复杂度取决于底层编码方式。
-
ZIPLIST(压缩列表):
- 查找复杂度:O(N)
- 原因:ZIPLIST 是一个连续的内存结构,查找某个 key 时需要遍历每个元素,直到找到目标 key,平均时间复杂度为 O(N),其中 NNN 是元素的个数。
-
HASHTABLE(哈希表):
- 查找复杂度:O(1)
- 原因:HASHTABLE 使用哈希算法将键映射到数组的索引位置,通过哈希计算直接找到对应的键值对,平均时间复杂度为 O(1)。
Redis 中 HashTable 查找元素总数的平均时间复杂度是多少?
在 Redis 中,HashTable 的表头结构包含一个字段 used
,专门用于记录当前字典中键值对的数量。因此,查找 HashTable 中元素总数的操作可以在常数时间内完成。
由于 used
字段直接记录了键值对的数量,Redis 查找 HashTable 中元素总数的操作只需要读取这个字段的值,因此其平均时间复杂度是 O(1)。
一个数据在 HashTable 中的存储位置是怎么计算的?
-
计算哈希值:
- 对 key 使用 MurmurHash2 哈希算法计算出一个哈希值。这个哈希值是一个整数,理论上可以取任何值。
-
计算索引值:
- 通过哈希值与哈希表的 哈希掩码 做与运算(
hash & sizemask
)来计算出该键值对在哈希表中的存储位置(索引值)。 - 哈希掩码 (
sizemask
) 是哈希表数组的大小减去 1。例如,如果哈希表的大小是 16,那么sizemask
就是15
(即 16 - 1)。
- 通过哈希值与哈希表的 哈希掩码 做与运算(
-
确定存储位置:
- 计算出的索引值即为该数据在 HashTable 中的存储位置,程序会根据这个索引值将键值对插入到相应的位置。
HashTable 怎么扩容?
-
分配新的哈希表(1 号表):
- Redis 首先为 HashTable 分配一个新的哈希表(1 号表)。这个新表的大小是旧表(0 号表)大小的两倍,或者是第一个大于等于当前哈希表大小乘以 2 的 2 的幂。
-
设置 rehash 标记:
- 在 rehash 过程中,Redis 使用一个标记位
rehashidx
来表示 rehash 进度。rehashidx
初始值为 -1,表示未开始 rehash。
- 在 rehash 过程中,Redis 使用一个标记位
-
渐进式 rehash:
- 每次对字典进行增删改查操作时,Redis 会顺带将哈希表 0 号表中
rehashidx
位置的数据迁移到 1 号表,并将rehashidx
增加 1。 - 这种做法是为了将 rehash 操作分摊到多次增删改查操作中,避免一次性 rehash 造成的性能问题。
- 每次对字典进行增删改查操作时,Redis 会顺带将哈希表 0 号表中
-
完成 rehash:
- 当所有键值对都从 0 号表迁移到 1 号表后,Redis 将 1 号表设为新的 0 号表,并清空原 0 号表(现在已经不再需要)。
HashTable 怎么缩容?
-
分配新的哈希表(1 号表):
- 程序会为 HashTable 分配一个新的哈希表(1 号表)。新表的大小是第一个大于等于当前哈希表已使用节点数(
used
)的 2 次幂。
- 程序会为 HashTable 分配一个新的哈希表(1 号表)。新表的大小是第一个大于等于当前哈希表已使用节点数(
-
设置 rehash 标记:
- 缩容操作开始时,
rehashidx
标记位设为 0,表示开始 rehash 操作。
- 缩容操作开始时,
-
渐进式 rehash:
- 每次对字典进行增删改查操作时,Redis 会将 0 号表中
rehashidx
位置的数据迁移到 1 号表,然后将rehashidx
加 1。 - 这种逐步迁移操作将 rehash 的成本分散到多次字典操作中,避免了一次性操作带来的性能开销。
- 每次对字典进行增删改查操作时,Redis 会将 0 号表中
-
完成 rehash:
- 当所有键值对都从 0 号表迁移到 1 号表后,Redis 将 1 号表设为新的 0 号表,并清空原 0 号表(现在已经不再需要)。
什么时候扩容,什么时候缩容?
HashTable 的扩容和缩容是根据负载因子来进行的。
-
扩容: 当以下两个条件中的任意一个被满足时,哈希表会自动扩容:
- 服务器当前没有在执行 BGSAVE 或者 BGREWRITEAOF,并且负载因子 >= 1。
- 服务器当前正在执行 BGSAVE 或者 BGREWRITEAOF,并且负载因子 >= 5。
-
缩容: 当哈希表的负载因子 < 0.1 时,程序会自动开始对哈希表进行缩容操作。
通过这样动态调整哈希表的大小,Redis 能够在节省内存和保证查询性能之间取得平衡。
跳表插入一条数据的平均时间复杂度是多少?
跳表(Skiplist)是一种支持快速查找、插入和删除操作的数据结构。它通过在一维有序链表的基础上增加多层索引,从而实现类似于二分查找的高效查找性能。跳表的索引层数通常会在插入或删除节点时根据概率随机调整,这使得跳表可以在平均情况下保持较低的索引高度。
由于跳表每一层的节点数量是递减的,大多数情况下查找一个元素不需要遍历所有节点,因此在查找、插入和删除操作的平均时间复杂度都是 O(logn)O(\log n)O(logn),其中 nnn 是跳表中的节点数。
为什么跳表和 HashTable 要配合使用?
Redis 中的有序集合(ZSet)底层使用了两种数据结构:跳表(Skiplist)和 HashTable。之所以使用这两种数据结构,是为了让有序集合能够同时具备这两者的优势,以便在不同场景下能够快速、高效地完成操作。
- HashTable 的优势:能够在 O(1)O(1)O(1) 的时间复杂度内,根据成员名直接查找到对应的分值。适用于根据成员名直接查找分值的操作,比如
ZSCORE
、ZADD
、ZREM
等。 - 跳表 的优势:是一种有序的数据结构,能够在 O(logn)O(\log n)O(logn) 的时间复杂度内完成有序查找、范围查找和插入操作。适用于根据分值进行排序的操作,比如
ZRANK
、ZRANGE
、ZREVRANGE
等。
因此,使用跳表和 HashTable 结合的方式能够充分利用两者的优势,实现快速的分值查找和高效的范围操作。
跳表中一个节点的层高是怎么决定的?
跳表的核心思想是通过在有序链表的基础上增加多层索引来加速查找操作。每个节点的层高(Level)是随机生成的,且遵循幂次定律,即生成更高层数的概率更低。
具体来说,在跳表中插入一个节点时,为了决定该节点的层高,Redis 使用了一种随机化策略:最底层的节点(Level 1)一定存在,而每增加一层的概率是 25%(或 1/4),最高层数是 32 层。这意味着:
- 每个节点的初始层高至少是 1。
- 随机算法确保了大部分节点的层高较低,而只有少数节点具有较高的层高。
这种概率分布有助于在保持跳表结构简单的同时,实现高效的查找和插入操作。