redis写在了简历里,所以基础的东西还是要会了。
redis有五种基础数据结构:string、hash、list、set、zet。具体设计和为啥要这样设计呢?
虽然redis是C语言写的,但是并没有直接用C语言的字符数组来当自己的string。而是自己设计了一种SDS(Simple Dynamic String)的类型。
SDS定义如下
struct sdshdr {
int len;//len:存储字符串的实际长度
int free;//free:存储剩余(空闲)的空间
char buf[];//buf[]:存储实际数据
};
SDS不仅定义字段和C语言字符数组不同,处理空字符’\0’也不同。SDS结尾也以空字符结尾,但是不计算字符串长度,SDS添加空字符到尾部操作是redis自动完成的,对程序员透明。遵循这个的好处是可以直接调用部分C语言的函数库。
SDS结构也包含了字节数组,但是不同的是,新增了两个字段。因此有些与C语言写的字符数组不同的效果。
求字符串长度是常数级。SDS的len字段让时间复杂度降低成了O(1)。C语言的字符数组不记录长度第一个问题,导致长度只能遍历,时间复杂度是O(N);
杜绝缓冲区溢出。free字段记录剩余的空间,拼接操作发现空间够,那就放心大胆做。C语言字符数组不记录长度导致的第二个问题就是缓冲区溢出,一旦我们调用了拼接函数,而内存不够,就会产生缓冲区溢出的情况;
二进制安全。SDS的len字段可以指示具体数据长度判断字符串是否结束。C语言当读取到“\0”,就认为已经读到结尾,后面即使有字符也不会读取。
C语言字符数组不记录长度导致的第三个问题就会导致每次增长或缩短C语言字符数组需要进行内存重分配(C语言字符数组底层是N+1个字符长的数组),内存重分配又导致两个问题
redis对以上两者问题进行了两个优化,核心目的是减少内存重分配次数
SDS动态字符串的当然可以扩容,机制类似Java的ArrayList。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。
redis 的字典底层是一个哈希表,它是无序字典。内部实现结构和 Java 的 HashMap 一致的,同样的数组 + 链表二维结构,产生hash冲突使用拉链法解决。只不过redis的hash的value只能是string。
在说明redis的字典前,需要先说明redis的哈希表、哈希表的节点。最后才解释字典如何实现。
redis哈希表定义如下
typedef struct dictht{
dictEntry **table;//数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表掩码,计算索引值,固定为size-1
unsigned long used;//已有节点数
} dictht;
redis哈希表 Node结构 dictEntry如下
type struct dictEntry{
void *key;//键
union{
void *val;
uint64_tu64;
int 64_ts64;
}v;//值
struct dictEntry *next;//下一个哈希表Node
} dictEntry;
Node中键值对中的值,可以是指针、是uint64_tu64整数、是64_ts64整数。
终于可以说到字典了
typedef struct dict{
dictType *type;//类型特点函数
void *private;//私有数据
dictht ht[2];//哈希表,两个原因是一个平时用一个仅在rehash用
int trehashidx;//rehash索引
}
其实上面的结构没啥大用。redis的字典结构面试只要把哈希算法、哈希冲突如何解决、渐进式rehash说了就基本OK了。
Redis 的列表相当于 Java 里的 LinkedList,注意它是链表。意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n)。
list定义如下
typedef struct list{
listNode *head;//头节点
listNode *tail;//尾节点
unsigned long len;//链表长度
void *(*dup)(void *ptr);//节点复制函数
void (*free)(void *ptr);//节点释放函数
void (*match)(void *ptr,void *key);//节点对比函数
}
具体的listNode节点
typedef strcut listNode{
struct listNode *prev;//前置节点
struct listNode *next;//后置节点
void *value;//节点值
}
从定义可以发现list底层是双向链表。因为*prev和*next都指向null,所以是无环的链表。深入再底层可以发现Redis 底层存储的还不是一个简单linkedlist
,而是称之为快速链表 quicklist
的一个结构。面试答出上面我觉得就足够了。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是
ziplist
,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist
。因为普通的链表需要的附加指针空间太大,会比较浪费空间,而且会加重内存的碎片化。比如这个列表里存的只是int
类型的数据,结构上还需要两个额外的指针prev
和next
。所以 Redis 将链表和ziplist
结合起来组成了quicklist
。也就是将多个ziplist
使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
redis 的集合相当于 Java 里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL
。
待续
redis是有序的set,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据结构。
redis一般用zskiplist组织
typedef struct zskiplist{
structz skiplistNode *header,*tail;
unsigned long length;//节点数
int level;//层数最大的节点的层数
}zskiplist;
skiplistNode节点定义如下
typedef stuct zskiplistNode{
struct zskiplistLevel{
struct zskipNode *forward;//前进指针
unsigned int span//跨度
}level[];
struct zskiplistNode *backward;//后退指针
double score;//分值
robj *obj;//成员对象
}zskiplistNode;
节点所在哪一层 1 至 32 之间的随机数随机生成的。跳表查询是从顶层自顶向下查找,类似二分。跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针(注:可以理解为维护了多条路径),从而达到快速访问节点的目的。
跳表redis用途