Redis数据结构和类型

Redis数据结构和类型_第1张图片

Redis 包含五种数据类型,分别为String、List、Hash、Set、ZSet

底层实现的数据结构包SDS、双向链表、压缩列表、哈希表、整数集合、跳表

  • redis结构图

Redis数据结构和类型_第2张图片

  • 数据类型和数据结构的关系

Redis数据结构和类型_第3张图片

Redis六种数据结构

一、动态字符串(SDS)

Redis 是用 C 语言实现的,但是它没有直接使用 C 语言的 char* 字符数组来实现字符串,而是自己封装了一个名为简单动态字符串(simple dynamic string,SDS) 的数据结构来表示字符串,也就是 Redis 的 String 数据类型的底层数据结构是 SDS

Redis数据结构和类型_第4张图片

总的来说,Redis 的 SDS 结构在原本字符数组之上,增加了三个元数据:len、alloc、flags,用来解决 C 语言字符串的缺陷,之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少

优点:

  • 获取字符串长度复杂度:C 语言的字符串长度获取 strlen 函数,复杂度是O(n),而 Redis 的 SDS 结构因为加入了 len 成员变量,所以是O(1)
  • 二进制安全:因为 SDS 不需要用 “\0” 字符来标识字符串结尾了
  • 不会发生缓冲区溢出:C 语言的字符串标准库提供的字符串操作函数,大多数(比如 strcat 追加字符串函数)都是不安全的,Redis 的 SDS 结构里引入了 alloc 和 leb 成员变量,这样 SDS API 通过 alloc - len 计算,可以算出剩余可用的空间大小,这样在对字符串做修改操作的时候,就可以由程序内部判断缓冲区大小是否足够用
  • 节省内存空间:SDS 结构中有个 flags 成员变量,表示的是 SDS 类型,之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少

二、链表(linkedlist)

list 结构为链表提供了链表头指针 head、链表尾节点 tail、链表节点数量 len、以及可以自定义实现的 dup、free、match 函数

Redis数据结构和类型_第5张图片

三、压缩列表(ziplist)

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组

当我们往压缩列表中插入数据时,压缩列表 就会根据数据是字符串还是整数,以及它们的大小会在 prevlen 和 encoding 这两个元素里保存不同的信息,这种根据数据大小进行对应信息保存的设计思想,正是 Redis 为了节省内存而采用的

Redis数据结构和类型_第6张图片

压缩列表除了查找复杂度高的问题,压缩列表在插入元素时,如果内存空间不够了,压缩列表还需要重新分配一块连续的内存空间,而这可能会引发连锁更新的问题

压缩列表里的每个节点中的  prevlen 属性都记录了「前一个节点的长度」,而且 prevlen 属性的空间大小跟前一个节点长度值有关,比如:

  • 如果前一个

节点的长度小于 254 字节,那么 prevlen 属性需要用 1 字节的空间来保存这个长度值;

  • 如果前一个

节点的长度大于等于 254 字节,那么 prevlen 属性需要用 5 字节的空间来保存这个长度值

这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」,连锁更新一旦发生,就会导致压缩列表 占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能

四、哈希表(hash)

Hash 表优点在于,它能以 O(1) 的复杂度快速查询数据。主要是通过 Hash 函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快

Redis数据结构和类型_第7张图片

但是存在的风险也是有,在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高,Redis 采用了链式哈希来解决哈希冲突,以及rehash

为了避免 rehash 在数据迁移过程中,因拷贝数据的耗时,影响 Redis 性能的情况,所以 Redis 采用了渐进式 rehash,也就是将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移

触发 rehash 操作的条件,主要有两个:

  • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作

五、跳表(skiplist)

有序列表 zset 的数据结构,它类似于 Java 中的 SortedSet 和 HashMap 的结合体,一方面它是一个 set 保证了内部 value 的唯一性,另一方面又可以给每个 value 赋予一个排序的权重值 score,来达到 排序 的目的

因为 zset 要支持随机的插入和删除,所以它 不宜使用数组来实现,关于排序问题,我们也很容易就想到 红黑树/ 平衡树 这样的树形结构,为什么 Redis 不使用这样一些结构呢

  1. 性能考虑:在高并发的情况下,树形结构需要执行一些类似于 rebalance 这样的可能涉及整棵树的操作,相对来说跳跃表的变化只涉及局部
  2. 实现考虑:在复杂度与红黑树相同的情况下,跳跃表实现起来更简单,看起来也更加直观;

跳跃表 skiplist 就是受到这种多层链表结构的启发而设计出来的。按照上面生成链表的方式,上面每一层链表的节点个数,是下面一层的节点个数的一半,这样查找过程就非常类似于一个二分查找,使得查找的时间复杂度可以降低到 O(logn)

Redis数据结构和类型_第8张图片

这种方法在插入数据的时候有很大的问题。新插入一个节点之后,就会打乱上下相邻两层链表上节点个数严格的 2:1 的对应关系。如果要维持这种对应关系,就必须把新插入的节点后面的所有节点 (也包括新插入的节点) 重新进行调整,这会让时间复杂度重新蜕化成 O(n)。删除数据也有同样的问题

