Redis的快主要表现在,它接受到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。
底层数据结构共6种:
从上图可以看出,String
类型的底层实现只有一种数据结构,就是简单动态字符串,List
、Hash
、Set
、Sorted Set
底层都有两种实现结构,它们的特点是一个键对应了一个集合的数据,这些数据结构都是值的底层实现。
为了实现从键到值的快速访问,Redis使用了一个哈希表来保存所有键值对。
一个哈希表其实就是一个数组,数组的每个元素称为一个哈希桶,所以一个哈希表由多个哈希桶组成,每个哈希桶中保持了键值对数据。哈希桶中实际上并不保存值本身,而是指向具体值的指针。
如下图所示,哈希桶中的entry
元素中保存了*key
和*value
指针,分别指向了实际的键和值,这样即使值是一个集合,也可以通过*value
指针被查找到。
这个哈希表中保存了所有的键值对,称为全局哈希表,有了它只需要O(1)
的时间复杂度来快速查找到键值对。
某一时刻,可能哈希表的操作变慢了,因为存在哈希冲突问题和refresh可能带来操作阻塞。
随着数据量的增加,哈希冲突不可避免,Redis使用链表法(同一个桶中多个元素用一个链表来保存)来解决冲突。
如上图所示,entry1
、entry2
、entry3
都需要保存在哈希桶3中,导致了哈希冲突,因此使用*next
指针来链接它们。
哈希冲突链上的元素只能通过指针逐一查找来操作,随着数据的增大,冲突变多,链表过长,进而导致这个链表上元素的查找耗时长,效率低下。
为了解决链表过长导致查找效率低下这个问题,Redis对哈希表做refresh操作(增加现有桶的数量),让逐渐增多的entry
元素能在更多的桶之间分散保存,减少单个桶中元素数量,减少单个桶中的冲突。
为了是refresh操作更高效,Redis默认使用两个全局哈希表(哈希表1和哈希表2):
entry
拷贝到哈希表2中)refresh中第二步的优化操作,如上图所示,巧妙的将一次性大量拷贝的开销,分摊到了多次处理请求的过程中。
Redis的键和值是通过哈希表组织的,
O(1)
集合类型的数据:
- 操作效率与集合底层数据结构有关。使用哈希表实现的集合要比使用链表实现的集合访问效率高。
- 操作效率与操作本身的执行特点有关。读写一个元素的操作要比读写所有元素的效率高。
总共有5中底层的数据结构:
数据结构 | 操作特征 | 操作复杂度 |
---|---|---|
整数数组 | 顺序读写,通过下标访问 | O(n) |
双向链表 | 顺序读写,通过指针逐个访问 | O(n) |
哈希表 | 通过计算哈希值访问 | O(1) |
压缩列表 | 可快速定位头尾 | O(n) |
跳表 | 通过多级索引访问 | O(logn) |
类似数据,数组中每一个元素都对应保存一个数据,不同点在于表头有zlbytes
(列表长度)、zktail
(列表尾的偏移量)、zllen
(类别中entry个数),在列表尾部zlend
(列表结束符)。
在压缩列表中:
O(1)
O(n)
有序链表只能逐一查找元素,导致操作缓慢,跳表在链表的基础上,增加多级索引,通过索引位置的几个跳转,实现数据的快速定位,时间复杂度O(logn)
集合类型的操作很多:
单元素操作时基础,范围操作非常耗时,统计操作通常高效、例外情况只有几个。
每一种集合类型对单个数据实现的增删改查操作。
O(1)
O(1)
这次操作的复杂度由集合采用的数据结构决定。
集合类型支持同时多多个元素进行增删改查,此时时间复杂度由当元素操作的时间复杂度和元素个数决定:
O(m)
,同时操作m个元素集合类型中的遍历操作,可以返回集合中所有数据:
也可以返回一个范围内的部分数据:
O(n)
O(n)
2.8版本,增加HSCAN操作,渐进式遍历,避免了一次性返回所有元素导致Redis阻塞。
集合类型对集合中所有元素个数的记录。例如LLEN和SCARD,时间复杂度O(1)
,因为当集合类型采用压缩列表、双向链表、整数数组时,这些数据结构中会专门记录元素的个数。
某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头和表尾的偏移量。
对于List类型的LPOP、RPOP、LPUSH、RPUSH操作时在列表的头尾进行增删元素,通过偏移量直接定位,时间复杂度是O(1)
。