Redis数据结构

Redis为什么这么快?快在哪里?

快在哪里?

它接受到一个键值对操作后,能以微秒级别的速度找到数据,并快速完成操作.


为什么?

一方面,这是因为他是内存数据库,所有的操作都是在内存上完成,内存的访问速度本身就很快.
另一方面,归功于它的数据机构.这是因为,键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是Redis快速处理数据的基础.


值的数据类型(数据的保存形式)

String字符串、List列表、Hash哈希、Set集合、Sorted Set有序集合


6种底层数据结构与Redis数据类型的关系

6中数据类型包括:简单动态字符串、双向链表、压缩列表、哈希表、跳表和整数数组

关系:

  • String:简单的动态字符串
  • List:双向链表、压缩列表
  • Hash:压缩列表、哈希表
  • Sorted Set:压缩列表、跳表
  • Set:哈希表、整数数组

String类型的底层实现只有一种数据结构就是简单动态字符串,而List、Hash、Set、Sorted Set这四种数据类型都有两种底层实现结构,通常情况下称之为集合类型,它们的特点就是一个键对应了一个集合的数据.


键和值用什么结构组织?

概括:为了实现从键到值得快速访问,Redis使用了一个哈希表来保存所有的键值对.

一个哈希表实际上就是一个数组,数组的每一个元素成为一个哈希桶.每个哈希桶中保存了键值对数据,当然,哈希桶中的元素保存的病不是值本身,而是指向具体值得指针,如下图:
在这里插入图片描述
哈希表的自大好处很明显,就是让我们以O(1)的时间复杂度来快速查找到键值对,我们只需要计算key的哈希值就能知道它所对应的哈希桶的位置,然后访问对应的key-value元素.这个计算只要依赖于哈希计算和数据量的多少没有直接关系,但是如果仅仅依赖于哈希表的O(1)复杂度和快速查找特性你会发现当Redis写入大量数据的时候可能出现操作有时候会突然变慢,这是因为你忽略了一个潜在的风险那就是哈希表的冲突问题和rehash可能带来的操作阻塞.

为什么会出现上述变慢的问题?

因为当哈希表中的数据量到达一定数量的时候,哈希冲突是无法避免的问题,哈希的得计算是一个数学问题一定会存在两个不同的key但是通过哈希计算得到相同的哈希值,这样也就带来了哈希冲突.
通俗点讲就是两个哈希值相同的不同的key的键值对对应到了同一个哈希桶中,也就是数组的同一个位置.
也就是说因为哈希冲突的真是存在,一般情况下哈希桶的数量是少于key的数量的.


Redis解决哈希冲突的方式

链式哈希:

链式哈希的意思是说同一个哈希桶中的多个键值对元素用一个链表来保存,他们之间用指针进行连接,如下图所示:
在这里插入图片描述
这就形成了一个链表,也叫作哈希冲突链.
但是依然存在一个问题,哈希链上的元素只能通过指针逐一查找再操作,如果某个哈希桶中的哈希链过长这就导致了链上的元素查找时间长,效率降低.
针对上述问题Redis会对哈希表进行rehash操作.

rehash操作:

rehash也就是增加现有的哈希桶的数量,让逐渐增多的key-value键值对元素能够在更多的痛之间分散保存,减少单个桶中元素数量,从而减少单个桶中的冲突.

rehash操作具体怎么做:
为了使rehash操作更高效,Redis默认使用了**两个全局哈希表**:哈希表1和哈希表2.
莫认状态下使用h[0],h[1]并不存在,随着数据的逐渐增加,redis开始执行rehash,这个过程分为三步:
1. 给h[1]分配比h[0]更大的空间,实际上的操作是如果负载因子h[0].used/h[0].size在没有进行BGADD或BGREMOVE的情况下大于等于1或者在进行BGADD或BGREMOVE的情况下大于等于5则进行扩容,并且扩容之后的大小为一个大于现在的全局哈希表中的哈希桶的entry数量的2倍的一个2的n次方的数
2. 把h[0]中的数据重新映射病拷贝到h[1]中
3. 释放h[0]的空间,具体的操作是释放h[0]的的空间并将重新映射后的h[1]修改为h[0],之前的h[0]作为下次映射的h[1]
原来的h[0]就进入了映射之前原来h[2]的状态用作下一次rehash操作.

但是第二步设计大量的数据拷贝,假设一次性把哈希表1的数据迁移到哈希表2中,作为单线程的redis这回造成线程阻塞,无法为其他请求提供服务.
为了避免上述的阻塞操作采用渐进式rehash,如下图所示:
在这里插入图片描述

简单来说就是在进行数据拷贝时,Redis仍然能够正常处理客户端的请求.
每处理一个请求,从哈希表1的第一个索引位置开始,顺带着讲这个索引位置上的key-value键值对元素拷贝到哈希表2中
等处理下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的key-value键值对元素,如下图所示:

