1.字符串(string)

string是Redis最基础的数据类型,底层通过int或SDS(动态字符串:Simple Dynamic String)实现。在设置一个字符串键值对后,Redis会根据value的类型、长度来决定使用哪种编码,通过不同的编码方式映射到不同的数据结构。

1.1 内部结构

图片

  • int类型数据结构支持incr、incrby、decr、decrby等命令对value值进行增减操作。

  • embstr编码的SDS结构是专门用于保存短字符串的优化编码方式,与raw编码不同,embstr编码通过一次内存分配函数将字符串KV对象和value值分配在连续的内存块中,提高CPU取数据的效率问题。

  • raw编码的SDS结构用于存储较长的字符串,通过俩次内存分配函数得到俩块内存,给字符串KV对象和value分别存储。

1.2 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
get [key] [value]  // 获取指定key对应value

set [key] [value] // 给指定key设置value,该命令会覆盖旧的KV

set [key] [value] [ex 秒数]/[px 毫秒]/[nx 纳秒] // 给指定key设置value和过期时间,该命令会覆盖旧的KV和过期时间

append [key] [value] // 把value追加到key的原值上

expire [key] [time] // 等价于set + expire命令

setex [key] [time] [value] // 给指定key设置过期时间,单位秒

setnx [key] [value] // 如果key不存在则set,否则返回0

incr [key] // 如果key为整数则使值增加1

incrby [key] [number] // 如果key为整数则使值增加number

decr [key] // 如果key为整数则使值减少1

decrby [key] [number] // 如果key为整数则使值减少number

1.3 应用场景

  • 分布式缓存: 字典常量数据、JSON字符串、图片视频等序列化数组、Session等
  • 分布式锁: 根据业务对接口或代码块加分布式锁
  • 常规计数: 网站浏览量、点击量、收藏量等
  • 接口监控: 通过AOP和Redis计数器对指定接口限流或屏蔽

2 哈希(hash)

hash类型在Redis类型中是指键值中的值本身又是一个键值对,底层通过ziplist(压缩列表)或hashtable(哈希表)实现,Redis会根据哈希表中的KV元素数量、每个元素值大小来决定使用哪种数据。

2.1 内部结构

图片

  • ziplist是Redis自己设计的数据结构,使用更加紧凑的结构实现多个元素的连续存储,相比较hashtable更加节约内存,但是在大量元素的情况下读写效率不如hashtable,大块连续的存储空间的要求也比较苛刻。

  • hashtable的结构与Java JDK1.7版本的HashMap类似,采用数组加链表的形式进行存储,相比较ziplist在大量元素情况下读写效率更好,因为读写时间复杂度为O(1)。

2.2 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
hset [object-key] [hash-key] [hash-value] // 对某个hash对象设置单个键值对

hsetnx [object-key] [hash-key] [hash-value] // 对某个hash对象设置单个键值对(如果hash-key已存在则什么都不做)

hset [object-key] [hash-key-1] [hash-value-1] [hash-key-2] [hash-value-2] // 对某个hash对象设置多个键值对

hexists [object-key] [hash-key] // 判断hash对象某个key是否存在(0否 1是)

hget [object-key] [hash-key-1] // 获取hash对象单个key的值

hmget [object-key] [hash-key-1] [hash-key-2] // 获取hash对象多个key的值

hlen [object-key] // 获取hash对象某个键值对

hgetall [object-key] // 获取hash对象所有键值对

hkeys [object-key] // 获取某个hash对象的key列表

hdel [object-key] // 删除hash的某个key

hincrby [object-key] [hash-key] num // hash对象某个数字类型value递增num(num为整数)

hincrbyfloat [object-key] [hash-key] num // hash对象某个数字类型value递增num(num为浮点)

2.3 应用场景

哈希一般用于存储结构简单、属性较多、修改频率较高的对象信息,比如购物车、商品信息、配置信息等。哈希存储对象相对于字符串存储对象的优势在于,整个修改操作可以只针对某个属性。

比如购物车的商品数量发生改变,字符串存储需要将内容提取出来转为对象,修改完属性后在转化为字符串存储,在购物车数据比较大的情况下会增加很多无意义的资源浪费。哈希类型存储购物车信息一般将每个商品的ID和数量作为元素的键值对,每次发生改变也仅仅是修改某个键值对的value,涉及到的资源消耗相对较少。

3.列表(list)

list类型是用来存储多个有序的字符串,3.2之前的版本底层通过ziplist(压缩列表)或linkedlist(链表)实现,Redis会根据列表中的KV元素数量、每个元素值大小来决定使用哪种数据结构,3.2包括以后版本采用quicklist代替。

3.1 内部结构

