02 数据结构
02 数据结构
Redis接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作。
因为一方面,这是因为它是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快;另一方面,这要归功于它的数据结构。
redis的数据结构
String 类型的底层实现只有一种数据结构,也就是简单动态字符串。而List、Hash、Set 和 Sorted Set 这四种数据类型,都有两种底层实现结构,我们会把这四种类型称为集合类型。
Redis 使用了一个哈希表来保存所有键值对,实现从键到值的快速访问。
一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶,每个哈希桶中保存了键值对数据,哈希桶中的元素保存的并不是值本身,而是指向具体值的指针。
哈希表 O(1) 的时间复杂度来快速查找到键值对,只需要计算键的哈希值,就可以知道它所对应的哈希桶位置,然后就可以访问相应的 entry 元素。哈希桶中的 entry 元素中保存了*key
和*value
指针,分别指向了实际的键和值。
因为这个哈希表保存了所有的键值对,把它称为全局哈希表。
哈希冲突
两个 key 的哈希值和哈希桶计算对应关系时,两个 key 的哈希值正好落在了同一个哈希桶中,因为哈希桶的个数通常要少于 key 的数量。
解决方法:链式哈希。同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针连接。
若某些哈希冲突链过长,进而导致这个链上的元素查找耗时长,故Redis 会对哈希表做rehash 操作。
rehash 增加现有的哈希桶数量,让逐渐增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
渐进式 rehash
Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中,如此等待下一个请求做同样操作。
巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。
对于 String 类型来说,找到哈希桶就能直接增删改查了,所以,哈希表的 O(1) 操作复杂度也就是它的复杂度。
对于集合类型来说,即使找到哈希桶了,还要在集合中再进一步操作。
集合数据操作效率
集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈希表、压缩列表和跳表。
整数数组和双向链表:
操作特征都是顺序读写,通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是 O(N),操作效率比较低。
压缩列表:
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。
压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。
而查找其他元素时,只能逐个查找,此时的复杂度就是 O(N) 了。
跳表:
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。
当数据量很大时,跳表的查找复杂度就是 O(logN)。
不同操作的复杂度
问:整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?
答:整数数组和压缩列表的设计,充分体现了 Redis“又快又省”特点中的“省”,也就是节省内存空间。整数数组和压缩列表都是在内存中分配一块地址连续的空间。Redis 之所以采用不同的数据结构,其实是在性能和内存使用效率之间进行的平衡。