1. Redis 有多少种数据结构?
主要有 5 种 Redis 对象,分别是 String、List、Hash、Set、Zset,这里的对象都指的是 Value 部分。底层实现依托于 sds、ziplist、skiplist、dict 等更基础的数据结构。
2. String(字符串)
2.1 简单介绍
字符串类型是 Redis 最基础的数据结构,字符串类型可以是JSON
、XML
甚至是二进制的图片等数据,但是最大值不能超过512MB
。在 Redis 中,String 是可以修改的,称为动态字符串
(Simple Dynamic String
简称SDS
),说是字符串但它的内部结构更像是一个ArrayList
,内部维护着一个字节数组,并且在其内部预分配了一定的空间,以减少内存的频繁分配。Redis
的内存分配机制是这样:
- 当字符串的长度小于 1MB 时,每次扩容都是加倍现有的空间。
- 如果字符串长度超过 1MB 时,每次扩容时只会扩展 1MB 的空间。
这样既保证了内存空间够用,还不至于造成内存的浪费,字符串最大长度为512MB
。分析一下 SDS 的数据结构:
struct SDS {
T capacity; //数组容量
T len; //实际长度
byte flages; //标志位,低三位表示类型
byte[] content; //数组内容
}
capacity
和len
两个属性都是泛型,为什么不直接用int类型
?因为Redis
内部有很多优化方案,为更合理的使用内存,不同长度的字符串采用不同的数据类型表示(int
、embstr
、raw
),且在创建字符串的时候len
会和capacity
一样大,不产生冗余的空间,所以String
值可以是字符串、数字(整数、浮点数) 或者 二进制。
Redis会根据当前值的类型和长度决定使用哪种内部编码来实现。字符串类型的内部编码有3
种:
- int:8个字节的长整型。
- embstr:小于等于39个字节的字符串。
- raw:大于39个字节的字符串。
2.2 应用场景
2.2.1 缓存
在 web 服务中,通常使用 MySQL 作为数据库,Redis 作为缓存。由于 Redis 具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web 端的大多数请求都是从 Redis 中获取的数据,如果 Redis 中没有需要的数据,则会从 MySQL 中去获取,并将获取到的数据写入 Redis。2.2.2 计数
Redis 中有一个字符串相关的命令incr key
,incr
命令对值做自增操作,返回结果分为以下三种情况:
- 值不是整数,返回错误
- 值是整数,返回自增后的结果
- 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 中的List
和Java
中的LinkedList
很像,底层都是一种链表结构,List
的插入和删除操作非常快,时间复杂度为 O(1),不像数组结构插入、删除操作需要移动数据。像归像,但是 Redis 中的List
底层可不是一个双向链表那么简单。
当数据量较少的时候它的底层存储结构为一块连续内存,称之为ziplist(压缩列表)
,它将所有的元素紧挨着一起存储,分配的是一块连续的内存;当数据量较多的时候将会变成quicklist(快速链表)
结构。
可单纯的链表也是有缺陷的,链表的前后指针prev
和next
会占用较多的内存,会比较浪费空间,而且会加重内存的碎片化。在 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 碰撞时将会把元素追加到链表上,值得注意的是在Redis
的Hash
中value
只能是字符串。
4.2 使用场景
4.2.1 购物车
hset [key] [field] [value]
命令, 可以实现以用户Id
,商品Id
为field
,商品数量为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 中的set
和Java
中的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(跳跃表)
当 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