1. Redis 有多少种数据结构?

主要有 5 种 Redis 对象,分别是 String、List、Hash、Set、Zset,这里的对象都指的是 Value 部分。底层实现依托于 sds、ziplist、skiplist、dict 等更基础的数据结构。
image.png

2. String(字符串)

2.1 简单介绍

字符串类型是 Redis 最基础的数据结构,字符串类型可以是JSONXML甚至是二进制的图片等数据,但是最大值不能超过512MB。在 Redis 中,String 是可以修改的,称为动态字符串(Simple Dynamic String简称SDS),说是字符串但它的内部结构更像是一个ArrayList,内部维护着一个字节数组,并且在其内部预分配了一定的空间,以减少内存的频繁分配。Redis的内存分配机制是这样:

  • 当字符串的长度小于 1MB 时,每次扩容都是加倍现有的空间。
  • 如果字符串长度超过 1MB 时,每次扩容时只会扩展 1MB 的空间。

这样既保证了内存空间够用,还不至于造成内存的浪费,字符串最大长度为512MB。分析一下 SDS 的数据结构:

  1. struct SDS {
  2. T capacity; //数组容量
  3. T len; //实际长度
  4. byte flages; //标志位,低三位表示类型
  5. byte[] content; //数组内容
  6. }

capacitylen两个属性都是泛型,为什么不直接用int类型?因为Redis内部有很多优化方案,为更合理的使用内存,不同长度的字符串采用不同的数据类型表示(intembstrraw),且在创建字符串的时候len会和capacity一样大,不产生冗余的空间,所以String值可以是字符串、数字(整数、浮点数) 或者 二进制。

Redis会根据当前值的类型和长度决定使用哪种内部编码来实现。字符串类型的内部编码有3种:

  1. int:8个字节的长整型。
  2. embstr:小于等于39个字节的字符串。
  3. raw:大于39个字节的字符串。

    2.2 应用场景

    2.2.1 缓存

    在 web 服务中,通常使用 MySQL 作为数据库,Redis 作为缓存。由于 Redis 具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web 端的大多数请求都是从 Redis 中获取的数据,如果 Redis 中没有需要的数据,则会从 MySQL 中去获取,并将获取到的数据写入 Redis。

    2.2.2 计数

    Redis 中有一个字符串相关的命令incr keyincr命令对值做自增操作,返回结果分为以下三种情况:
  • 值不是整数,返回错误
  • 值是整数,返回自增后的结果
  • key不存在,默认键为0,返回1

比如文章的阅读量,视频的播放量等等都会使用 Redis 来计数,每播放一次,对应的播放量就会加1,同时将这些数据异步存储到数据库中达到持久化的目的。

2.2.3 共享 Session

在分布式系统中,用户的每次请求会访问到不同的服务器,这就会导致 session 不同步的问题,假如一个用来获取用户信息的请求落在 A 服务器上,获取到用户信息后存入 session。下一个请求落在 B 服务器上,想要从 session 中获取用户信息就不能正常获取了,因为用户信息的 session 在服务器 A 上,为了解决这个问题,使用 Redis 集中管理这些 session,将 session 存入redis,使用的时候直接从 Redis 中获取就可以了。

2.2.4 限速

为了安全考虑,有些网站会对 IP 进行限制,限制同一 IP 在一定时间内访问次数不能超过 n 次。

2.3 String 常用命令

set [key]  [value]   给指定key设置值(set 可覆盖老的值)
get [key]   获取指定key 的值
del [key]   删除指定key
exists [key]  判断是否存在指定key
mset [key1]  [value1]  [key2]  [value2] ...... 批量存键值对
mget [key1]  [key2] ......   批量取key
expire [key]  [time]    给指定key 设置过期时间  单位秒
setex [key]  [time]  [value]  等价于 set + expire 命令组合
setnx [key]  [value]   如果key不存在则set 创建,否则返回0
incr [key]           如果value为整数 可用 incr命令每次自增1
incrby [key] [number]  使用incrby命令对整数值 进行增加 number

3. List(列表)

3.1 简单介绍