skiplist 为了避免这一问题,它不要求上下相邻两层链表之间的节点个数有严格的对应关系,而是 为每个节点随机出一个层数(level)。从上面的创建和插入的过程中可以看出,每一个节点的层数(level)是随机出来的,而且新插入一个节点并不会影响到其他节点的层数,因此,插入操作只需要修改节点前后的指针,而不需要对多个节点都进行调整,这就降低了插入操作的复杂度。

六、整数集合(intset)

intset实质就是一个有序数组,存储元素紧密,空间利用率高,并通过二分法降低查找元素的时间复杂度,而且不容易因频繁地插入删除而产生内存碎片

支持整型编码,intset中所有数据元素的存储类型是一致的。新插入数据时,如果数据的类型大于当前intset的数据类型,为了防止溢出,会对其进行升级操作,然后才能将新元素添加到整数集合里

Redis数据结构和类型_第9张图片

Intset 只支持升级,不支持降级

升级会引起整个 intset 进行内存重分配,并移动集合中的所有元素,这个操作的复杂度为O(n)

升级整数集合:

1)根据新元素的类型,拓展整数集合底层数组的空间大小,并且为新元素分配空间。

2)将底层数组现有的元素都转成新原属相同的类型,并且将转换后的元素放置到正确的位上,而且放置元素的过程中,需要继续位置数组的有序性质不变。

3)将新元素加入到底层数组里面

Redis五种基本数据类型

一、String

字符串对象的编码可以是int(整数 可以用long类型)、raw(超过39字节)、embstr(小于等于39字节)

raw编码会调用两次内存分配函数来创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的内存,空间依次包含redisObject和sdshdr结构

  • 应用场景
    • 计数器:incr操作用来计数
    • 缓存:缓存普通信息
  • 常用API
set   [key]  [value]   给指定key设置值
get  [key]   获取指定key 的值
setex    [key]  [time]  [value]  等价于 set + expire 命令组合
expire [key]  [time]    给指定key 设置过期时间  单位秒
exists  [key]  判断是否存在指定key
mset  [key1]  [value1]  [key2]  [value2] ...... 批量存键值对
mget  [key1]  [key2] ......   批量取key
incr   [key]           如果value为整数 可用 incr命令每次自增1
incrby  [key] [number]  使用incrby命令对整数值 进行增加 number

二、Hash

Redis 散列可以存储多个键值对之间的映射。和字符串一样,散列存储的值既可以是字符串又可以是数值,并且用户同样可以对散列存储的数字值执行自增或自减操作。这个和 Java 的 HashMap 很像,每个 HashMap 有自己的名字,同时可以存储多个 k/v 对

  • 使用场景
    • Hash更适合存储结构化的数据:存储对象的每个属性
    • 购物车场景:hset [key] [field] [value] 存储购物车的三个要素
    • 用户已读:key:uid field:mid value:时间戳
  • 常用API
hset  [key]  [field] [value]    新建字段信息
hget  [key]  [field]    获取字段信息
hgetall  [key]  获取指定key 字典里的所有字段和值
hmset  [key]  [field1] [value1] [field2] [value2] ......   批量创建

三、List

有序可重复列表,编码可以是ziplist、linkedlist,列表对象保存的所有字符串元素的长度都小于 64 字节并且保存的元素数量小于 512 个,使用 ziplist 编码;否则使用 linkedlist

  • 使用场景
    • 消息队列:rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试
    • 可以用来实现粉丝/点赞列表,通过rpush插入数据,然后使用lrange命令读取最新的元素列表
  • 常用API
rpush  [key] [value1] [value2] ......    链表右侧插入
rpop    [key]  移除右侧列表头元素,并返回该元素
lpop   [key]    移除左侧列表头元素,并返回该元素
llen  [key]     返回该列表的元素个数
lrange [key]  [start_index] [end_index]   获取list 区间内的所有元素 (时间复杂度为 O(n))

四、Set

Redis 的set和list都可以存储多个字符串,他们之间的不同之处在于,list是有序可重复,而set是无序不可重复

  • 使用场景
    • 业务场景用户白名单:点赞、投稿
    • 业务失败兜底留存:用户打赏失败后记录信息
  • 常用API
sadd  [key]  [value]  向指定key的set中添加元素
smembers [key]    获取指定key 集合中的所有元素
scard [key]    获取集合的长度
srem [key] [value]  删除指定元素

五、SortSet

zset也叫SortedSet一方面它是个 set ,保证了内部 value 的唯一性,另方面它可以给每个 value 赋予一个score,代表这个value的排序权重。它的内部实现用的是一种叫作“跳跃列表”的数据结构

  • 使用场景
    • 排行榜:score为热度值或者点赞数等 飙升榜
    • 带权重的消息队列:重要的消息 score 大一些,普通消息 score 小一些,可以实现优先级高的任务先执行
    • 投稿审核优先级队列:score为mediaId uuid 发号器,越大说明最新
  • 常用API
zadd [key] [score] [value] 向指定key的集合中增加元素
zrem [key] [value]  删除元素
zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
zrevrange [key] [start_index] [end_index]  获取范围内的元素列表 ,按score排序 逆序输出
zrangebyscore [key] [score1] [score2]  输出score范围内的元素列表
zcard [key]  获取集合列表的元素个数
zscore [key] [value] 获取元素的score

你可能感兴趣的:(Redis,redis,链表,java)