二、Redis的五种常用数据类型
在开篇之前,我先来介绍一下写这篇文章的意图,这是我学习过程中的一些思考,也是引领我一直探索下去的兴趣所在。
首先,对于Redis五种常用的数据类型,大部分人都耳熟能详;这几种数据类型的操作指令,随口都可以说出一大堆,但是这些数据类型的存储组织方式,如果被人问到能说出个所以然来吗?
有点编程基础的人可能能猜到一二,string不就是普通字符串,list要么是数组(并不是),要么是链表,set、zset、hash底层都是通过hashtable实现的嘛。但是事实真的那么简单吗?没有仔细学习过的人可能对自己说的话心里都犯嘀咕。
就算学习过常用数据类型的底层存储结构,不同数据结构之间的转换条件,这些数据结构都用来存储哪些类型的数据,又能说得清楚吗?不经过整理是说不清楚的。
本着以上几点学习疑问,我对Redis常用数据类型底层的数据结构进行了整理,通过这一节大家可以系统地学习Redis常用数据类型的存储方式。
1. 前言
不捋一下脉络,可能大家在读到这篇文章时会有些疑惑,因为我会先说字符串,再讲string、list等数据类型,大家看到既出现字符串又出现string可能会一脸懵,两者有什么不同吗?
我们知道,Redis只能保存字符串,不支持保存二进制数据,所以字符串是所有key和value的存在形式。而string、list等数据类型是以对象的形式存在的,除了key和value是以字符串形式存在外,还包含很多其他属性。
2. 字符串
Redis的字符串并没有直接采用C字符串,而是自己实现了个SDS结构(simple dynamic string,简单动态字符串)进行存储。
SDS存储结构.png
len
表示保存的字符串的长度,不包含空字符'\0'。
free
表示buf数组中未使用的字节数,len+free+1
即为字节数组的长度。
buf
字节数组存储字符串内容,最后以空字符'\0'结尾
SDS与C字符串都以'\0'结尾,这种实现使得SDS可以共用某些C字符串的函数,而无需另外实现。但是SDS相比C字符串有很多优化的地方:
- C字符串通过遍历的方式获取字符串长度,时间复杂度为O(n);而SDS本身使用
len
属性保存了字符串长度,获取字符串长度的时间复杂度为O(1)。 - 拼接字符串时,C字符串是直接拼接到紧邻的内存中,可能会覆盖掉内存中原有的内容;而SDS在字符串拼接前会先判断剩余空间
free
是否满足需求,不满足则重新分配内存进行扩容。 - 由于C字符串不记录长度,所以每次字符串修改时都需要重新分配内存;而SDS采用空间预分配和惰性释放策略,减少内存重分配次数。
SDS在缩短字符串长度时,并不会马上释放空间,而是留作以后SDS增长时使用。SDS也提供了相应的API来释放空闲空间,必要时可以调用API释放内存空间。
3. Redis对象 Redis使用对象来表示数据库中的键和值,其中键对象一般为字符串对象,值对象则可以指向不同类型的数据。
Redis对象.png
type
记录了对象类型,可以取值为REDIS_STRING
、REDIS_LIST
、REDIS_HASH
、REDIS_SET
、REDIS_ZSET
,分别表示string、list、hash、set、zset对象。encoding
表示对象底层的编码和存储方式,取值和对象的对应关系如下表所示:编码常量 | 底层数据结构 |
---|---|
REDIS_ENCODING_INT | long类型的整数 |
REDIS_ENCODING_EMBSTR | embstr编码的动态字符串(SDS) |
REDIS_ENCODING_RAW | 简单动态字符串(SDS) |
REDIS_ENCODING_HT | 字典 |
REDIS_ENCODING_QUICKLIST | 快表 |
REDIS_ENCODING_ZIPLIST | 压缩列表 |
REDIS_ENCODING_INTSET | 整数集合 |
REDIS_ENCODING_SKIPLIST | 跳跃表+字典 |
ptr
是指向具体内存数据结构的指针,由于Redis有多种数据类型,每种数据类型都有多种数据结构可以实现,因此ptr
用void来修饰。4. string对象 string是Redis中最简单的数据类型,它以键值对的方式存储,我们可以通过
set [key] [value]
命令添加一个string对象,也可以通过del [key]
命令来删除一个string对象(当然,del
命令对所有类型的key都适用)。当string保存的是一个整数值,并可以用long型存储时,那么string对象会将整数值保存在
ptr
里面,将void*
转换成long型,编码为int;当整数值太大无法用long型存储时,会将整数值转换成字符串,用SDS来存储,编码为embstr或者raw。String存储整数值.png
当string对象保存的是一个字符串,并且字符串长度小于等于39字节时,就会用embstr编码来保存字符串值。此时SDS与字符串redisObject紧邻在一起,只需申请一次内存。
embstr.png
当字符串内容长度大于39字节时,会用raw编码来保存字符串,申请内存时需申请两次,一次用来存储redisObject,另一次用于存储SDS结构。释放内存也需要两次释放。
raw.png
另外需要注意的是,string保存浮点数时用embstr或者raw编码保存,当执行数值运算的时候,会转换回浮点型进行运算,运算完之后再转为字符串形式保存。
5. list对象 在Redis中,列表对象可用
lpush
或者rpush
命令来创建,并往里面添加元素。也可通过lpop
或者rpop
命令来弹出列表对象里面的元素。【二、Redis的五种常用数据类型】考虑到链表的附加空间相对太高,对于64位操作系统来说,prev 和 next 指针就要占去 16 个字节 ;并且链表的每个节点都单独分配,内存碎片化程度较高,list对象在底层使用quicklist(快速列表)来存储。quicklist是由一个个ziplist组成的链表,每个ziplist连续地存放多个list元素,ziplist中的每个entry块存放一个list元素。相对链表实现来说,quicklist可以大大节省prev 和 next 指针的占用空间。
List对象.png
quicklist内部每个ziplist大小默认为8k,超过这个大小就会另起一个ziplist,封装成quicklistNode添加到quicklist尾部。单个ziplist的大小可通过参数配置。
# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb<-- not recommended for normal workloads
# -4: max size: 32 Kb<-- not recommended
# -3: max size: 16 Kb<-- probably not recommended
# -2: max size: 8 Kb<-- good
# -1: max size: 4 Kb<-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2
quicklistNode中的ziplist可用压缩的方式存储,压缩深度通过参数
list-compress-depth
来配置,默认为0(不压缩)。如果设置为1,则首尾两个ziplist不压缩,其他都压缩。不压缩时元素存取速度较快,省去了压缩和解压的开销,但是占用的空间会比较多。6. hash对象 我们可以采用Redis的hash数据类型来存储value包含多个键值对的数据,如保存包含多个属性的用户信息。我们可以通过
hset [key] [field] [value]
命令来创建hash对象并往里面添加元素。当元素个数较少,并且每个元素较短时,hash对象底层通过ziplist来存储,field和value存储在两个相邻的entry中。
Hash对象ziplist.png
当元素个数较多,或者存在较长的元素时,hash对象会转为dict(字典)存储。临界点的元素个数和元素长度可由参数
hash-max-ziplist-entries
和hash-max-ziplist-value
进行配置,默认值分别为512和64.
Hash对象dict.png
7. set对象 Redis中的set数据结构可以去重地保存元素,我们可以使用
sadd
指令往set里添加元素,也可以通过scard
指令统计集合元素数量。默认情况下,当集合元素全为能用long声明的整数值,并且元素个素不大于512个(可通过参数
set-max-intset-entries
配置)时,集合数据通过intset来存储,intset中的元素按从小到大的顺序排列。其他情况下,集合数据一律使用dict(字典)来存储,字典的键为集合元素的value,字典的值为null。Set.png
8. zset对象 与set对象一样,zset对象也可以去重地保存元素,zset中的元素还可以按照score的顺序排列,score相同的情况下按照value的大小排序。
默认情况下,当zset中的元素个数不大于128(通过参数
zset-max-ziplist-entries
控制),并且所有元素的长度都不大于64字节(通过参数zset-max-ziplist-value
控制)时,zset底层采用ziplist来存储,相邻的两个entry分别表示value和score。如果两个条件有一个不满足,都采用hashtable+skiplist的形式存储,其中hashtable存储value和score的映射关系,skiplist按顺序存储value。ZSet对象.png
之所以采用hashtable+skiplist的形式存储zset,是因为这两种结构都各有好处。首先,zset是按照score排序的,skiplist的排序特性刚好满足这种需求;然后,hashtable保存了value和score的映射关系,可以直接通过value来获取score,时间复杂度为O(1)。假如没有使用hashtable,获取score就得在skiplist中逐层下沉地搜索,时间复杂度为O(lg n)。虽然同时使用hashtable和skiplist来保存zset,但是他们共用同一个value和score,因此不会造成空间浪费。
至于为啥zset使用skiplist实现排序功能,而不用红黑树呢?我总结出来主要有两点:
- skiplist实现比红黑树实现起来更简单,代码更容易维护;
- skiplist区间查找效率更高,跳表可以做到O(lg n) 的时间复杂度定位区间的起点,然后再顺序往后遍历就可以了。
读者如果有什么疑问或者建议可以在下方留言,我将会在看到的第一时间给予解答或者完善文章,提升自己也惠及他人
推荐阅读
- EffectiveObjective-C2.0|EffectiveObjective-C2.0 笔记 - 第二部分
- 一个人的碎碎念
- 野营记-第五章|野营记-第五章 讨伐梦魇兽
- 遇到一哭二闹三打滚的孩子,怎么办┃山伯教育
- Shell-Bash变量与运算符
- 赢在人生六项精进二阶Day3复盘
- 清明,是追思、是传承、是感恩。
- 2019年12月24日
- 陇上秋二|陇上秋二 罗敷媚
- 牛人进化+|牛人进化+ 按自己的意愿过一生