1、基本数据结构

  • String(字符串)
  • List(列表)
  • Hash(哈希)
  • Set(集合)
  • Sorted Set(有序集合)
    上面都是Redis键值对中值的数据类型,也就是数据的保存形式。
     

2、底层数据结构

底层数据结构一共有6种,分别是简单动态字符串双向链表压缩列表哈希表跳表整数数组

redis 获取哈希数量 redis 哈希 数据结构_数组

3、键和值本身用什么结构组织?

为了实现从键到值的快速方法,Redis使用了一个哈希表来保存所有的键值对。

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

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

redis 获取哈希数量 redis 哈希 数据结构_数据_02

4、哈希冲突

哈希桶的个数通常要少于Key的数量,两个Key的哈希值和哈希桶计算对应关系的时候,正好落在了同一个哈希桶中。

解决方式:链式哈希。即同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针来连接。

带来的新问题:因此哈希冲突链上的元素只能通过指针逐一查找再操作,如果哈希表中写入的数据越来越多,哈希冲突也会越来越多,那么就会导致某些哈希冲突链会越来越长,影响查找效率。

解决方式:哈希表rehash

 

5、哈希表rehash

增加现有的哈希桶数量,让逐渐增多的entry元素在更多的哈希桶中分散保存,从而减少当个哈希桶中元素的数量,从而减少单个哈希桶的冲突。

操作方式:Redis默认采用了两个全局哈希表:哈希表1和哈希表2。一开始插入数据的时候,默认使用哈希表1,此时哈希表2并没有分配空间。随着数据的增多,Redis开始执行rehash。

步骤:

  1. 给哈希表2分配更大的空间,比如是当前哈希表1大小的2倍。
  2. 把哈希表1中的数据重新映射并拷贝到哈希表2。
  3. 释放哈希表1的空间,留作下一次rehash扩容备用。
     

问题:因为涉及到大量的数据拷贝,如果一次性的把哈希表1中的数据都迁移完成,会造成Redis的线程阻塞。

解决方式:渐进式rehash

6、渐进式rehash

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

7、集合类型的底层数据结构和操作复杂度

集合类型的底层数据结构主要有5种:整数数组、双向链表、哈希表、跳表、压缩列表

压缩列表:类似一个数组,数据中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段:zlbytes、zltail和 zllen,分别表示列表长度、列表尾的偏移量和列表中entry的个数,在表尾还有一个zlend,表示列表结束。如果要查找第一个元素和最后一个元素,通过表头的三个字段的长度直接定位,复杂度为O(1),其他元素只能逐个查找,复杂度为O(N)。

redis 获取哈希数量 redis 哈希 数据结构_数据_03

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

8、数据结构的时间复杂度

名称

时间复杂度

哈希表

O(1)

跳表

O(logN)

双向链表

O(N)

压缩列表

O(N)

整数数组

O(N)

9、不同操作的复杂度

主要是针对集合类型的操作

  1. 每一种集合类型对单个数据实现的增删改查操作是最基础的
  2. 范围操作,即对集合的遍历操作,可以返回集合中的所有元素。此类操作的复杂度一般是O(N),比较耗时,应该尽量避免。Redis2.8版本开始提供了渐进式的SCAN遍历操作,每次只返回有限数量的数据。
  3. 统计操作,即集合类型对集合中所有元素个数的记录。因集合类型采用压缩列表、双向链表、整数数组这些数据结构,结构中专门记录了元素的个数统计,通常是高效的。
  4. 压缩列表和双向链表都会记录表头和表尾的偏移量,因此对于List 类型的LPOP,RPOP,LPUSH,RPUSH四个操作来说,比较高效。可以用于FIFO队列的场景