专业术语
sds:simple dynamic string 简单动态字符串,redis自己开发的一个字符串的抽象类型
embstr:embedded sds string embstr编码的SDS,与SDS区别在于内存仅需要申请一次,而SDS需要申请两次。适用于短字符串,优点是效率高
Redis 对象结构
Redis 五种对象类型
redis对象数据结构如图所示:
每一个redis对象都用一个key进行存储,在redis对象中有对象类型和编码,对象类型和编码的对应关系如下:
string 字符串类型
应用场景:
5中类型中最简单、常用用的类型,扩展性是非常高得
- key是string,value可以是json,常规key-value缓存
- 可以用于一些计数器,比如文章点赞数
编码选择:
REDIS_ENCODING_INT
:如果value是long类型的整数,则使用此编码
REDIS_ENCODING_RAW
:专门保存长字符串的编码,如果value长度>=40
(redis-3.3),则使用的是raw编码进行存储。
- 不足:raw需要调用两次的内存分配函数来分别创建
redisObject
和sdshdr
REDIS_ENCODING_EMBSTR
:专门保存短字符串的编码,如果value长度<40
,则使用embstr编码,属于紧凑型的数据结构
- 优点:只需要一次内存分配,数据比较小的时候使用的是这种编码,数据更紧凑,效率更高
- 缺点:没有提供修改的函数,所以是只读的,如果要对此编码数据进行修改,会变成raw再执行修改,然后结束
底层数据结构:
string的int编码:
在ptr指针中直接指向long类型整数
string的raw编码
ptr指向一个redis自定义的string类型->sdshdr,然后在buf缓冲中指向一个字符数组
string的embstr编码
于raw类似,只不过在所有属性都和RedisObject在一块连续的内存空间上,所以它只需要进行一次的内存申请,也代表着它很快!
string 相关命令
命令行 | 含义 |
---|---|
set key value | 赋值key的值为value |
get key | 获取key的value值 |
del key | 删除key |
expire key seconds | 设置key在seconds秒后过期 |
setex key seconds value | 赋值key的值为value,在seconds秒后过期 |
ttl key | 查看key还有多久过期 |
setnx key value | 如果key不存在,才新增key和value |
strlen key | 计算指定key的值的长度 |
incr key | 加1 |
incrby key numbers | 指定增加值,numbers可以是负值 |
mset key1 value1 key2 value2 … | 批量添加 |
mget key1 key2 key3 … | 批量获取 |
list 列表类型
应用场景:
- 各种列表, twitter的关注列表、粉丝列表等,最新消息排行
- 消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出执行。
编码选择:
当列表对象同时满足以下两个条件时,列表对象使用ziplist进行存储,否则使用linkedlist存储
- 列表对象保存的所有字符串元素的长度小于64字节
- 列表对象保存的元素数量小于512个
list的zpilist编码
ptr指向一个压缩列表,所有的压缩列表都类似数据结构
list的linkedlist编码
双向无循环的链表,无循环代表头和尾都指向NULL,不会形成一个循环,优点和java的链表一样,插入快但索引慢
list 相关命令
命令行 | 含义 |
---|---|
lpush key value1 value2 | 左侧插入value |
rpush key value1 value2 | 右侧插入value |
lpop key | 左侧弹出value |
rpop key | 右侧弹出value |
llen key | 查看key的长度 |
lindex key index | 查看列表中某个index对应的value值 |
setnx key value | 如果key不存在,才新增key和value |
lrange key startIndex endIndex | 查看指定元素,下标从0开始,-1为倒数第一个 |
ltrim key startIndex endIndex | 让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除,下标同上 |
set 集合类型
应用场景:
- 增加的时候可以保证去重,比如文章中用户每次点赞的时候不允许重复
编码选择:
同时符合以下两种情况时,使用的是intset编码,否则采用hashtable编码
- 集合对象保存的所有元素都是整数值
- 集合对象保存的所有元素<=512个
set的intset编码(intset底层为整形集合实现)
intset编码与raw类似,只不过将字符数组换成了整形数组
set的hashtable编码(底层是字典,每个键都是字符串对象。字典对应的值为NULL)
set集合类型下使用了hashtable,那么它指向的value保存什么呢?
看图中可以看出,所有的数据都指向了NULL
set 相关命令
命令行 | 含义 |
---|---|
sadd key value1 value2 | 添加元素到集合中 |
smembers key | 查看集合中的所有元素 |
sismember key value | 查看value是否在集合中 |
scard key | 查询集合的长度 |
spop key | 取出集合中的一个元素 |
del key | 删除集合 |
zset 有序集合类型
应用场景:
一些需要对存放的数据进行排序,比如正序、倒序;不允许重复的成员,后面的数据会覆盖前面的数据
- 根据时间排序的新闻列表
- csdn的访问量排行榜
编码选择:
当zset满足以下两个条件的时候,使用ziplist,否则使用skiplist
- 保存的元素个数小于128个
- 保存的所有元素大小都小于64字节
zset的ziplist编码
一样使用ziplist压缩列表,在中间数据的部分保存的时key-value,连续存储的结构
zset的skiplist编码
typedef struct zset{
// 跳表
zskiplist *zsl;
// 字典
dict *dice;
} zset;
使用字典+跳表的方式,将字典和跳表两种数据结构组合
其中跳表通过在每个节点中(基于层和跨度等)维持多个指向其它节点的指针来实现快速访问
更多参考:Redis 源码分析(七) :skiplist
zset有序集合单独使用字典或跳表中的一种数据结构就可以实现,那么为什么要使用两者的组合呢?
- 字典的优势在于如果要查找成员的分值,时间复杂度是O(1),但是字典是以无序的方式保存集合元素,所以每次范围操作的时候需要进行排序
- 跳表的优势在于如果要执行范围操作,不需要进行排序,可以直接获取,但是跳表如果是进行查找操作,则时间复杂度为O(logN)
- 因此Redis使用两种数据结构的组合,扬长避短的方式来共同实现有序集合
zset有序集合使用两种数据结构会不会造成数据冗余?
不会,两种数据结构会通过指针来共享相同数据元素的成员和分值,所以不会产生重复的成员和分值而造成内存的浪费。
zset 相关命令
命令行 | 含义 |
---|---|
zadd key value1 score1 value2 score2 | 添加元素到有序集合中 |
zscore key value | 查看key的score值,输出score>=负无穷,score<=正无穷的所有元素 |
zrange key 0 -1 | 正序输出 |
zrangebyscore key -inf +inf | 正序输出 |
zrevrange key 0 -1 | 倒序输出 |
zcard key | 查看key中的元素个数 |
zrangebyscore key indexStart endStart | 获得key中score>=indexStart 且score<=endStart的元素,正序排列 |
zrevrangebyscore key indexStart endStart | 同上,倒序排列 |
zrem key value | 删除key中的元素value |
hash 字典类型
应用场景:
- 存放结构化数据,比如user对象,修改时不需要反序列化可以直接修改
编码选择:
同时满足以下两种情况,则是zpilist,否则就是hashtable
3. hash里面的键值对中,key和value的长度全都小于46个字节
4. hash里面的键值对中,个数小于512个
hash的ziplist编码
zpilist压缩链表,数据部分存储的是一个对象的多个属性名和属性值,修改就是直接对所在数据位置修改,而不需要进行反序列化,因为它们是结构化存储,而不是像json一样当作一个string来存储
hash的hashtable编码
使用字典的方式存储属性名和属性值,修改也是直接对数据进行修改,不需要反序列化操作
hash 相关命令
命令行 | 含义 |
---|---|
hset key name value | 添加属性元素name和value到key中 |
hget key name | 查看key的name值 |
hmset key name1 value1 name2 value2 | 批量添加key的属性元素 |
hmget key name1 name2 | 批量获取key的属性元素 |
hlen key | 获得key的属性元素个数 |
hgetall key | 查询key中的所有元素 |
思考
为什么redis要自己开发一个字符串类型?
主要有如下几种原因:
- C中字符串中没有长度数据,每次都需要进行计算,使用一个元素去遍历,直到\0结束符号,复杂度为
O(n)
而在SDS中的复杂度是O(1)
,更快 - 缓冲区溢出:C字符串是存在缓存冲溢出的情况,前提是在增加元素之前没有进行足够的内存分配
而SDS动态的执行空间的扩展,API会自动的进行空间的扩展, - 在C字符串中,内存的重新分配是很耗费性能的
- 空间预分配:
- 如果对SDS进行修改后,SDS的长度<1M,那么这个时候,预分配的len=free
- 如果对SDS进行修改后,SDS的长度>=1M,只会按照1M去预分配
- 惰性释放:
- 在删除字符串的时候,不及时释放,还继续保留空间,下次使用的时候就不需要再进行申请了
- 空间预分配:
- C字符串中字符串是以
\0
结尾的,并且字符串不能包含空字符串,因此C字符串使用范围较小
而在SDS中,使用len来判断数据长度,可以保存任意的数据。
总结
文章以图解的方式介绍了Redis中的五种数据类型和8中数据编码,并对其应用场景进行分析举例,相信大家看完对Redis的底层有了一定的了解
8种编码encoding对比总结:
数据编码 | 描述 | 优点 | 缺点 |
---|---|---|---|
REDIS_ENCODING_INT | long类型的整数 | 占用内存少、速度快 | 仅仅针对long类型整数 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 | 一次内存申请与释放 | 不适用于长的字符串 |
REDIS_ENCODING_RAW | 简单动态字符串 | 适用于长的字符串存储 | 相对于embstr,需要两次内存申请与释放 |
REDIS_ENCODING_HT | 字典 | 读写时间复杂度O(1) | 占用内存较多 |
REDIS_ENCODING_LINKEDLIST | 双向链表 | 元素的个数较多时,访问元素的时间比压缩列表更快 | 每个节点都维护了前置指针、后置指针,占用更多内存且内存不连续容易产生内存碎片 |
REDIS_ENCODING_ZIPLIST | 压缩列表 | 空间连续,占用内存少,速度快 | 元素数量大时,访问元素的时间较长 |
REDIS_ENCODING_INTSET | 整数集合 | 占用内存远小于hashtable | 仅仅存整数 |
REDIS_ENCODING_SKIPLIST | 跳表+字典 | 不满足ziplist可以使用skiplist来作为内部实现 |
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/17848.html