如果谈到Redis高效的数据结构,相信小伙伴们一定会脱口而出 String(字符串)、List(列表)、Hash(哈希)、Set(集合)和Sorted Set(有序集合)。但是准确的说这些并不是数据结构,而是数据的保存形式。我想要谈论的是这些数据形式背后的底层实现。
一共有8种 Simple Dynamic String(简单动态字符串)、LinkedList (双向链表)、ZipList(压缩列表)、Dict(哈希表)、SkipList(跳表)、IntArray(整数数组)、DictTree(字典树)、 QuickList(快速列表)
| 数据形式 | 底层实现 |
|---|---|
| String | SDS |
| List | LinkedList ZipList 新版本QuickList |
| Hash | Dict ZipList |
| Sorted Set | SkipList ZipList DictTree |
| Set | Dict IntArray |
知道了底层实现的基本概念,我们再来谈论一下键和值的结构组织,实现从键到值的快速访问。
这里学过Java,JDK7的HashMap的同学可以类比。在Redis里称为全局哈希表,由一个数组,数组的每个元素是一个哈希桶链表。这里需要注意的是哈希桶的entry元素里保存的是key和value指针
由此因此哈希表的O(1)复杂度和快速查找的特性,Redis非常快。
除此之外,依旧存在两个风险。
- 随着数据的增加,哈希表的冲突加剧。 Redis 采用的方法和JDK7一样拉链法,对于哈希冲突的entry用链表连接,当某一个链表恰巧元素特别多的时候又会引发新的性能问题,属于该哈希桶的key查询效率下降,故而需要对数据扩容rehash元素
- rehash可能带来阻塞 rehash这步和JDK的实现就有所不同了。JDK的rehash是阻塞的,简单保证数据的正确。 Redis的rehash采用了一种渐进式的思路。rehash的过程分为两个部分,其中之一是Redis后台会用一个线程来进行复制元素到新链表,另一部分在这个期间Redis处理的请求会发生在之前的旧链表中,如果该哈希桶的元素还没被后台线程负责,则在处理的同时处理。巧妙地吧一次性大量的拷贝开销分担到多次请求中。
三种特殊的数据结构
压缩列表
表头zlbytes、zltail、zllen 以及表尾 zlend 表示表长度、列表尾的偏移量、列表中的entry个数、列表结束,通过表头三个字段可以O(1)的遍历第一个元素和最后一个元素以及长度,其他操作还是O(N)复杂度。
跳表
在链表的基础上增加了多级索引,通过多级索引位置的跳转快速定位数据。复杂度O(logN)
快读列表
QuickList 是 ZipList 和 LinkedList 的混合体。数据分段保存,每一个段都被保存为一个ZipList,段和段之间使用LinkedList连接。除了头节点和尾节点,节点数据还进行了压缩 LZF 算法压缩。
这样设计很明显是为了节省内存空间,而头尾节点不压缩的原因则是因为解压缩需要开销CPU耗时,会使得pop push操作变慢。