api层面上,redis为我们提供了5种数据结构:string,hash,list,set,sorted set。除了会使用这5种数据结构,我们还需要知道他们的底层实现,这样我们在使用这些api的过程中,才能清楚他们的时间复杂度。
string
redis没有使用c的字符串,而是自己实现了一种字符串结构,称为simple dynamic string(SDS)。
SDS包含三部分:
- int len 表示当前字符串的长度
- int free 表示剩余可使用空间的长度
- char[] buf 字节数组,用于保存字符串
优点:
-
O(1)获取字符串的长度
-
减少修改字符串时导致的内存重新分配的次数
-
空间预分配:每次增加字符串时会额外申请一块空间,以备下次使用。如果下次够用,则不会重新分配空间。
-
扩容规则:如果SDS的len属性<1M,会给free分配相同的空间大小;如果>1M,则额外给free分配1M的空间大小
-
惰性空间回收:字符串缩短时,不会将buf多余空间立即回收,而是记录到free上,以备将来使用。
list
redis中,list是使用双向链表来实现的(之一)。
其中,node结构包含:
- Node prev
- Node next
- V value
list结构包含
- Node head
- Node tail
- int len
由此可见,redis的list是由一个记录了头节点和尾节点以及链表长度的双向链表实现的。
下边我们来分析下常见的list的api的时间复杂度:
- lpush rpush O(1)
- lpushx rpushx O(1) 当且仅当key存在并且是一个list类型时才执行操作
- lpop rpop O(1)
- blpop brpop O(1) 阻塞式pop
- llen O(1)
- lrange O(s+n) s是偏移量,n是制定区间的大小
- lrem O(n) 移除愿随
- lset key index value O(n) set元素
- linsert key before O(n) insert元素
- ...
hash
hash底层使用字典来实现的,redis本身的key value存储也是用字典来实现的。
我们来看下字典的数据结构
首先是hash表节点:Node
- key
- value
- next 解决hash冲突
然后是hash表:HashTable
- Node[] table
- int size //table的长度
- int used //已有键值对的数量
最后是字典表:Dist
- HashTable[] ht; //长度为2,一般情况下只是使用ht[0],rehash时会使用ht[0]和ht[1]
- int rehashIndex; //rehash时代表当前rehash的进度;没有rehash时,值为-1
分析:由上边结果可知。redis的hash实现与java的hashMap结果基本一致。而redis的字典表是由两个这样的hash表实现的。一般情况下,redis只会使用ht[0],ht[1]只有在rehash的过程中才会使用。
rehash的过程:
-
为ht[1]分配空间。具体根据是扩容还是缩容采用不同的分配策略。
-
扩容:将ht[1]扩容为大于ht[0].used*2的最小的一个2的n次方幂
-
缩容:将ht[1]缩容为大于ht[0].used的最小的一个2的n次方幂
-
执行rehash。将ht[0]中的键值对rehash到新的ht[1]中。
-
交换引用。ht[0]=ht[1]; ht[1]=null;
rehash过程的第二步采用的渐进式rehash。即不是一次性的将ht[0]全部rehash到ht[1]中。而是将这个过程分配到多次对hash表的访问中。reHashIndex从0开始,每次将对应的桶rehash到ht[1]中,然后rehashIndex++。直到rehash最后一个桶结束,将rehashIndex=-1.
由上,在整个渐进式rehash过程中,对字典的delete,get,update等操作是同时操作ht[0]和ht[1]的,但是,对字典的insert只操作ht[1].
sorted set
sorted set是有序的集合,它在redis中是通过跳跃表来实现的。跳跃表是一种有序的数据结构,它通过每个节点维护多层指向其他节点的指针来实现。一般层高越高,跨度越大。通过跳跃表,可以在平均O(logN),最坏O(N)的时间复杂度内查找节点元素。
redis中跳跃表的数据结构如下:
- header
- tail
- level:最大层数,也即层数最大的节点的层数。注意:同一个跳跃表中的不同节点位于不同的层级上,一般来讲,层级越高,节点数越少,跨度越大。
- length:节点数量
跳跃表节点的数据结构:
-
Level[] level:
-
每一个level元素包含一个指向下一节点的指针以及相应的跨度。
-
backword:后退指针,用于跳跃表从表尾向表头遍历
-
score
-
value
跳跃表的查询逻辑:从头节点H开始,从高层开始,查找下一个节点A。如果下一个节点A比要查找的节点小,表示没有跳跃过度,则继续查找节点A高层的下一个节点;如果节点A比要查找的节点大,表示跳跃过度,则继续查找头节点H的稍低一层的下一个节点。以此类推。如图:

set
redis中的set,当元素全部是整数且元素数量比较小时,采用intset实现;否则用hashTable的key来实现。
intset顾名思义就是一个整数的集合。整数元素全部存储在一个int数组中,按照升序排列,方便查找。
intset的升级
intset可以存储16,32,64位的整数。假如,一个intset开始存储的都是16位的整数,长度为4,则其此时的空间大小为:164=64;后来,要插入一个32位的整数,则需要对这个intset进行升级,将数组大小扩展为325=160,同时将原先的4个16位整数变为32位的。以此完成升级操作。