图片

  • ziplist的优缺点上面已经提到了。
  • linkedlist和ziplist都是线性数据结构,当元素较多或者某元素内存占用较大时,连续存储不太现实,还得采用linkedlist方式进行存储。

3.2 应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
lpush [key] [value] [value]...  // 在list的头部插入单个或多个value,如果可以不存在则先创建(返回插入个数)

rpush [key] [value] [value]... // 在list的尾部插入单个或多个value,如果可以不存在则先创建(返回插入个数)

lpushx [key] [value] // 在list的头部插入单个value,如果key不存在则什么都不做

rpushx [key] [value] // 在list的尾部插入单个value,如果key不存在则什么都不做

lrange [key] [start] [end] // 获取list从start到end之间的元素,start和end可为负数,-1代表尾部元素,-2代表倒数第二个

llen [key] // 获取list的长度

lpop [key] // 返回并删除list中的第一个元素

rpop [key] // 返回并删除list中的最后一个元素

lrem [key] [count] [value] // 从头遍历并删除count个值为value的元素,如果count=0,则删除全部

3.3 应用场景

Redis关于此类型结构的操作比较丰富,支持两端插入和弹出、还可以获取指定范围的元素列表、获取指定索引下标的元素等,是一个比较灵活的数据结构,因此应用场景也比较灵活。可以利用list实现消息队列、列表的分页数据缓存等。

4.去重集合(set)

set类型是一个无序并唯一的键值集合,底层通过inset(整数集合)或hashtable(哈希表)实现,根据集合内部的元素类型、个数来决定使用哪种数据结构。

4.1 内部结构

图片

intset集合存在的意义就是尽量减少内存的使用,当intset的条件无法满足时,使用hashtable进行存储,set集合对hashtable的使用和Java一样,仅仅使用key而已。

4.2 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sadd [key] [value-1] [value-2]...  // 添加单个或多个元素

scard [key] // 获取集合中元素个数

sismember [key] [value] // 判断某个元素是否存在(返回 0否 1是)

spop [key] // 随机弹出一个元素,如果set没有元素则返回(nil)

srandmember [key] [num] // 随机获取集合中num个元素,num为大于总数则获取全量,为负数则随机获取num的绝对值个(可能有重复)

smove [key-A] [key-B] [value] // 将key-A的某个元素移动到key-B中

srem [key] [value] // 删除某个元素(返回 0没找到 1删除成功)

sdiff [key] [key-A] [key-B] // 差集,返回key有、key-A和key-B没有的所有元素(也可以一对一比较)

sdiffstore [key] [key-A] [key-B] // 差集,计算后将结果重置到key中

sinter [key] [key-A] [key-B] // 交集,返回多个key中共有的元素

sdiffstore [key] [key-A] [key-B] // 交集,计算后将结果重置到key中

sunion [key] [key-A] [key-B] // 并集,返回多个key中包含的所有元素

sunionstore [key] [key-A] [key-B] // 并集,计算后将结果重置到key中

4.3 应用场景

set类型具有无序、不可重复、支持并交差等特性,在电商、社交、视频网站等平台有着广泛的应用,通过将用户的标签信息存储在set结构中,可以向用户推荐相同兴趣爱好的好友,或者存储好友信息来展示相同好友功能等。

通过set集合提供的SADD、SRANDMEMBER命令可以实现抽奖功能。需要注意的是Redis在2.6版本才支持SADD命令的count参数,在3.2版本支持SRANDMEMBER命令的count参数,2.6版本以下仅支持单次获取一个随机元素。

5.有序去重集合(zset)

zset类型相对于set类型,每个元素多了一个score值,保证元素内容不重复的前提下,通过score值对元素自动排序,底层通过ziplist(压缩列表)或skiplist(跳跃表)实现,根据集合内部的元素个数、大小来决定使用哪种数据结构。

5.1 数据结构

图片

  • ziplist的优缺点上面已经提到了。
  • skiplist也是一个有序的数据结构,为了避免链表结构这种查找元素需要从头到尾遍历的弊端,采用在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
  • zset还会维护一个字典结构(hashtable),用于新增元素时判断是否已存在

5.2 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
zadd [key] [score-1] [value-1] [score-2] [value-2]...  // 添加单个或多个元素

zrange [key] [start] [end] [withscores] // 根据范围列出zset的所有元素和对应score,如果end为-1则代表尾部,-2为倒数第二个元素

zrangebyscore [key] [start-score] [end-score] // 根据score范围取元素(闭区间)

zrangebyscore [key] [start-score] [end-score] [withscores] // 根据score范围取元素和对应score(闭区间)

zrangebyscore [key] [start-score] [end-score] [withscores] limit [start-index] [size] // 根据score范围取元素和对应score(闭区间),并分页

zcard [key] // 获取元素个数

