越努力,越幸运,
本文已收藏在GitHub中 JavaCommunity, 里面有面试分享、源码分析系列文章,欢迎收藏,点赞
https://github.com/Ccww-lx/Ja...
在实际开发,Redis
使用会频繁,那么在使用过程当中咱们该如何正确抉择数据类型呢?哪些场景下适用哪些数据类型。并且在面试中也很常会被面试官问到Redis数据结构方面的问题:git
当咱们分析理解了Redis
数据结构,能够为了咱们在使用Redis
的时候,正确抉择数据类型使用,提高系统性能。github
Redis
底层数据结构Redis
是一个内存键值key-value
数据库,且键值对数据保存在内存中,所以Redis
基于内存的数据操做,其效率高,速度快;面试
其中,Key
是String
类型,Redis
支持的 value
类型包括了 String
、List
、 Hash
、 Set
、 Sorted Set
、BitMap
等。Redis
可以之因此可以普遍地适用众多的业务场景,基于其多样化类型的value
。redis
而Redis
的Value
的数据类型是基于为Redis
自定义的对象系统redisObject
实现的,算法
typedef struct redisObject{ //类型 unsigned type:4; //编码 unsigned encoding:4; //指向底层实现数据结构的指针 void *ptr; ….. }
redisObject
除了记录实际数据,还须要额外的内存空间记录数据长度、空间使用等元数据信息,其中包含了 8 字节的元数据和一个 8 字节指针,指针指向具体数据类型的实际数据所在位置:数据库
其中,指针指向的就是基于Redis
的底层数据结构存储数据的位置,Redis
的底层数据结构:SDS
,双向链表、跳表,哈希表,压缩列表、整数集合实现的。segmentfault
那么Redis底层数据结构是怎么实现的呢?数组
咱们先来看看Redis
比较简单的SDS
,双向链表,整数集合。微信
SDS
、双向链表和整数集合SDS
,使用len
字段记录已使用的字节数,将获取字符串长度复杂度下降为O(1),并且SDS
是惰性释放空间的,你free
了空间,系统把数据记录下来下次想用时候可直接使用。不用新申请空间。
整数集合,在内存中分配一块地址连续的空间,数据元素会挨着存放,不须要额外指针带来空间开销,其特色为内存紧凑节省内存空间,查询复杂度为O(1)效率高,其余操做复杂度为O(N);数据结构
双向链表, 在内存上能够为非连续、非顺序空间,经过额外的指针开销前驱/后驱指针串联元素之间的顺序。
其特色为节插入/更新数据复杂度为O(1)效率高,查询复杂度为O(N);
Hash
哈希表哈希表,其实相似是一个数组,数组的每一个元素称为一个哈希桶,每一个哈希桶中保存了键值对数据,且哈希桶中的元素使用dictEntry
结构,
所以,哈希桶元素保存的并非键值对值自己,而是指向具体值的指针,因此在保存每一个键值对的时候会额外空间开销,至少有增长24个字节,特别是Value
为String
的键值对,每个键值对就须要额外开销24个字节空间。当保存数据小,额外开销比数据还大时,这时为了节省空间,考虑换数据结构。
那来看看全局哈希表全图:
虽然哈希表操做很快,但Redis
数据变大后,就会出现一个潜在的风险:哈希表的冲突问题和 rehash
开销问题,这能够解释为何哈希表操做变慢了?
当往哈希表中写入更多数据时,哈希冲突是不可避免的问题 , Redis 解决哈希冲突的方式,就是链式哈希,同一个哈希桶中的多个元素用一个链表来保存,它们之间依次用指针链接,如图所示:
当哈希冲突也会愈来愈多,这就会致使某些哈希冲突链过长,进而致使这个链上的元素查找耗时长,效率下降。
为了解决哈希冲突带了的链过长的问题,进行rehash
操做,增长现有的哈希桶数量,分散单桶元素数量。那么rehash
过程怎么样执行的呢?
Rehash
为了使rehash
操做更高效,使用两个全局哈希表:哈希表 1 和哈希表 2,具体以下:
但因为表1和表2在从新映射复制时数据大,若是一次性把哈希表 1 中的数据都迁移完,会形成 Redis
线程阻塞,没法服务其余请求。
为了不这个问题,保证Redi
s能正常处理客户端请求,Redis
采用了渐进式 rehash
。
每处理一个请求时,从哈希表 1 中依次将索引位置上的全部 entries 拷贝到哈希表 2 中,把一次性大量拷贝的开销,分摊到了屡次处理请求的过程当中,避免了耗时操做,保证了数据的快速访问。
在理解完Hash
哈希表相关知识点后,看看不常见的压缩列表和跳表。
压缩列表,在数组基础上,在压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表长度、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。
优势:内存紧凑节省内存空间,内存中分配一块地址连续的空间,数据元素会挨着存放,不须要额外指针带来空间开销;查找定位第一个元素和最后一个元素,能够经过表头三个字段的长度直接定位,复杂度是 O(1)。
跳表 ,在链表的基础上,增长了多级索引,经过索引位置的几个跳转,实现数据的快速定位,以下图所示:
好比查询33
特色:当数据量很大时,跳表的查找复杂度为O(logN)。
综上所述,能够得知底层数据结构的时间复杂度:
数据结构类型 | 时间复杂度 |
---|---|
哈希表 | O(1) |
整数数组 | O(N) |
双向链表 | O(N) |
压缩列表 | O(N) |
跳表 | O(logN) |
Redis
自定义的对象系统类型即为Redis
的Value
的数据类型,Redis
的数据类型是基于底层数据结构实现的,那数据类型有哪些呢?
String
、List
、Hash
、Sorted Set
、Set
比较常见的类型,其与底层数据结构对应关系以下:
数据类型 | 数据结构 |
---|---|
String | SDS(简单动态字符串) |
List | 双向链表 压缩列表 |
Hash | 压缩列表<br/>哈希表 |
Sorted Set | 压缩列表<br/>跳表 |
Set | 哈希表<br/>整数数组 |
数据类型对应特色跟其实现的底层数据结构差很少,性质也是同样的,且
String
,基于SDS实现,适用于简单key-value
存储、setnx key value
实现分布式锁、计数器(原子性)、分布式全局惟一ID。
List
, 按照元素进入List
的顺序进行排序的,遵循FIFO(先进先出)规则,通常使用在 排序统计以及简单的消息队列。
Hash
, 是字符串key
和字符串value
之间的映射,十分适合用来表示一个对象信息 ,特色添加和删除操做复杂度都是O(1)。
Set
,是String
类型元素的无序集合,集合成员是惟一的,这就意味着集合中不能出现重复的数据。 基于哈希表实现的,因此添加,删除,查找的复杂度都是 O(1)。
Sorted Set
, 是Set
的类型的升级, 不一样的是每一个元素都会关联一个 double 类型的分数,经过分数排序,能够范围查询。
那咱们再来看看这些数据类型,Redis Geo
、HyperLogLog
、BitMap
?
Redis Geo
, 将地球看做为近似为球体,基于GeoHash 将二维的经纬度转换成字符串,来实现位置的划分跟指定距离的查询。特色通常使用在跟位置有关的应用。
HyperLogLog
, 是一种几率数据结构,它使用几率算法来统计集合的近似基数 , 错误率大概在0.81%。 当集合元素数量很是多时,它计算基数所需的空间老是固定的,并且还很小,适合使用作 UV 统计。
BitMap
,用一个比特位来映射某个元素的状态, 只有 0 和 1 两种状态,很是典型的二值状态,且其自己是用 String 类型做为底层数据结构实现的一种统计二值状态的数据类型 ,优点大量节省内存空间,但是使用在二值统计场景。
在理解上述知识后,咱们接下来讨论一下根据哪些策略选择相对应的应用场景下的Redis
数据类型?
Redis
数据类型策略在实际开发应用中,Redis能够适用于众多的业务场景,但咱们须要怎么选择数据类型存储呢?
主要依据就是时间/空间复杂度,在实际的开发中能够考虑如下几个点:
当数据量比较大,数据自己比较小,使用String
就会使用额外的空间大大增长,由于使用哈希表保存键值对,使用dictEntry
结构保存,会致使保存每一个键值对时额外保存dictEntry
的三个指针的开销,这样就会致使数据自己小于额外空间开销,最终会致使存储空间数据大小远大于本来数据存储大小。
可使用基于整数数组和压缩列表实现了 List
、Hash
和 Sorted Set
,由于整数数组和压缩列表在内存中都是分配一块地址连续的空间,而后把集合中的元素一个接一个地放在这块空间内,很是紧凑,不用再经过额外的指针把元素串接起来,这就避免了额外指针带来的空间开销。并且采用集合类型时,一个 key 就对应一个集合的数据,能保存的数据多了不少,但也只用了一个 dictEntry
,这样就节省了内存。
Redis
集合类型统计模式常见的有:
Set
;Redis
中List
和 Sorted Set
是有序集合,List
是按照元素进入 List
的顺序进行排序的,Sorted Set
能够根据元素的权重来排序;Bitmap
自己是用 String
类型做为底层数据结构实现的一种统计二值状态的数据类型 , Bitmap经过 BITOP 按位 与、或、异或的操做后使用 BITCOUNT 统计 1 的个数。HyperLogLog
是一种用于统计基数的数据集合类型 ,统计结果是有必定偏差的,标准误算率是 0.81% 。须要精确统计结果的话,用 Set 或 Hash 类型。Set
类型,适用统计用户/好友/关注/粉丝/感兴趣的人集合聚合操做,好比
Redis
中List
和 Sorted Set
是有序集合,使用应对集合元素排序需求 ,好比
Bitmap
二值状态统计,适用数据量大,且可使用二值状态表示的统计,好比:
HyperLogLog
是一种用于统计基数的数据集合类型, 统计一个集合中不重复的元素个数 ,好比
Redis
中List
和 Sorted Set
是有序集合支持范围查询,可是Hash
是不支持范围查询的
消息队列,使用Redis
做为消息队列的实现,要消息的基本要求消息保序、处理重复的消息和保证消息可靠性,方案有以下:
基于List | 基于Strems | |
---|---|---|
消息保序 | 使用LPUSH/RPOP |
使用XADD/XREAD |
阻塞读取 | 使用BRPOP |
使用XREAD block |
重复消息处理 | 生产者自行实现全局惟一ID | Streams自动生成全局惟一ID |
消息可靠性 | 使用BRPOPLPUSH |
使用PENDING List自动留存消息 |
适用场景 | 消息总量小 | 消息总量大,须要消费组形式读取数据 |
基于位置 LBS 服务,使用Redis
的特定GEO
数据类型实现,GEO
能够记录经纬度形式的地理位置信息,被普遍地应用在 LBS 服务中。 好比:打车软件是怎么基于位置提供服务的。
Redis
之因此那么快,是由于其基于内存的数据操做和使用Hash
哈希表做为索引,其效率高,速度快,并且得益于其底层数据多样化使得其能够适用于众多场景,不一样场景中选择合适的数据类型能够提高其查询性能。
谢谢各位点赞,没点赞的点个赞支持支持 最后,微信搜《Ccww技术博客》观看更多文章,也欢迎关注一波。