Redis的数据类型(对象)

1. 概述

在这篇Redis的底层数据结构中,我们介绍了Redis的底层数据结构。但是Redis并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象。我们可以把它们看作Redis的五个数据类型。即使是同一种数据类型,在不同的场景下,其底层数据结构可能是不一样的,这样做的目的是优化在不同场景下的使用效率。

首先明确,Redis中使用对象来表示数据库中的键和值,每当创建一个键值对时,我们会至少创建两个对象,一个键对象,一个值对象。键总是一个字符串对象,而值可以是上面提到的五种类型的任一种。所以,当我们称“字符串键”、“列表键”等时,说的是键所对应的值的类型。

Redis中的每一个对象都有一个redisObject结构表示,该结构中和保存数据有关的三个属性分别是type、encoding、ptr。type代表此对象的数据数据类型(那五种),encoding指明了对象底层的数据结构类型。ptr是个指针,指向对象底层实现的数据结构。

typedef struct redisObject{
     
     unsigned type:4; //类型
     
     unsigned encoding:4; //编码
     
     void *ptr; //指向底层数据结构的指针
     ......
} robj;

介绍完了Redis用不同的数据结构实现各种数据类型的总体策略,下面开始分别介绍这五种数据类型。

2. 字符串对象(类型)

2.1 编码

字符串对象的编码可以是int、raw或embstr。int 编码是用来保存整数值,raw编码是用来保存长字符串,而embstr是用来保存短字符串。其实 embstr 编码是专门用来保存短字符串的一种优化编码。

  • int 编码:保存的是可以用 long 类型表示的整数值。
  • raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
  • embstr 编码:保存长度小于等于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

embstr与raw都使用redisObject和sds保存数据,区别在于,embstr的使用只分配一次内存空间(因为它的redisObject和sds是连续的),而raw需要分配两次内存空间(分别为redisObject和sds分配空间)。因此与raw相比,embstr的好处在于创建时少分配一次空间,删除时少释放一次空间,以及对象的所有数据连在一起,寻找方便。而embstr的坏处也很明显,如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,因此redis中的embstr实现为只读。

另外,Redis中对于浮点数类型也是作为字符串保存的,在需要的时候再将其转换成浮点数类型。

2.2 编码的转换

  • 当 int 编码保存的值不再是整数,或大小超过了long的范围时,自动转化为raw。
  • embstr是只读的,在对embstr对象进行修改时,都会先转化为raw再进行修改,因此,只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节。

3. 列表对象

3.1 编码

列表对象的编码可以是ziplist或者linkedlist。看图

3.2 编码转换

同时满足下面两个条件时,才会使用ziplist(压缩列表)编码,不能满足这两个条件的时候使用 linkedlist 编码。

  • 列表保存元素个数小于512个
  • 每个字符串元素长度都小于64字节

4. 哈希对象

4.1 编码

哈希对象的键是一个字符串类型,值是一个键值对集合。哈希对象的编码可以是ziplist或者hashtable。

ziplist编码的哈希对象底层使用压缩列表,新添加的键值对紧挨在一起,并保存在压缩列表的表尾。

hashtable编码的哈希对象底层实现是字典,哈希对象中的每个键值对都用一个字典键值对来保存。

在前面介绍压缩列表时,我们介绍过压缩列表是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,相对于字典数据结构,压缩列表用于元素个数少、元素长度小的场景。其优势在于集中存储,节省空间。

4.2 编码转换

同时满足下面两个条件时,使用ziplist(压缩列表)编码:

  • 哈希对象保存的所有键值对的键和值的字符串常都都小于64字节
  • 哈希对象保存的所有键值对数量小于512个

5. 集合对象

无序,不可重复,不可按索引定位元素

5.1 编码

集合对象的编码可以是 intset 或者 hashtable。

intset 编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合中。

hashtable 编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,这里的每个字符串对象就是一个集合中的元素,而字典的值则全部设置为 null。这里可以类比Java集合中HashSet 集合的实现,HashSet 集合是由 HashMap 来实现的,集合中的元素就是 HashMap的key,而 HashMap 的值都设为 null。

5.2 编码的转换

当集合同时满足以下两个条件时,使用 intset 编码:

  • 集合对象中所有元素都是整数
  • 集合对象保存的元素数量不超过512

6. 有序集合对象

和集合对象相比,有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。

6.1 编码

有序集合的编码可以是 ziplist 或者 skiplist。

ziplist 编码的有序集合对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。并且压缩列表内的集合元素按分值从小到大的顺序进行排列,小的放置在靠近表头的位置,大的放置在靠近表尾的位置。

skiplist 编码的有序集合对象使用 zet 结构作为底层实现,一个 zset 结构同时包含一个字典和一个跳跃表:

typedef struct zset{

     zskiplist *zsl; //跳跃表
     
     dict *dict; //字典
} zset;

字典的键保存元素的值,字典的值则保存元素的分值;跳跃表节点的 object 属性保存元素的成员,跳跃表节点的 score 属性保存元素的分值。这两种数据结构会通过指针来共享相同元素的成员和分值,所以不会产生重复成员和分值,造成内存的浪费。

说明:其实有序集合单独使用字典或跳跃表其中一种数据结构都可以实现,但这里为什么使用两种数据结构组合起来呢?原因是假如我们单独使用字典,虽然能以 O(1) 的时间复杂度查找成员的分值,但是因为字典是以无序的方式来保存集合元素,所以每次进行范围操作的时候都要进行排序;假如我们单独使用跳跃表来实现,虽然能执行范围操作,但是根据成员查找分值的操作时间复杂度是O(logN)。因此Redis使用了两种数据结构来共同实现有序集合。

6.2 编码转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

  • 保存的元素数量小于128;
  • 保存的所有元素成员的长度都小于64字节。

7. 五种数据类型的应用场景

  • 对于string 数据类型,因为string 类型是二进制安全的,可以用来存放图片,视频等内容,另外由于Redis的高性能读写功能,而string类型的value也可以是数字,可以用作计数器(INCR,DECR),比如分布式环境中统计系统的在线人数,秒杀等。
  • 对于 hash 数据类型,value 存放的是键值对,比如可以做单点登录存放用户信息。
  • 对于 list 数据类型,可以实现简单的消息队列,另外可以利用lrange命令,做基于redis的分页功能
  • 对于 set 数据类型,由于底层是字典实现的,查找元素特别快,另外set 数据类型不允许重复,利用这两个特性我们可以进行全局去重,比如在用户注册模块,判断用户名是否注册;另外就是利用交集、并集、差集等操作,可以计算共同喜好,全部的喜好,自己独有的喜好等功能。
  • 对于 zset 数据类型,有序的集合,可以做范围查找,排行榜应用,取 TOP N 操作等。

8. 参考文献

《Redis设计与实现》黄健宏

Redis详解(五)------ redis的五大数据类型实现原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值