5种基础数据结构
string
字符串类型,通过一个key指向一个value。高级的用法,通过SET k v EX seconds NX实现分布式锁。底层数据结构:int, sds_raw, sds_embstr
list
相当于java中的链表,两端都可以push或pop,可以实现简单消息队列的功能,如果生产者用lpush发送消息,消费者用rpop消费消息。也可以实现栈的功能,lpush, lpop结合使用。底层数据结构:quicklist
set
类似于java中HashSet,存储的数据无序的、唯一的,set之间可以做并集,交集和差集操作。可以用来去重。底层数据结构:inset,dict
zset
完整的名称是SortedSet,就是有序集合,既有结合的数据唯一性,又具有有序性。每个成员在添加时需要制定一个浮点型分数,成员按分数从小到大排序。底层数据结构:ziplist, (skiplist, dict)
hash
相当于java的HashMap,存储键值对,可以用来存一个对象。底层数据结构:ziplist,dict
高级数据结构
Bitmap
位图,最多容纳2^32个bit,可以用来存储一个用户的在线状态,从该用户访问系统的第一天起,在线的那一天就可以设置为1;也可以统计日活,将用户id作为下标,如果在线了就置为1。
Geohash
将经纬度映射为一个hash值,这个值就是geohash,然后可以根据geohash计算距离,可以用来做附近搜索等。
HyperLogLog
可以以极低的空间成本统计系统的日活、月活。只需要12K内存,在标准误差0.81%的前提下,能够统计2^64个数据。HyperLogLog将数据hash成64位整数,用前14个bit做为桶的index,也就是总共有2^14个桶,剩下的50位找maxbit,所以需要6个bit存储maxbit。
Bloomfilter
将数据做n次hash操作,映射到字节数组中,判断一条数据是否已存在与BloomFilter,需要同样的过程,如果n个hash得到的index在字节数组中都为1,认为这条数据存在BloomFilter中。BloomFilter存在假阳性,假阳性率也就是误差率可以计算。
内部数据结构
dict
用在database的key,value存储;在hash中的field较多是也会用dict存储。主要操作:dictFind,dictAdd(在key不存在时添加,否则不添加),dictReplace(在key不存在时添加,在key存在时更新value),reHash过程。
sds:simple dynamic string
支持不同长度范围的字符串,也就是多个sds_header,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64,除sdshdr5之外,其他的header都由3个字段构成,len:字符串的实际长度,alloc:字符串容量,flags:总是占用一个字节,低三位标识当前的header类型。sdshdr5只有一个字节的,低三位标识header类型,高5位标识字符串长度,没有alloc字段,最多容纳2^5-1个字符。
robj: redisObject
redis的database使用dict来存储数据,key都是string类型,而value可以有多中数据类型,为了在value中将数据类型统一,引入了redisObject类型,robj的字段如下:
- type: 对象的数据类型。占4个bit。可能的取值有5种:OBJ_STRING, OBJ_LIST, OBJ_SET, OBJ_ZSET, OBJ_HASH,分别对应Redis对外暴露的5种数据结构(即我们在第一篇文章中提到的第一个层面的5种数据结构)。
- encoding: 对象的内部表示方式(也可以称为编码)。占4个bit。可能的取值有10种,即前面代码中的10个OBJ_ENCODING_XXX常量。
- lru: 做LRU替换算法用,占24个bit。这个不是我们这里讨论的重点,暂时忽略。
- refcount: 引用计数。它允许robj对象在某些情况下被共享。
- ptr: 数据指针。指向真正的数据。比如,一个代表string的robj,它的ptr可能指向一个sds结构;一个代表list的robj,它的ptr可能指向一个quicklist。
由此可见robj也是对外暴露的数据结构与内部数据结构的一个桥梁,允许外部暴露的数据结构在内部可以有多种存储类型。
ziplist
经过特殊编码的双向表,ziplist将表中的每一项存放在连续的地址空间,一个ziplist整体占用一大块内存。ziplist的内存结构如下:
<zlbytes><zltail><zllen><entry>...<entry><zlend>
各个部分在内存上是前后相邻的,它们分别的含义如下:
- zlbytes: 32bit,表示ziplist占用的字节总数(也包括zlbytes本身占用的4个字节)。
- zltail: 32bit,表示ziplist表中最后一项(entry)在ziplist中的偏移字节数。zltail的存在,使得我们可以很方便地找到最后一项(不用遍历整个ziplist),从而可以在ziplist尾端快速地执行push或pop操作。
- zllen: 16bit,表示ziplist中数据项(entry)的个数。zllen字段因为只有16bit,所以可以表达的最大值为2^16-1。这里需要特别注意的是,如果ziplist中数据项个数超过了16bit能表达的最大值,ziplist仍然可以来表示。那怎么表示呢?这里做了这样的规定:如果zllen小于等于2^16-2(也就是不等于2^16-1),那么zllen就表示ziplist中数据项的个数;否则,也就是zllen等于16bit全为1的情况,那么zllen就不表示数据项个数了,这时候要想知道ziplist中数据项总数,那么必须对ziplist从头到尾遍历各个数据项,才能计数出来。
- entry: 表示真正存放数据的数据项,长度不定。一个数据项(entry)也有它自己的内部结构,这个稍后再解释。
- zlend: ziplist最后1个字节,是一个结束标记,值固定等于255。 每一个数据项entry的构成如下:
<prevrawlen><len><data>
我们看到在真正的数据(<data>)前面,还有两个字段:
-
<prevrawlen>: 表示前一个数据项占用的总字节数。这个字段的用处是为了让ziplist能够从后向前遍历(从后一项的位置,只需向前偏移prevrawlen个字节,就找到了前一项)。这个字段采用变长编码。
- 如果前一个数据项占用字节数小于254,那么<prevrawlen>就只用一个字节来表示,这个字节的值就是前一个数据项的占用字节数。
- 如果前一个数据项占用字节数大于等于254,那<prevrawlen>就用5个字节来表示,其中第1个字节的值是254(作为这种情况的一个标记),而后面4个字节组成一个整型值,来真正存储前一个数据项的占用字节数。
-
<len>: 表示当前数据项的编码类型和数据长度(即部分的长度),总共有9种情况,也采用变长编码。
hash在field较少(小于512个)且value较小(小于64字节)时,使用ziplist存储,key和value在ziplist是相邻的两项。
quicklist
双向链表,链表中的每个节点是ziplist或者是ziplist的压缩数据。是list的内部实现。
skiplist
注意:图中前向指针上面括号中的数字,表示对应的span的值。即当前指针跨越了多少个节点,这个计数不包括指针的起点节点,但包括指针的终点节点。
假设我们在这个skiplist中查找score=89.0的元素(即Bob的成绩数据),在查找路径中,我们会跨域图中标红的指针,这些指针上面的span值累加起来,就得到了Bob的排名(2+2+1)-1=4(减1是因为rank值以0起始)。需要注意这里算的是从小到大的排名,而如果要算从大到小的排名,只需要用skiplist长度减去查找路径上的span累加值,即6-(2+2+1)=1。
可见,在查找skiplist的过程中,通过累加span值的方式,我们就能很容易算出排名。相反,如果指定排名来查找数据(类似zrange和zrevrange那样),也可以不断累加span并时刻保持累加值不超过指定的排名,通过这种方式就能得到一条O(log n)的查找路径。
redis中sorted set的实现:
- 当数据较少时,sorted set是由一个ziplist来实现的。
- 当数据多的时候,sorted set是由一个叫zset的数据结构来实现的,这个zset包含一个dict + 一个skiplist。dict用来查询数据到分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找)。
intset
用来做set的底层数据结构,当set中的值是64位以内的整数,且数量较少是,用intset存储,否则用dict存储。intset类似于ziplist,只不过intset只能存储整数。结构如下:
<encoding><length><contents>
- encoding: 数据编码,表示intset中的每个数据元素用几个字节来存储。它有三种可能的取值:INTSET_ENC_INT16表示每个元素用2个字节存储,INTSET_ENC_INT32表示每个元素用4个字节存储,INTSET_ENC_INT64表示每个元素用8个字节存储。因此,intset中存储的整数最多只能占用64bit。
- length: 表示intset中的元素个数。encoding和length两个字段构成了intset的头部(header)。
- contents: 是一个柔性数组(flexible array member),表示intset的header后面紧跟着数据元素。这个数组的总长度(即总字节数)等于encoding * length。柔性数组在Redis的很多数据结构的定义中都出现过(例如sds, quicklist, skiplist),用于表达一个偏移量。contents需要单独为其分配空间,这部分内存不包含在intset结构当中。