美文网首页
20201110-Redis底层数据结构

20201110-Redis底层数据结构

作者: 爱钓鱼的码农 | 来源:发表于2020-11-10 15:37 被阅读0次

前言

Redis高性能及高吞吐的Key-Value类型存储,适合缓存、秒杀、分布式锁等场景。

Redis存储值支持类型

String、哈希表、列表、集合、有序集合。

数据类型与底层数据结构对应

底层数据结构.png
哈希表.png

Redis所有的数据通过全局哈希表保存所有的键值对,一个哈希表就是一个数组,数组的每个元素称为一个哈希桶。
每次可以通过O(1)的时间复杂度快速找到对应的键值对,我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。
哈希桶中的 entry 元素中保存了key和value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过value指针被查找到。
当往 Redis 中 写入大量数据后,就可能发现操作有时候会突然变慢了。
这其实是因为一个潜在的风险点,那就是
哈希表的冲突*和 **rehash **可能带来的操作阻塞。

哈希冲突.png

哈希冲突链解决哈希冲突

entry1、entry2 和 entry3如果key的哈希值为一致的,则都需要保存在哈希桶 3 中,从而导致了哈希冲突。
此时,entry1 元素会通过一个next指针指向entry2,同样,entry2 也会通过next指针 指向 entry3。
这样一来,即使哈希桶 3 中的元素有 100 个,我们也可以通过 entry 元素 中的指针,把它们连起来形成了一个链表,也叫作哈希冲突链。
注:哈希冲突链上的元素只能通过指针逐一查找再操作。如果 哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长。

rehash解决哈希冲突

rehash的本质是减少哈希的冲突,通过增大哈希桶的数量避免单个哈希桶过大。
为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表1、哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。
随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

  1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;
  3. 释放哈希表 1 的空间。
    到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来 的哈希表 1 留作下一次 rehash 扩容备用。
    但是第二步涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都 迁移完,会造成 Redis 线程阻塞,无法服务其他请求。此时,Redis 就无法快速访问数据了。

渐进式 rehash

渐进式rehash.png

简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝 到哈希表 2 中;
等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操
作,保证了数据的快速访问。

集合数据操作效率

String 类型不同,一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位 置,第二步是在集合中再增删改查。集合中数据的操作与底层数据结构有关,例如使用哈希表实现的集合,要比使用链表实现的集合访问效率更高。
集合类型数据结构:整数数组、双向链表、哈 希表、压缩列表和跳表。
哈希表通过计算哈希值获取哈希桶操作复杂度基本是 O(1);
整数数组、双向链表,它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低;

压缩表

压缩表.png

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同 的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的 偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段 的长度直接定位,复杂度是 O(1)。
而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

跳表

跳表.png

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说,跳 表在链表的基础上,增加了多级索引通过索引位置的几个跳转,实现数据的快速定位。
为了提高查找速度,我们来增加一级索引、从第一个元素开始,每两个元素选一个出来作 为索引。这些索引再通过指针指向原始的链表。
例如,从前两个元素中抽取元素 1 作为一 级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时我们只需要 4 次查找就 能定位到元素 33 了。
如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。
例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这 样,我们只需要 3 次查找,就能定位到元素 33 了。
可以看到,这个查找过程就是在多级索引上跳来跳去,最后定位到元素。
这也正好符 合“跳”表的叫法。当数据量很大时,跳表的查找复杂度就是 O(logN)。

不同操作的复杂度

单元素操作

每一种集合类型对单个数据实现的增删改查操作。例如,Hash 类 型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等。这些操 作的复杂度由集合采用的数据结构决定,例如,HGET、HSET 和 HDEL 是对哈希表做操 作,所以它们的复杂度都是 O(1);Set 类型用哈希表作为底层数据结构时,它的 SADD、 SREM、SRANDMEMBER 复杂度也是 O(1)。

范围操作

集合类型中的遍历操作,可以返回集合中的所有数据,比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS,或者返回一个范围内的部分数据,比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。
这类操作的复杂度一般是 O(N),比较耗时, 我们应该尽量避免。

统计操作

集合类型对集合中所有元素个数的记录,例如 LLEN 和 SCARD。这 类操作复杂度只有 O(1),这是因为当集合类型采用压缩列表、双向链表、整数数组这些数 据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作.

其余操作

某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操 作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂 度也只有 O(1),可以实现快速操作。

Redis类型与场景

Redis 之所以能快速操作键值对,一方面是因为 O(1) 复杂度的哈希表被广泛使用,包括 String、Hash 和 Set,它们的操作复杂度基本由哈希表决定,另一方面,Sorted Set 也采 用了 O(logN) 复杂度的跳表。
集合类型的范围操作,因为要遍历底层数据结构,复杂度通常是 O(N)。可以用其他命令来替代,例如可以用 SCAN 来代替, 避免在 Redis 内部产生费时的全集合遍历操作。
List 类型,它的两种底层实现结构:双向链表和压缩列表的操作复杂度都是 O(N)。
因此可以因地制宜地使用 List 类型。例如它的 POP/PUSH 效率很高,那么就将它主要用于 FIFO 队列场景,而不是作为一个可以随机读写的集合。
Redis 数据类型丰富,每个类型的操作繁多,我们通常无法一下子记住所有操作的复杂度。

相关文章

网友评论

      本文标题:20201110-Redis底层数据结构

      本文链接:https://www.haomeiwen.com/subject/krndbktx.html