zcard [key] [start-score] [end-score] // 获取指定socre范围的元素个数

zrank [key] [value] // 获取某个元素的下标值

zscore [key] [value] // 获取某个元素的score值

zrem [key] [value-1] [value-2] // 删除指定元素

5.3 应用场景

既然数据结构的亮点是排序,那么应用场景自然也是和排序相关。应用最为广泛的就是排行榜系统,例如某个网站的文章访问量、视频播放量/点赞量、电商系统商品的销售量等等。

Redis的ZRANGEBYLEX命令支持对相同的score元素进行排序,实现方式是将元素字符串转化为二进制数组的字节数进行比较,因此可以用来实现手机号、姓名等排序场景。

6.SDS(动态字符串)

SDS的实现原理和Java的ArrayList差不多,都是采用动态扩容的方式来减少内存的频繁分配。当字符串长度小于1M时,扩容后为原来的两倍,当字符串超过1M时,每次固定扩容1M。Redis对SDS的扩容限制为最高512M,也就是说Redis的字符串最多可以存储512M的字符串,超过此容量会报错。

6.1 内部结构

图片

7.ziplist(压缩列表)

ziplist存在的意义就是压缩、节约内存,当然这种节约内存是相对于数组而言进行的优化,在Redis中存在的意义是为了弥补链表和哈希表在内存分配上的不足。

7.1 数组存储的弊端

我们都知道数组中每个元素的大小相同,在我们存储字符串的时候,不能保证存储的每个元素大小都相同,这就需要以占用内存最大的元素作为数组元素大小的标准。假设某个数组中最大的元素占用30KB,数组中小于30KB的元素存储就会出现部分下标的内存浪费:
图片

7.2 数组压缩存储

数组的优势在于分配一片连续的空间,提升CPU的内存访问效率,压缩列表保留了这个优势,同时在结构上将每个下标的进行压缩,压缩后的占用保持和对应元素大小一致,达到节约内存的目的:
图片

这就有个问题了,每个元素占用的内存块不固定,无法向数组那样通过下标内存大小计算出下个元素的位置。因此压缩列表为每个元素添加了一个length属性,通过这个属性就很容易计算出下个元素的位置并提取数据了:
图片

7.2 优缺点

ziplist的特点是数据在内存中连续存储,不会像linkedlist、hashtable那样分散存储,不需要额外的存储指针来管理分散内存块之间的关联,再加上ziplist内存块中绝大多数空间都用来存储元素数据,且元素数据不会浪费内存空间,因此ziplist的优势在于节约内存。

当元素数量上升,或某个元素占用内存过大,会导致当前内存块大小无法满足而必须要扩展,内存块越大扩展的消耗就越大。对于list类型来说,提供的常用命令都是对列表的增删操作,大量元素下的情况下linkedlist更能胜任。对于hash类型来说,无论元素数量多大查询某个元素的时间复杂度永远是O(1),也比ziplist的更合适。

8.quicklist(压缩列表+链表混合)

列表类型在Redis3.2版本以前采用ziplist和linkedlist进行存储,前者的思想是时间换空间,后者的思想是空间换时间,无论哪种都有很明显的优缺点为此Redis3.2版本开始对列表数据结构进行了改造,创建了quicklist代替ziplist和linkedlist。

8.1 内存结构

quicklist是ziplist和linkedlist的混合体,从宏观上讲仍然是一个linkedlist,而其中的每个元素都是一个zipList,zipList内部紧凑且连续的存储着真实的元素数据:
图片

8.1 新增节点过程

quicklist仍然是将元素存储在zipList中,上面也讲过zipList不适合存储大型字符串和大量元素,扩容是他的硬伤。当新插入的元素无法放入zipList中时,quicklist会直接创建一个新的zipList并将数据存入,然后将新创建的zipList作为一个元素添到linkedlist中,删除也是同样的道理。

9.skiplist(跳表)

对于链表来说,即使结构中的数据是有序存储,如果想要查询某个数据也只能从头到尾遍历,查询效率较低。skiplist也是一种有序链表,为了提高查询效率,skiplist在每个节点上建立索引,避免从头到尾遍历,是一种空间换时间的优化思想。

9.1 内部结构

传统链表获取第9个元素,需要通过指针地址查询9次:
图片

跳表获取第9个元素,通过元素存储的索引,只需要通过指针地址查询5次:
图片

如果获取的是第8个元素,则通过索引遍历到索引9(大于8),然后退回索引7向后遍历:
图片

当元素数量增多,可以建立多层索引提高效率:
图片

Redis跳表结构:
图片

Redis跳表的元素索引存储的是链表下坐标,由于整个列表根据score排序存储,因此score的范围查询同样可以走索引。

评论