Reids数据结构&慢操作
Redis快的原因:
#0.Redis是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快
#1.Redis的键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,
Redis快的体现:
#0. redis在接收到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作
Redis的数据结构
#0.Redis键值对中值的数据类型(也就是数据的保存形式):
String(字符串)、List(列表)、Hash(哈希)、Set(集合)、SortedSet(有序集合)
#1.底层数据结构(底层实现)
String(字符串类型)->简单动态字符串
List(列表)->双向链表、压缩列表,快速列表是把压缩列表作为元素的双向链表
Hash(哈希)->压缩列表、哈希表
Sorted set(有序结合)->压缩列表、跳表
Set(集合)->哈希表、整数数组
其中除了String之外,其他类型都有两种底层实现结构,这四种类型也被称为集合类型,它们的特点是一个键对应了一个集合的数据
问题1:键与值之间的结构组织
1.这些数据结构都是值的底层实现,键和值之间用什么结构组织?
答:Redis从键到值的快速访问,Redis使用了一个哈希表来保存所有键值对
一个哈希表就是一个数组,数组的每个元素成为一个哈希桶,所以也称为,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据。
哈希桶的entry元素中保存了*key和*value指针,分别指向了实际的键和值,即便值是一个集合,也可以通过*value指针被查到
由于这个表保存了所有键值对也称为全局哈希表,哈希表的好处就是可以用O(1)的时间复杂度来快速查找到键值对,只需要通过计算键的哈希值就可以知道它所对应的哈希桶位置,从而访问到相应的entry元素
在查找过程中主要依赖于哈希计算和数据量的多少并没有直接关系,也就是说,不管哈希表里面有10万和键还是100万个键,只需要一次计算就能找到相应的键
问题2:哈希操作为什么变慢,如何解决
如果只是了解了哈希表的O(1)复杂度和快速查找特性,那么,当往redis写入大量数据后,就可能发现操作有时候突然变慢了,这其实是因为忽略了一个潜在风险,就是哈希表的冲突问题和rehash可能带来的操作阻塞
#1.第一个W,为什么哈希表操作慢了?
当我们往哈希表中写入更多数据时,哈希冲突是不可避免的问题,这里的哈希冲突,也就是两个key的哈希值和哈希桶计算对应关系是,正好落在了一个哈希桶中,由于哈希桶的个数通常要少于key的数量,也就是说,难免会有一些key的哈希值对应到了同一个哈希桶中
#2.第二个w,如何解决哈希冲突
reids解决哈希冲突的方式,就是链式哈希,链式哈希就是指同一个哈希桶中的多个元素用一个链表来保存,他们之间依次用指针连接。
如下图,entry1、entry2、entry3都需要保存在哈希桶3中,导致了哈希冲突,此时,entry1元素会通过一个*next指针指向entry2,同样,entry2也会通过*next指针指向entry3,这样即使哈希桶中的元素有100个,也可以通过entry元素中的指针,把他们连起来就形成了一个链表,也叫作哈希冲突链。
问题3:哈希冲突链过长如何解决
#0.第一个w。为什么出现哈希链过长的问题
上面通过链式哈希解决了哈希操作慢(哈希冲突)的问题,但是依然存在哈希冲突链上的元素只能通过指针逐一查找在操作,如果哈希表里写入的数据越来越多,哈希冲突也越来越多,这就会导致某些哈希冲突链过长,从而影响这个链上的元素查找耗时长
#1.第二个w,如何解决哈希链过长的问题
(1)redis会对哈希表做rehash操作,rehash就是增加现有哈希桶的数量,让逐渐增多的entry元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个桶中的冲突。
(2)为了rehash操作更加高效,Redis默认使用了两个全局哈希表,哈希表1和哈希表2,当刚插入数据时,默认使用哈希表1,此时的哈希表2并没有被分配空间,随着数据逐步增更多,redis开始执行rehash,整个过程分为三步:
.给哈希表2分配更大的空间,例如是当前哈希表1大小的两倍
.把哈希表1中的数据重新映射并拷贝到哈希表2中
.释放哈希表1的空间
到这里,我们就可以从哈希表1切换到哈希表2,用增大的哈希表2保存更多数据,而原来的哈希表1留作下一次rehash扩容备用
在此过程中,第二部涉及大量的数据拷贝,如果一次性把哈希表1中的数据都迁移完,会造成redis线程阻塞,无法服务其他请求,此时,redis就无法快速访问数据了。
问题4:哈希表的rehash操作过程汇总,涉及大量的数据拷贝,如何处理
#0.第一个W,为什么会出现大量数据拷贝
为了解决哈希链过长,哈希表1向哈希表2数据映射拷贝时出现了大量的数据拷贝
#1.第二个W,rehash过程中涉及的大量数据拷贝如何处理
为了避免这个问题,Redis采用了渐进式rehash,在第二部拷贝数据时,Redis仍然正常处理客户端请求,每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有entries拷贝到哈希表2中,等处理下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的entries,巧妙的把一次性大量拷贝的开销,分摊到多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问,如下图
问题5:集合数据操作效率,有哪些底层数据结构
#0.集合数据操作效率与String类型不同,一个集合类型的值,第一步是通过全局哈希表找到对应的哈希桶位置,第二步是在集合中在增删改查,所以,集合的操作效率与哪些因素相关呢?(1)与集合的底层数据结构有关,例如:使用哈希表实现的集合,要比使用链表实现的集合访问效率更高(2)操作效率和这些操作本身的执行特点有关,比如读写一个元素的操作要比读写所有元素的效率高#1.为什么集合类型有那么多的底层结构,他们都是怎么组织数据的,都很快吗?集合类型的底层数据结构主要有5种:整数数组、双向链表、哈希表、压缩列表和跳表(1)哈希表的操作特点上面已经说明过了;(2)整数数组和双向链表:它们的操作特征都是顺序读写,也就是通过数组下标或者链表的指针逐个元素访问,操作复杂度基本是O(N),操作效率比较低;(3)压缩列表:如果要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是O(1),而查找其他元素时,只能逐个查找,复杂度就是O(N)了。压缩列表说明:类似于数组,数组上的每个元素都对应保存一个数据,和数组不同的在于,压缩列表在表头有三个字段,zlbytes、zltail、zllen,分别表示列表长度,列表尾的偏移量和列表中的entry个数,压缩列表在表尾还有一个zlnd表示列表结束。(4)跳表:有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表,跳表在链表的基础上,增加了多机索引,通过索引位置的几个跳转,实现数据的快速定位,当数据量很大时,跳表的查找复杂度就是 O(logN)。#2.数据结构的时间复杂度 ---选择依据哈希表:O(1)跳表:O(logN)双向链表:O(N)压缩列表:O(N)整数数组:O(N)#3.集合类型的操作复杂度 ---选择集合类型的依据单元素操作是基础,范围操作非常耗时统计操作通常高效,例外情况只有几个(1)单元素操作,是指每一种集合类型对单个数据实现的增删改查操作,例如:Hash类型的HGET、HSET、HDEL和Set类型的SADD、SREM、SRANDMEMBER等。这些操作的复杂度由集合采用的数据结构决定,例如:HGET、HSET和HDEL是对哈希表做操作,所以他们的复杂度都是O(1);Set类型用哈希表作为底层数据结构时,它的SADD、SREM、SRANDMEMBER 复杂度也是 O(1)注意:集合类型支持同时对多个元素进行增删改查,例如Hash类型的HMGET和HMSET,Set类型的SADD也支持同时增加多个元素,此时这些操作的复杂度就是有单个元素操作复杂度和元素个数决定的,例如,HMSET增加M个元素时,复杂度就总O(1)变成O(M)了(2)范围操作,是指集合类型中的遍历操作,可以返回集合中的所有数据,比如Hash类型的HGETALL和Set类型的SMEMBERS,或者返回一个范围内的部分数据,比如List类型的LRANGE和ZSet类型的ZRANGE。这类操作的复杂度一般是O(N),比较耗时,应该尽量避免zset有个ZSCORE的操作,用于返回单个集合member的分数,它的操作复杂度是O(1),这就是收益于你这看到的hash table。这个hash table保存了集合元素和相应的分数,所以做ZSCORE操作时,直接查这个表就可以,复杂度就降为O(1)了注意:从Redis2.8版本开始提供了SCAN系列操作(包括HSCAN,SSCAN和ZSCAN),这类操作实现了渐进式遍历,每次只返回有限数量的数据,相比于HGETALL、SMEMBERS这类操作来说,避免了一次性返回所有元素而导致的Redis阻塞。为了使用scan命令,扩容时用的是高位加法排序。(3)统计操作,是指集合类型对集合中所有元素个数的记录,例如LLEN和SCARD,这类操作复杂度只有O(1),这是因为当集合类型采用压缩列表,双向列表、整数数组这些数据结构时,这些结构中专门记录了元素的个数统计,因此可以高效地完成相关操作(4)例外情况,某些数据结构的特殊记录,例如:压缩列表和双向链表都会记录表头和表尾的偏移量,这样对于List类型的LPOP、RPOP、LPUSH、RPUSH这四个操作来说,它们是在列表头尾增删元素,这就可以通过偏移量直接定位,所以他们的复杂度也只有O(1)可以实现快速操作。#2.什么是简单动态字符串,和常用的字符串是一回事吗?暂无果,待后续完善
问题6:整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,为什么Redis把它们作为地层结构数据?
主要有两方面原因:#1.内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少,Redis是内存数据库,大量的数据存到内存中,此时需要尽可能的优化,提高内存的利用率#2.数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排序的方式存储,同时利用CPU高速缓存不会降低访问速度,当数据元素超过设定阈值后(64字节),避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率#3.Redis的List底层使用压缩列表本质上是将所有元素紧挨着存储,所以分配的是一块连续的内存空间,虽然数据结构本身没有时间复杂度的优势,但是这样节省空间而且也能避免一些内存碎片;#3.如果在数组上是随机访问,对CPU告诉缓存是否友好?数组的随机访问,如果命中CPU的缓存那还算是友好的;如果没有命中缓存,那是不友好的并且还会浪费cpu预加载的操作。CPU读取内存的时候,会把一片连续的内存块读取出来,然后放到缓存中。因为数组结构是连续的内存地址,所以数组全部或者部分元素被连续存在CPU缓存里面,cpu读取缓存里面的每个元素的时间平均只要3个CPU时钟周期。而链表的节点是分散在堆空间(内存)里面的,cpu需要去读取内存,平均读取时间需要100个CPU时钟周期,cpu访问数组比链表快了大约33倍。
数据结构小结:
#1.redis底层数据结构,包括了reids中用来保存每个键和值的全局哈希表结构,也包括了支持集合类型实现的双向链表、压缩列表、整数数组、哈希表和跳表这五大底层结构。#2.reids之所以能快速操作键值对,一方面是因为O(1)复杂度的哈希表被广泛使用,包括String、Hash和Set,他们的操作复杂度基本由哈希表决定,另一方面,Sorted Set也采用了O(logN)复杂度的跳表,不过集合类型的范围操作,因为要遍历底层数据结构,复杂度通常是O(N),建议使用其他命令代替,例如可以用SCAN来代替,避免在Redis内部产生费时的全集合遍历操作。#3.List类型复杂度较高,它的两种底层实现结构:双向链表和压缩列表的操作复杂度都是O(N),可以考虑合适的使用List类型,例如,它的POP/PUSH效率很高,可以考虑将它主要用于FIFO队列场景,而不是作为一个可以随机读写的集合#4.整数数组和压缩列表都是在内存中分配一块地址连续的空间,然后把集合中的元素一个接一个地放在这块空间内,非常紧凑。因为元素是挨个连续放置的,我们不用再通过额外的指针把元素串接起来,这就避免了额外指针带来的空间开销,节省内存空间
跳表的说明:
如果我们要在链表中查找 33 这个元素,只能从头开始遍历链表,查找 6 次,直到找到 33 为止。此时,复杂度是 O(N),查找效率很低。为了提高查找速度,我们来增加一级索引:从第一个元素开始,每两个元素选一个出来作为索引。这些索引再通过指针指向原始的链表。例如,从前两个元素中抽取元素 1 作为一级索引,从第三、四个元素中抽取元素 11 作为一级索引。此时,我们只需要 4 次查找就能定位到元素 33 了。如果我们还想再快,可以再增加二级索引:从一级索引中,再抽取部分元素作为二级索引。例如,从一级索引中抽取 1、27、100 作为二级索引,二级索引指向一级索引。这样,我们只需要 3 次查找,就能定位到元素 33 了。