Redis 的数据类型可谓是 Redis 的精华所在,同样的数据类型,例如字符串存储不同的值对应的实际存储结构也是不同,当你存储的 int 值是实际的存储结构也是 int,如果是短字符串(小于 44 字节)实际存储的结构为 embstr,长字符串对应的实际存储结构是 raw,这样设计的目的是为了更好的节约内存。
我们本文的面试题是 Redis 有哪些数据类型?
典型回答
Redis 最常用的数据类型有 5 种:String(字符串类型)、Hash(字典类型)、List(列表类型)、Set(集合类型)、ZSet(有序集合类型)。
1.字符串类型
字符串类型(Simple Dynamic Strings 简称 SDS),译为:简单动态字符串,它是以键值对 key-value 的形式进行存储的,根据 key 来存储和获取 value 值,它的使用相对来说比较简单,但在实际项目中应用非常广泛。
字符串的使用如下:
127.0.0.1:6379> set k1 v1 # 添加数据
OK
127.0.0.1:6379> get k1 # 查询数据
"v1"
127.0.0.1:6379> strlen k1 # 查询字符串的长度
(integer) 5
我们也可以在存储字符串时设置键值的过期时间,如下代码所示:
127.0.0.1:6379> set k1 v1 ex 1000 # 设置 k1 1000s 后过期(删除)
OK
我们还可以使用 SDS 来存储 int 类型的值,并且可以使用 incr
指令和 decr
指令来操作存储的值 +1 或者 -1,具体实现代码如下:
127.0.0.1:6379> get k1 # 查询 k1=3
"3"
127.0.0.1:6379> incr k1 # 执行 +1 操作
(integer) 4
127.0.0.1:6379> get k1 # 查询 k1=4
"4"
127.0.0.1:6379> decr k1 # 执行 -1 操作
(integer) 3
127.0.0.1:6379> get k1 # 查询 k1=3
"3"
字符串的常见使用场景:
- 存放用户(登录)信息;
- 存放文章详情和列表信息;
- 存放和累计网页的统计信息(存储 int 值)。
……
2.字典类型
字典类型 (Hash) 又被成为散列类型或者是哈希表类型,它是将一个键值 (key) 和一个特殊的“哈希表”关联起来,这个“哈希表”表包含两列数据:字段和值。例如我们使用字典类型来存储一篇文章的详情信息,存储结构如下图所示: 同理我们也可以使用字典类型来存储用户信息,并且使用字典类型来存储此类信息就无需手动序列化和反序列化数据了,所以使用起来更加的方便和高效。
字典类型的使用如下:
127.0.0.1:6379> hset myhash key1 value1 # 添加数据
(integer) 1
127.0.0.1:6379> hget myhash key1 # 查询数据
"value1"
字典类型的数据结构,如下图所示:
通常情况下字典类型会使用数组的方式来存储相关的数据,但发生哈希冲突时才会使用链表的结构来存储数据。
3.列表类型
列表类型 (List) 是一个使用链表结构存储的有序结构,它的元素插入会按照先后顺序存储到链表结构中,因此它的元素操作 (插入和删除) 时间复杂度为 O(1),所以相对来说速度还是比较快的,但它的查询时间复杂度为 O(n),因此查询可能会比较慢。
列表类型的使用如下:
127.0.0.1:6379> lpush list 1 2 3 # 添加数据
(integer) 3
127.0.0.1:6379> lpop list # 获取并删除列表的第一个元素
1
列表的典型使用场景有以下两个:
- 消息队列:列表类型可以使用 rpush 实现先进先出的功能,同时又可以使用 lpop 轻松的弹出(查询并删除)第一个元素,所以列表类型可以用来实现消息队列;
- 文章列表:对于博客站点来说,当用户和文章都越来越多时,为了加快程序的响应速度,我们可以把用户自己的文章存入到 List 中,因为 List 是有序的结构,所以这样又可以完美的实现分页功能,从而加速了程序的响应速度。
4.集合类型
集合类型 (Set) 是一个无序并唯一的键值集合。
集合类型的使用如下:
127.0.0.1:6379> sadd myset v1 v2 v3 # 添加数据
(integer) 3
127.0.0.1:6379> smembers myset # 查询集合中的所有数据
1) "v1"
2) "v3"
3) "v2"
集合类型的经典使用场景如下:
- 微博关注我的人和我关注的人都适合用集合存储,可以保证人员不会重复;
- 中奖人信息也适合用集合类型存储,这样可以保证一个人不会重复中奖。
集合类型(Set)和列表类型(List)的区别如下:
- 列表可以存储重复元素,集合只能存储非重复元素;
- 列表是按照元素的先后顺序存储元素的,而集合则是无序方式存储元素的。
5.有序集合类型
有序集合类型 (Sorted Set) 相比于集合类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。有序集合的存储元素值也是不能重复的,但分值是可以重复的。
当我们把学生的成绩存储在有序集合中时,它的存储结构如下图所示:
有序集合类型的使用如下:
127.0.0.1:6379> zadd zset1 3 golang 4 sql 1 redis # 添加数据
(integer) 3
127.0.0.1:6379> zrange zset 0 -1 # 查询所有数据
1) "redis"
2) "mysql"
3) "java"
有序集合的经典使用场景如下:
- 学生成绩排名;
- 粉丝列表,根据关注的先后时间排序。
考点分析
关于 Redis 数据类型的这个问题,对于大多数人既熟悉又陌生,熟悉的是每天都在使用 Redis 存取数据,陌生的是对于 Redis 的数据类型知之甚少,因为对于普通的开发工作使用字符串类型就可以搞定了。但是善用 Redis 的数据类型可以到达意想不到的效果,不但可以提高程序的运行速度又可以减少业务代码,可谓一举两得。
例如我们经常会把用户的登录信息存储在 Redis 中,但通常的做法是先将用户登录实体类转为 JSON 字符串存储在 Redis 中,然后读取时先查询数据再反序列化为 User 对象,这个过程看似没什么问题,但我们可以有更优的解决方案来处理此问题,比如我们可以使用 Hash 存储用户的信息,这样就无需序列化的过程了,并且读取之后无需反序列化,直接使用 Map 来接收就可以了,这样既提高了程序的运行速度有省去了序列化和反序列化的业务代码。
与此知识点相关的面试题还有以下几个:
- 有序列表的实际存储结构是什么?
- 除了五种基本的数据类型之外,还有什么数据类型?
知识扩展
有序列表的内部实现
有序集合是由 ziplist (压缩列表) 或 skiplist (跳跃表) 组成的。
ziplist 介绍
当数据比较少时,有序集合使用的是 ziplist 存储的,如下代码所示:
127.0.0.1:6379> zadd myzset 1 db 2 redis 3 mysql
(integer) 3
127.0.0.1:6379> object encoding myzset
"ziplist"
从结果可以看出,有序集合把 myset 键值对存储在 ziplist 结构中了。 有序集合使用 ziplist 格式存储必须满足以下两个条件:
- 有序集合保存的元素个数要小于 128 个;
- 有序集合保存的所有元素成员的长度都必须小于 64 字节。
如果不能满足以上两个条件中的任意一个,有序集合将会使用 skiplist 结构进行存储。 接下来我们来测试以下,当有序集合中某个元素长度大于 64 字节时会发生什么情况? 代码如下:
127.0.0.1:6379> zadd zmaxleng 1.0 redis
(integer) 1
127.0.0.1:6379> object encoding zmaxleng
"ziplist"
127.0.0.1:6379> zadd zmaxleng 2.0 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(integer) 1
127.0.0.1:6379> object encoding zmaxleng
"skiplist"
通过以上代码可以看出,当有序集合保存的所有元素成员的长度大于 64 字节时,有序集合就会从 ziplist 转换成为 skiplist。
小贴士:可以通过配置文件中的 zset-max-ziplist-entries(默认 128)和 zset-max-ziplist-value(默认 64)来设置有序集合使用 ziplist 存储的临界值。
skiplist 介绍
skiplist 数据编码底层是使用 zset 结构实现的,而 zset 结构中包含了一个字典和一个跳跃表,源码如下:
typedef struct zset {dict *dict;zskiplist *zsl;
} zset;
跳跃表的结构如下图所示:
根据以上图片展示,当我们在跳跃表中查询值 32 时,执行流程如下:
- 从最上层开始找,1 比 32 小,在当前层移动到下一个节点进行比较;
- 7 比 32 小,当前层移动下一个节点比较,由于下一个节点指向 Null,所以以 7 为目标,移动到下一层继续向后比较;
- 18 小于 32,继续向后移动查找,对比 77 大于 32,以 18 为目标,移动到下一层继续向后比较;
- 对比 32 等于 32,值被顺利找到。
从上面的流程可以看出,跳跃表会想从最上层开始找起,依次向后查找,如果本层的节点大于要找的值,或者本层的节点为 Null 时,以上一个节点为目标,往下移一层继续向后查找并循环此流程,直到找到该节点并返回,如果对比到最后一个元素仍未找到,则返回 Null。
高级数据类型
除了有 5 大基本数据类型外,还有 GEO(地理位置类型)、HyperLogLog(统计类型)、Stream(流类型)。
GEO(地理位置类型)是 Redis 3.2 版本中新增的数据类型,用于存储和查询地理位置的,使用它我们可以实现查询附近的人或查询附近的商家等功能(这部分的内容会在后面的章节单独讲解)。
Stream(流类型)是 Redis 5.0 版本中新增的数据类型,因为使用 Stream 可以实现消息消费确认的功能,使用“xack key group-key ID”命令,所以此类型的出现给 Redis 更好的实现消息队列提供了很大的帮助。
HyperLogLog(统计类型)是本文介绍的重点,HyperLogLog (下文简称为 HLL) 是 Redis 2.8.9 版本添加的数据结构,它用于高性能的基数 (去重) 统计功能,它的缺点就是存在极低的误差率。
HLL 具有以下几个特点:
- 能够使用极少的内存来统计巨量的数据,它只需要 12K 空间就能统计 2^64 的数据;
- 统计存在一定的误差,误差率整体较低,标准误差为 0.81%;
- 误差可以被设置辅助计算因子进行降低。
HLL 的命令只有 3 个,但都非常的实用,下面分别来看。
1.添加元素
127.0.0.1:6379> pfadd key "redis"
(integer) 1
127.0.0.1:6379> pfadd key "java" "sql"
(integer) 1
相关语法: pfadd key element [element ...]
此命令支持添加一个或多个元素至 HLL 结构中。
2.统计不重复的元素
127.0.0.1:6379> pfadd key "redis"
(integer) 1
127.0.0.1:6379> pfadd key "sql"
(integer) 1
127.0.0.1:6379> pfadd key "redis"
(integer) 0
127.0.0.1:6379> pfcount key
(integer) 2
从 pfcount 的结果可以看出,在 HLL 结构中键值为 key 的元素, 有 2 个不重复的值:redis 和 sql,可以看出结果还是挺准的。 相关语法: pfcount key [key ...]
此命令支持统计一个或多个 HLL 结构。
3.合并一个或多个 HLL 至新结构
新增 k 和 k2 合并至新结构 k3 中,代码如下:
127.0.0.1:6379> pfadd k "java" "sql"
(integer) 1
127.0.0.1:6379> pfadd k2 "redis" "sql"
(integer) 1
127.0.0.1:6379> pfmerge k3 k k2
OK
127.0.0.1:6379> pfcount k3
(integer) 3
相关语法:pfmerge destkey sourcekey [sourcekey ...]
** pfmerge 使用场景:当我们需要合并两个或多个同类页面的访问数据时,我们可以使用 pfmerge 来操作。
总结
本文我们介绍了 Redis 的 5 大基础数据类型的概念以及简单的使用:String(字符串类型)、Hash(字典类型)、List(列表类型)、Set(集合类型)、ZSet(有序集合类型),还深入的介绍了 ZSet 的底层数据存储结构:ziplist (压缩列表) 或 skiplist (跳跃表)。除此之外我们还介绍了 Redis 中的提前 3 个高级的数据类型:GEO(地理位置类型)用于实现查询附近的人、HyperLogLog(统计类型)用于高效的实现数据的去重统计(存在一定的误差)、Stream(流类型)主要应用于消息队列的实现。