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
