redis知识盘点【贰】_五种类型

上篇文章:redis知识盘点【壹】_基础知识


为力争此系列文章都可以保证一定的干货率,所以有别于其他介绍redis的文章,本文将舍弃那些通篇累牍的redis指令,需要时可自行去redis中文网进行查阅。


redis主要支持字符串(String)、哈希(Map)、列表(list)、集合(sets) 和有序集合(sorted sets)五种数据类型,而redis的五种数据类型是对外的,实际内部各种类型还有两种以上自己的编码实现,根据不同的场景使用不同的编码。


可通过下面指令查看key的类型

type  key ;

此时返回的类型枚举见下表:

redis知识盘点【贰】_五种类型

其中,每种对象类型又有各自的编码实现,如下图:

redis知识盘点【贰】_五种类型


可通过下面指令查看key的字符编码
object  encoding  key ;

此时返回的编码枚举见下表:

redis知识盘点【贰】_五种类型


redis首先内部构造了一个名为redisObject的结构体,可用于表示所有类型的对象。结构体中和保存数据相关的有3个属性,分别为type(用于保存对象的类型),encoding(用于保存对象的编码)和*ptr(用于指向底层实现数据结构的指针)。具体细化到各种类型的不同实现,下面逐一进行说明。


字符串型

内部编码有如下3种:
int8个字节的长整型
embstr:小于等于39个字节的字符串
raw:大于39个字节的字符串

其中,embstr和raw都是基于redis的内部抽象类型simple dynamic string (SDS)实现的。SDS封装了int len(字符串长度),int free(buf数组中未使用字节的数量)和char buf[](字符串组)。SDS主要解决了C字符数组最后一位必须是'/0'的问题,redis是通过len来判断字符串长度,所以SDS中可以放空字符。另外,SDS在内存分配方面也做了足够的优化,当对SDS修改后,SDS的长度(len)小于1MB,则分配和len属性一样大的未使用空间;如果大于等于1MB,则分配1MB的未使用空间。当删除部分字符时,删掉的字符空间不会释放,而会继续作为未使用空间。


当对int编码的字符串追加文本字符串后,对象会自动转换为raw编码;另外由于embstr编码不支持对内容进行修改,所以对embstr编码对象执行任何修改命令后,对象都会转换为raw编码。


字符串类型的应用场景
1.存储用户信息json;
2.计数;
3.共享session;
4.限速(比如发送短信五分钟限制);

字符串get和set时间复杂度为O(1)。

getrange、mget和mset时间复杂度为O(n),n为返回字符串个数。



哈希型

内部编码2种:
ziplist(压缩列表):当哈希类型元素小于hash-max-ziplist-entries(默认512个,配置在.conf文件中,下同)时、同时所有值都小于hash-max-ziplist-value(默认64字节)时,redis使用ziplist作为内部实现。优点节省内存。
hashtable(哈希表):当哈希类型不能满足ziplist时,redis使用hashtable作为内部实现。


ziplist是redis内部实现的一个抽象模型,其结构如下:

redis知识盘点【贰】_五种类型

其中,zlbytes用于记录压缩列表占用的内存字节数;zltail用于记录压缩列表表尾节点距离起始地址有多少字节;zllen用于记录压缩列表包含的节点数量;entry为压缩列表各个节点;zlend为图书值0xFF标记压缩列表的末端。


entry的结构如下:

redis知识盘点【贰】_五种类型

其中,previous_entry_length用于记录前一个节点的长度;encoding用于记录节点的content保存数据的类型和长度;content用于保存节点的值。


当哈希型对象使用ziplist编码时,其entry依次保存元素的key和value。


hashtable抽象模型的结构如下:

redis知识盘点【贰】_五种类型


字典dict结构中,type是一个纸箱dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数;privdata保存了需要传给那些类型特定函数的可选参数;ht包含2个dictht的数组,一般使用ht[0],ht[1]位rehash时使用;trehashidx记录rehash进度,没有进行时为-1。


哈希表dictht结构中,table指针是指向一个dictEntry数组,其中每个dictEntry保存一个键值对;size保存了哈希表的大小;used记录了哈希表目前已有节点的数量;sizemask总是等于size-1,用于决定键应该被路由到table数组哪个索引上。


哈希表节点dictEntry结构中,key保存了键值对中的键;v保存了键值对中的值,可以是一个指针、uint64_t整数或int64_t整数;next用于连接另一个哈希表节点的指针,hash冲突时使用。


哈希类型的应用场景
存储用户信息,按field-value形式保存,只用一个键保存。

哈希类型hget和hset时间复杂度为O(1),hmget、hmset、hkeys和hvals时间复杂度为O(n)。


列表型

存储多个有序字符串,可以进行两边的插入(push)和弹出(pop)。

