redis为啥这么快?
一方面,他是内存数据库,所有操作都在内存的层面完成。另一方面,这要归功于他的数据结构。redis的键值对是按照一定的数据结构来组织的,操作键值对最终就是对数据结构来进行增删改查的操作,所以高效的数据结构就是reids快速处理数据的基础。
redis的底层数据结构一共有六种,分别是简单动态字符串,双向链表,压缩列表,哈希表,跳表和整数数组
可以看到,String类型的底层实现只有一种数据结构,也就是简单的动态字符串。而List,Hash,Set和Sorted Set这四种数据类型,都有两种底层实验结构,通常情况下,我们会把这种类型成为集合类型,他们的特点是一个键对应了一个集合的数据。由此我们需要思考几个问题:
这是数据结构都是值的底层实现,键和值本身之间用什么结构组织?
为什么集合类型有那么多的底层结构,他们都是怎么组织数据的,都很快吗?
什么是简单动态字符串,和常用的字符串是一回事吗?
第一个问题redis为了实现从键到值的快速访问,redis使用了一个哈希表来保存所有键值对。 一个哈希表就是一个数组,数组中的每一个元素称为哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。如果值是集合类型的数据,哈希桶中的元素保存并不是值本身,而是指向具体值的指针。不管值是String还是集合类型,哈希桶中的元素都是指向他们的指针。如果在redis中写入大量数据的时候,有可能突然发现操作变慢了,其潜在风险为:哈希表的冲突问题和rehash可能带来的操作阻塞。
为什么哈希表变慢了?
当像hash表中写入更多数据是,哈希冲突是不可避免的问题。这里的哈希冲突是指:两个Key的哈希值和哈希桶计算对应关系是,正好落在了同一个哈希桶。比较哈希桶的个数通常要少于key的数量,也就是说,难免会有一些key的哈希值对应到了同一个hash桶中。redis解决哈希冲突的办法是链式哈希,就是值同一个哈希桶中的多个元素用一个链表来保存,他们之间依次用指针连接。此时忍存在一个问题,哈希冲突链上的元素只能通过指针逐一查找在操作。如果哈希表中写入的数据越来越多,哈希冲突链过长,进而导致这个链上的元素查找耗时长,效率降低。对于追求快的redis来说不能接受,所以redis会对哈希表进行rehash操作,rehash就是增加现有的哈希桶数量,让逐渐增多的entry元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。 redis为了使reshash更加的高效,redis默认使用了两个全局哈希表:哈希表1和哈希表2。一开始插入数据时,默认使用哈希表1,此时哈希表2并没有被分配空间。随着数据逐步增多,redis开始执行rehash,这个过程分为三步:一,给哈希表2分配更大的空间,例如时当前哈希表1大小的两杯;二,把哈希表1中的数据重新映射并拷贝到哈希表2中;三释放哈希表1的空间。
这个过程看似简单,但是第二步涉及大量的数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成redis线程阻塞,无服务其他请求了。 为了避免这个问题,redis采用了渐进式rehash。在第二步拷贝数据时,redis仍然能正常处理客户端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中,等待下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的entries。这样就巧妙地把一次大量的拷贝开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。所以哈希表的O(1)就是他的复杂度。
除了哈希表,redis集合类型的底层数据结构主要还有:整数数组,双向连标,压缩表和跳表。
其中整数数组和双向链表也很常见,他们的操作特征都是顺序读写,也就是通过数组下表或者链表的指针逐个元素访问,操作复杂度基本时O(n),操作效率比较低;压缩列表和跳表少见。
压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数据不同的时,压缩列表在表头有三个字段zlbytes(列表长度),zltail(列表尾的偏移量)和zllen(列表中的 entry个数),在压缩列表在表尾还有一个zlend表示列表结束。
在压缩列表中,如果我们要查找定位第一个和最后一个元素,可以通过表头的三个字段和长度定位,复杂度为o(1).而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是O(n)了。
有序列表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。具体来说跳表在链表的基础是那个,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位,如图
如果在这个链表中查找33这个元素,只能从头开始便利链表,查找6次,知道找到33为止。此时,复杂度是O(n),查找效率很低。
为了提高查找速度,我们来增加一级索引。这些索引在通过指针指向原始的链表。例如,从前两个元素中抽取元素1作为一级索引,从三 四元素中抽取元素11作为一级索引。此时,我们只需要4次差就能定位到元素33了。如果我们还想再快,可以增加二级索引:从一级索引中,再抽取出部分元素作为二级索引,如图,这样我们只需要查询三次,就能定位到元素33了。可以看到,在这个查找过程就是在多级索引上跳来跳去,最后定位到元素。当数据量很大的时候跳表的复杂度就是O(logN)。
集合类型的操作类型很多,有读写单个集合元素的,例如HGET,HSET,也有操作多个元素的,例如SADD,还有对整个集合进行遍历操作的,例如SMEMBERS。这么多操作他们的复杂度也各不相同。而复杂度的高低有时我们选择集合类型的重要依据。记住四句口诀,帮助你快速记住集合常见操作的复杂度。
“单元素操作是基础,番位操作非常耗时,统计操作通常高效,例外情况只有几个”
第一,单元素操作是指每一种集合类型对单个数据实现的增删改查操作。例如,hash类型的HGET,HSET和HDEL,set类型的SADD,SREM,SREANDMEMBER等,这些操作的复杂度由集合才有的数据结构决定,例如 HGET,HSET和HDEL都是对哈希表做操作,所以对他们的复杂度都是O(1);Set类型用哈希表作为底层数据结构时,他的SADD,SREM,SRANDMEMBER复杂度也是O(1)。