Redis 中的ListJava中的LinkedList很像,底层都是一种链表结构,List的插入和删除操作非常快,时间复杂度为 O(1),不像数组结构插入、删除操作需要移动数据。像归像,但是 Redis 中的List底层可不是一个双向链表那么简单。

当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表),它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)结构。

可单纯的链表也是有缺陷的,链表的前后指针prevnext会占用较多的内存,会比较浪费空间,而且会加重内存的碎片化。在 Redis 3.2 之后就都改用ziplist+链表的混合结构,称之为quicklist(快速链表)ziplist的每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。

3.2 应用场景

由于List是一个按照插入顺序排序的列表,所以应用场景相对还较多的,例如:

3.2.1 消息队列

列表用来存储多个有序的字符串,既然是有序的,那么就满足消息队列的特点。使用lpush+rpop或者rpush+lpop实现消息队列。除此之外,redis支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列。

Redis 虽然支持消息队列的实现,但是并不支持 ack。所以 Redis 实现的消息队列不能保证消息的可靠性,除非自己实现消息确认机制,不过这非常麻烦,所以如果是重要的消息还是推荐使用专门的消息队列去做。

3.2.2 栈

由于列表存储的是有序字符串,满足队列的特点,也就能满足栈先进后出的特点,使用lpush+lpop或者rpush+rpop实现栈。

3.2.3 文章列表

因为列表的元素不但是有序的,而且还支持按照索引范围获取元素。lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表。比如我们可以使用命令lrange key 0 9分页获取文章列表。

3.3 List 常用命令

rpush [key] [value1] [value2] ......    链表右侧插入
rpop [key]  移除右侧列表头元素,并返回该元素
lpop   [key]    移除左侧列表头元素,并返回该元素
llen  [key]     返回该列表的元素个数
lrem [key] [count] [value]  删除列表中与value相等的元素,count是删除的个数。 count>0 表示从左侧开始查找,删除count个元素,count<0 表示从右侧开始查找,删除count个相同元素,count=0 表示删除全部相同的元素
lindex [key] [index]  获取list指定下标的元素 (需要遍历,时间复杂度为O(n)) index 代表元素下标,index 可以为负数, index= 表示倒数第一个元素,同理 index=-2 表示倒数第二 个元素。
lrange [key]  [start_index] [end_index]   获取list 区间内的所有元素(时间复杂度为 O(n))
ltrim  [key]  [start_index] [end_index]   保留区间内的元素,其他元素删除(时间复杂度为 O(n))

4. Hash(字典)

4.1 简单介绍

Redis 中的Hash和 Java 的HashMap更加相似,是数组+链表的结构,当发生 hash 碰撞时将会把元素追加到链表上,值得注意的是在RedisHashvalue只能是字符串。

4.2 使用场景

4.2.1 购物车

hset [key] [field] [value]命令, 可以实现以用户Id商品Idfield,商品数量为value,恰好构成了购物车的3个要素。

4.2.2 存储对象

hash类型的(key, field, value)的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象,如:

key=JavaUser293847
value={
  “id”: 1,
  “name”: “SnailClimb”,
  “age”: 22,
  “location”: “Wuhan, Hubei”
}

4.3 Hash 常用命令

hset  [key]  [field] [value]    新建字段信息
hget  [key]  [field]    获取字段信息
hdel [key] [field]  删除字段
hlen  [key]   保存的字段个数
hgetall  [key]  获取指定key 字典里的所有字段和值 (字段信息过多,会导致慢查询 慎用:亲身经历 曾经用过这个这个指令导致线上服务故障)
hmset  [key]  [field1] [value1] [field2] [value2] ......   批量创建
hincr  [key] [field]   对字段值自增
hincrby [key] [field] [number] 对字段值增加number

5. Set(集合)

5.1 简单介绍

Redis 中的setJava中的HashSet有些类似,它内部的键值对是无序的、唯一 的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。当集合中最后一个元素被移除之后,数据结构被自动删除,内存被回收。