这样就巧妙地将一次性大量拷贝的开销分摊到多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问.
至于rehash具体的算法有时间可以去看dictRehash(int,int)的源码去深入理解,就现在而言我认为应该和Java中HashMap进行二次哈希来避免hash碰撞的原理大致想通.

对于String类型来讲找到哈希桶就可以直接进行增删改查操作,哈希表的O(1)操作复杂度也就是String类型的操作复杂度,但是对于集合类型来讲就没有那么容易了,因为对于集合类型我们即使找到了哈希桶中的key-value键值对元素仍然需要在集合中再进一步对数据进行操作.


集合数据操作效率

影响因素

首先,与集合的底层数据结构有关.例如,使用哈希表实现的结合要比使用链表实现的集合操作效率更高.
其次,操作效率和这些操作本身的执行特点有关,例如,读写一个元素的操作要比读写所有元素的效率高.

数据结构再说明
  1. 哈希表的操作特点就是通过哈希计算直接找到key-value键值元素从而进行操作,时间复杂度仅仅是O1,操作效率较高.

  2. 整数数组和双向链表的操作特征都是顺序读写,也就是通过下标或者链表指针逐个进行元素的访问,操作复杂度是On,操作效率比较低.

  3. 压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据.和数组不同的是压缩列表在表头有三个字段zlbytes/zltail和zllen,分别表示列表长度/列表尾的偏移量和列表中entry元素的个数;压缩列表在表尾还有一个zlend,表示列表结束.在压缩列表中查找第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是O1,但是查找其他元素的时候只能逐个查找,时间复杂度就是On了.
    压缩列表示意图如下:
    在这里插入图片描述

  4. 有序链表只能之一查找元素,导致性能非常缓慢,跳表实在链表的基础上增加了多级索引,通过索引位置的几个跳转实现数据的快速定位.在这个查找过程中实际上就是在多级索引上跳动,最后定位到元素,当数据量很大时,跳表的操作复杂度就是Ologn.
    跳表示意图如下:
    在这里插入图片描述


不同操作的复杂度

4种集合类型的操作类型很多,或操作单个集合元素,或操作多个集合元素,或对整个集合进行遍历操作,他们的操作复杂度各不相同.

  • 单元素操作是基础
  • 范围操作非常耗时
  • 统计操作非常高效
  • 例外情况只有几个

第一,单元素操作是指每一种集合类型对单个数据实现的增删改查操作.

这些操作的复杂度取决于集合所采用的底层数据结构.
例如Hash类型的HGET/HSET/HDEL的底层数据结构是哈希表所以操作复杂度就是O1,Set类型的SADD/SREM/SRANDMEMBER以哈希表作为底层数据结构时的复杂度也是O1.
值得注意的是集合类型支持多个元素进行增删改查操作.
例如Hash类型的HMGET和HMSET,Set类型的SADD等,这些操作的时间复杂度就是由单个元素的操作复杂度和元素个数决定的,比如Om.

第二,范围操作是指集合类型中的遍历操作,可以返回集合中的所有数据.

比如Hash类型的HGETALL,Set类型的SMEMBERS,或者返回一个范围内的部分数据,比如List类型的LRANGE和ZSet类型的ZRANGE,这类操作的时间复杂度一般都是On.
但是Redis提供了SCAN系列操作,这类操作实现了渐进式遍历,每次只返回有限数量的数据,避免了一次性返回所有元素而导致的线程阻塞.

第三,统计操作是指集合类型对集合中所有元素个数的记录.

比如LLEN/SCARD,这类操作的时间复杂度只有O1,可以高效的完成相关操作.

第四,例外情况是指某些数据结构的特殊记录.

比如对于List类型的LPOP/RPOP/LPUSH/RPUSH来讲,可以直接通过偏移量直接定位表头和表尾元素实现快速操作,时间复杂度尾O1.


整数数组和压缩列表存在的必要性

  1. 内存利用率方面,由于数组和压缩列表是非常紧凑的数据结构,它比链表占用的空间更小,这样对于一个内存数据库来讲可以尽量的优化以提高内存的使用率.
  2. 数组对CPU高速缓存方面支持更友好,在集合数据元素较少的情况下默认采用内存紧凑排列的方式进行存储,同时利用CPU的高速缓存不会降低访问速度,当数据量到达一定阈值的时候为避免查询时间复杂度太高转为哈希表或跳表的数据结构来存储以保证操作效率.并且CPU缓存行的大小是64字节,同时Redis使用数组和压缩列表时单个元素的大小也是64个字节,所以即使是在数组上的随机访问对于CPU的高速缓存也是友好的.
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值