Redis核心技术与实战 - 02

                           —— 了解Redis“快”的基本原理,理解 Redis 中有哪些潜在的“慢操作”

目录

一、Redis的 【key:value】 用什么结构组织?  

1、Redis通过链式哈希来解决Hash冲突

2、Redis中rehash的方式:

      渐进式 rehash

二、数据结构:是指Value中不同数据的保存形式的底层实现

1、整数数组和双向链表:

2、压缩列表:

3、跳表:

三、不同操作的复杂度

Redis接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。一方面,这是因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快。另一方面,这要归功于它的数据结构。

一、Redis的 【key:value】 用什么结构组织?

Redis核心技术与实战 - 02 【08:54前】

为了实现从键到值的快速访问,Redis 使用了一个 哈希表

一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。每个哈希桶中保存了键值对数据。

哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。这也就是说,不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

哈希桶中的 entry 元素中保存了*key和*value指针,分别指向了实际的键和值,这样一来,即使值是一个集合,也可以通过*value指针被查找到。

redis 底层存储结构 redis底层结构如何实现_Redis

哈希表的最大好处很明显,就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对——我们只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。查找过程主要依赖于哈希计算,和数据量的多少并没有直接关系。但是,在你往Redis中大量写入数据后,可能发现操作有时候会突然变慢了。这可能是哈希表的冲突问题rehash 可能带来的操作阻塞

1、Redis通过链式哈希来解决Hash冲突

哈希冲突链上的元素只能通过指针逐一查找再操作。如果哈希表里写入的数据越来越多,哈希冲突可能也会越来越多,这就会导致某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。

为了解决哈希冲突链过长而导致的效率降低的问题,Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。

redis 底层存储结构 redis底层结构如何实现_压缩表_02

2、Redis中rehash的方式:

为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。

随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:

  1. 给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;
  2. 把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;【该操作涉及大量的数据拷贝,如果一次性把哈希表 1 中的数据都迁移完,会造成 Redis 线程阻塞,无法服务其他请求,降低访问效率】
  3. 释放哈希表 1 的空间。

渐进式 rehash

Redis 采用了渐进式 rehash,解决步骤二造成的阻塞问题:

简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如下图所示:

redis 底层存储结构 redis底层结构如何实现_压缩表_03

 

二、数据结构:是指Value中不同数据的保存形式的底层实现

Value支持的数据类型 

Redis核心技术与实战 - 02 【08:54后】

redis 底层存储结构 redis底层结构如何实现_redis 底层存储结构_04

String 类型的底层实现只有一种数据结构,也就是简单动态字符串。 【什么是简单动态字符串,和常用的字符串的区别?

List、Hash、Set 和 Sorted Set 这四种数据类型,都有两种底层实现结构。通常情况下,我们会把这四种类型称为集合类型,它们的特点是一个键对应了一个集合的数据。

String 类型:找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是它的复杂度了。

集合类型:即使找到哈希桶了,还要在集合中再进一步操作。

集合数据操作效率:

一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中再增删改查。那么,集合的操作效率和哪些因素相关呢?

redis 底层存储结构 redis底层结构如何实现_redis 底层存储结构_05

1、整数数组和双向链表:

它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低;

问题:整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

  1. 内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。
  2. 数组对CPU高速缓存支持更友好

所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

2、压缩列表:

实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

redis 底层存储结构 redis底层结构如何实现_Redis_06

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了。

3、跳表:

有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。

具体来说,跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。

redis 底层存储结构 redis底层结构如何实现_数据结构_07

如果我们要在链表中查找 33 这个元素,只能从头开始遍历链表,查找 6 次,直到找到 33 为止。此时,复杂度是 O(N),查找效率很低。

为了提高查找速度,我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。例如,从前两个元素中抽取元素 1 作为一级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时,我们只需要 4 次查找就能定位到元素 33 了。

如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。

三、不同操作的复杂度

集合类型的操作类型很多,它们的复杂度也各不相同。而复杂度的高低又是我们选择集合类型的重要依据。

  1. 单元素操作是基础;是指每一种集合类型对单个数据实现的增删改查操作
  2. 范围操作非常耗时;是指集合类型中的遍历操作
  3. 统计操作通常高效;是指合类型对集合中所有元素个数的记录
  4. 例外情况只有几个:是指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂度也只有 O(1),可以实现快速操作。