5.2 应用场景

  • 比如:在在线讨论社区中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。
  • 好友、关注、粉丝、感兴趣的人集合:
    1)sinter命令可以获得A和B两个用户的共同好友;
    2)sismember命令可以判断A是否是B的好友;
    3)scard命令可以获取好友数量;
    4) 关注时,smove命令可以将B从A的粉丝集合转移到A的好友集合
  • 首页展示随机:美团首页有很多推荐商家,但是并不能全部展示,set类型适合存放所有需要展示的内容,而srandmember命令则可以从中随机获取几个。
  • 抽奖功能:用户点击抽奖按钮,参数抽奖,将用户编号放入集合,然后抽奖,分别抽一等奖、二等奖,如果已经抽中一等奖的用户不能参数抽二等奖则使用spop key [count],反之使用srandmember key [count]

    5.3 Set 常用命令

    sadd [key]  [value]  向指定key的set中添加元素
    smembers [key]    获取指定key 集合中的所有元素
    sismember [key] [value]   判断集合中是否存在某个value
    scard [key]    获取集合的长度
    spop [key]   弹出一个元素
    srem [key] [value]  删除指定元素
    

    6. Zset(有序集合)

    6.1 简单介绍

    Zset也叫SortedSet一方面它是个set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个score,代表这个 value 的排序权重。它的内部实现由两种数据结构支持:ziplist 和 skiplist。

    6.1.1 ziplist(压缩列表)

    当 Zset 使用 ziplist 作为存储结构的时候,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。

    6.1.2 skiplist(跳跃表)

    image.png
    当 Zset 使用 skiplist 作为存储结构时,使用 skiplist 按序保存元素分值,使用 dict 来保存元素和分值的对应关系。具体实现可参考 https://www.yuque.com/codershenghai/javalearning/qtuoil#edE8j

    6.1.3 Zset 为什么同时需要使用字典和跳表来实现?

    Zset 是一个有序列表,字典和跳表分别对应两种查询场景,字典用来支持按成员查询数据,跳表则用以实现高效的范围查询,这样两个场景,性能都做到了极致。

    6.2 Zset 应用场景

    6.2.1 排行榜

    list不同的是Zset它能够实现动态的排序。比如用来存储粉丝列表,在线讨论社区项目的关注模块用到了Zset,value 为粉丝的用户 ID,score 为关注时间,这样我们可以对粉丝列表按关注时间进行排序。

Zset还可以用来存储学生的成绩,value值是学生的 ID,score是他的考试成绩。 我们对成绩按分数进行排序就可以得到他的名次。

6.2.2 延迟消息队列

在一个下单系统中,下单后需要在 15 分钟内进行支付,如果 15 分钟未支付则自动取消订单。将下单后的 15 分钟后时间作为 score,订单作为 value 存入 Redis,消费者轮询去消费,如果消费的大于等于这笔记录的 score,则将这笔记录移除队列,取消订单。

6.3 Zset 常用命令

zadd [key] [score] [value] 向指定key的集合中增加元素
zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
zrevrange [key] [start_index] [end_index]  获取范围内的元素列表 ,按score排序 逆序输出
zcard [key]  获取集合列表的元素个数
zrank [key] [value]  获取元素再集合中的排名
zrangebyscore [key] [score1] [score2]  输出score范围内的元素列表
zrem [key] [value]  删除元素
zscore [key] [value] 获取元素的score

7. HyperLogLog

  • 常用于计数,它采用一种基数算法,用于完成独立总数的统计。
  • 占据空间小,无论统计多少个数据,只占12K的内存空间。
  • 是一种不精确的统计算法,标准误差为0.81%。
  • 比如:在线讨论社区中统计独立访客UV,使用当前日期作为 key,IP地址作为 value 存入 HyperLoglog。如果要统计指定日期范围内的 UV,那么整理该日期范围内的 key 到一个列表中,然后对这个列表求一个 union,然后统计 HyperLoglog 的 size 即可。

    8. Geo

    用于支持存储地理位置信息。

    参考

  1. 一口气说出Redis 5种数据结构及对应使用场景,面试要加分的
  2. redis五大数据类型使用场景
  3. 我是面试官,Redis面试攻略第一弹