内部编码2种:
ziplist,当列表类型元素小于list-max-ziplist-entries(默认512个)时、同时所有值都小于list-max-ziplist-value(默认64字节)时,redis使用ziplist作为内部实现。优点节省内存。

linkedlist,当列表类型无法满足ziplist时,使用linkedlist。


ziplist的redis实现如上,不再赘述。


linkedlist也是redis自己实现的抽象模型,结构如下:

redis知识盘点【贰】_五种类型


其中,head是表头指针;tail是表尾指针;len是链表长度计数器;dup函数用于复制链表节点锁保存的值;free函数用于释放链表节点锁保存的值;match函数则用于对比链表节点锁保存的值和另一个输入值是否相等。


各ListNode节点之间通过prev和next指针组成双端链表。表头节点的prev指针和表尾节点的next指针都指向null,对链表的访问以null为终点。


列表类型的应用场景
1.消息队列,lpush和brpop即可实现阻塞队列。生产者客户端使用lpush在列表左侧插入元素,多个消费者使用brpop命令阻塞式抢列表尾部的元素,保证了消费的负载均衡和高可用;
2.文章列表,文章使用哈希,再放入list中。

列表命令组合使用的口诀
lpush+lpop=stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=Message Queue(消息队列)

列表lpush和rpush时间复杂度为O(1)。

linsert和lset时间复杂度为O(n)。对lset来说,n是列表长度,但是如果是列表第一个或最后一个元素,n则为1;对linsert来说,n是列表元素的位置,最坏情况是插在末尾。

lrange时间复杂度为O(s+n),s为从列表的表头或者表尾到偏移量位置的元素个数,n代表返回的元素总数。



集合型


不允许有重复元素,不能通过索引下标获取元素。一个集合最多可存储2^32-1个元素。


内部编码有2种:
intset(整数集合),当列表类型元素小于set-max-ziplist-entries(默认512个)时,redis使用intset 作为内部实现。优点节省内存。

hashtable(哈希表),当集合类型无法满足intset时。


intset为redis内部的抽象模型,结构如下:

redis知识盘点【贰】_五种类型


其中,encoding为contents数组的属性,有int16_t、int32_t和int64_4三种;length是整数集合的元素数量;contents为整数集合的底层实现,每个元素都是contents数组的一个数组项。当数组中新保存的数字超过原来的属性长度时,redis会自动升级数组属性;但是一旦升级后就不会降级。


hashtable的redis实现如上,不再赘述。当集合使用hashtable作为底层实现时,value放的是null。


集合类型的应用场景
用户标签。

集合命令组合使用的口诀
sadd = 标签
spop/srandmember = 抽签
sadd + sinter = 社交需求

集合sadd时间复杂度为O(n),n是需要添加到集合的元素总数。
sismember时间复杂度为O(1)。
smembers时间复杂度为O(n)。
sunion和sunionstore(取并集)时间复杂度为O(n),n是集合的元素总数。
sinter和sinterstore(取交集)时间复杂度为O(n*m),n是最小集合的元素总数,m是集合个数。
sdiff和sdiffstore(取差集)时间复杂度为O(n),n是所有集合的元素总数。



有序集合型


不允许有重复元素,不能通过索引下标获取元素。有序,以每个元素的score作为排序依据。


内部编码有2种:
ziplist,当列表类型元素小于zset-max-ziplist-entries(默认128个)时、同时所有值都小于zset-max-ziplist-value(默认64字节)时,redis使用ziplist作为内部实现。优点节省内存。

skiplist(跳跃表),当ziplist不满足时。


ziplist的redis实现如上,不再赘述。当有序集合使用ziplist作为底层实现时,ziplist依次保存元素的成员(member)和分数(score)。元素按分值从小到大进行排序,分值小的靠近表头。


当有序集合采用skiplist编码实现时,其实是同时使用了skiplisthashtable(字典)来实现。排序的时候读取skiplist,而查找成员时读取hashtable。具体实现时底层数据是同一套,只不过被skiplisthashtable分别引用而已。


有序集合类型的使用场景
排行榜,可以多维度。

有序集合zadd时间复杂度为O(log(n)),n是集合的元素总数。
zinterstore时间复杂度为O(n*k)+O(mlog(m)),n是最小有序集合的元素总数,k是这些求交集的有序集合总数,m是最后返回的有序集合计算结果中元素个数。
zunionstore时间复杂度为O(n)+O(M long(M)),n是所有有序集合大小总数,m是最终有序集合的元素总数。


另外虽然redis官方没有强制,但是我们一般在开发工作中约定俗成地使用冒号:作为key中各元素的分隔符。以图书业务场景为例:哈希类型用book:{id};集合类型用books:{type_id};有序集合用books:{ranking_list_type};整数类型用book:{id}:{property}。


redis知识盘点【贰】_五种类型



下一篇文章计划说一下redis的持久化。


redis知识盘点【叁】_持久化