Redis之数据结构

168 阅读5分钟

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   字节数组,用于保存字符串

优点: 

  1. O(1)获取字符串的长度

  2. 减少修改字符串时导致的内存重新分配的次数

  3. 空间预分配:每次增加字符串时会额外申请一块空间,以备下次使用。如果下次够用,则不会重新分配空间。

  4. 扩容规则:如果SDS的len属性<1M,会给free分配相同的空间大小;如果>1M,则额外给free分配1M的空间大小

  5. 惰性空间回收:字符串缩短时,不会将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的过程:

  1. 为ht[1]分配空间。具体根据是扩容还是缩容采用不同的分配策略。

  2. 扩容:将ht[1]扩容为大于ht[0].used*2的最小的一个2的n次方幂

  3. 缩容:将ht[1]缩容为大于ht[0].used的最小的一个2的n次方幂

  4. 执行rehash。将ht[0]中的键值对rehash到新的ht[1]中。

  5. 交换引用。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位的。以此完成升级操作。