Redis 是使用 C语言编写的 key-value 数据库, 操作速度极快, 整体来说, 可以从数据结构和服务器事件驱动两个方面来解释. 下面就介绍redis所以引用的数据结构.
Redis 没有使用 C语言传统的字符串, 而是构建了自定义的 SDS(simple dynamic string) 类型. Redis 使用C类型字符串的地方只是在一些字符串无需修改的地方, 如日志打印.
包含字符串的键值对在底层都是使用 SDS 实现的.
struct sdshdr{
int len; // 记录buf数组已使用的字节的长度, 也是sds 字符串的长度.
int free; // 记录buf数组中未使用字节的数量
char buf[]; // 字节数组, 用于保存字符串
}
// SDS 实例
free = 0 // 表示该sds 没有分配任何使用空间
len = 5 // 表示该sds 保存了一个5个字节长度的字符串
buf = ['R','e','d','i','s','\0'] // char数组, 前五个字符为 'R','e','d','i','s';0
// 最后一个字节则保存了空字符串 '\0'. 这样的好处是可以重用部分 C 函数.
free = 0 free = 10
len = 5 + "Redis" => len = 10
buf = ['R','e','d','i','s','\0']len=6 buf = ['R','e','d','i','s','R','e','d','i','s','\0',...]len=21(free + len + 1)
- 当修改后len 长度大于1MB时, free 固定分配 1MB 的空间
```c
free = 0 free = 1MB
len = 0.9MB + "长度1MB字符串" => len = 1.9MB
buf = [...]len=2MB buf = [...]len=2.9MB + 1byte(free + len + 1byte)
相对C字符串的优点
List 使用LinkedList(链表)作为数据结构, 出了List之外, 发布订阅, 慢查询, 监视器 也用到了链表.
typeof struct listNode{
struct listNode *prev; // 前置节点
struct listNode *next; // 后置节点
void *value; // 节点的值
}
typeof struct list{
listNode *head; // 表头 (首节点指向null, 不形成环形)
listNode *tail; // 表尾 (尾节点指向null, 不形成环形)
unsigned long len; // 链表长度
void *(*dup)(void *ptr); // 复制节点所保存的值
void (*free)(void *ptr); // 释放节点所保存的值
int (*match)(void *ptr, void *key); // 用于对比节点所保存的值和另一个输入值是否相等.
}
字典是Redis 的String操作的底层实现
typeof struct dict{
dictType *type; // 类型特定函数
void *privatedata; // 私有数据, 保存哪些传递给特定函数的可选参数
dictht ht[2]; // 哈希表, 字典只使用ht[0], 当进行rehash时使用 ht[1].
int treshahidx; // rehash 索引, 当rehash 不再行进中时, 值为-1
}
typeof struct dictht{
dictEntry **table;// 哈希表数组
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码, 用于计算索引值. 总等于1
unsigned long used; // 该哈希表当前节点数量
}
typeof struct dictEntry{
void *key; // 键
union{ // 值
void *val;
uint64 _tu64;
int64 _ts64;
} v;
struct dictEntry *next; // 指向下一个哈希表节点, 形成环形
};
ZSET 在包含元素数量较多时就是使用 skiplist实现, 另一个地方就是集群节点的内部结构. 其效率可以和平衡树媲美, 且实现更简单.
插入和删除的时间复杂度就是查询元素插入位置的时间复杂度, 即 O(logN).
typeof struct zskiplist{
struct skiplistNode *head, *tail; // 表头节点和表尾节点
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
}
typeof struct zskiplistNode{
struct zskiplistLevel{ // 层
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
struct zskiplistNode *backward; // 后退指针
double score; // 分数
robj *obj; // 成员对象
}
// 插入第一个数 1
Level1: head -> Node(1)
// 插入第二个数 2. 1: 先插入节点到Level1, 然后根据随机算法生成level数, 假如level=2
Level2: head -> Node(1) -> Node(2)
Level1: head -> Node(1) -> Node(2)
// 插入第三个数 3. 1: 和插入第二个节点类似, 假如level=1
Level2: head -> Node(1) -> Node(2)
Level1: head -> Node(1) -> Node(2) -> Node(3)
// 插入第四个数 4. 1: 和插入第二个节点类似, 假如level=1
Level3: head -> Node(1) -> Node(4)
Level2: head -> Node(1) -> Node(2)
Level1: head -> Node(1) -> Node(2) -> Node(3) -> Node(4)
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) // ZSKIPLIST_P = 0.25
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; // ZSKIPLIST_MAXLEVEL = 32
}
intset 是Set 的底层实现之一, 当一个集合只包含整数值元素且元素不多的时候启用.
typeof struct intset{
// 编码方式: 1. INTSET_ENC_INT16 content中是int16_t的整数值
// 2. INTSET_ENC_INT32 content中是int32_t的整数值
// 2. INTSET_ENC_INT64 content中是int64_t的整数值
uint32_t encoding;
uint32_t length;// 集合中的元素数量
int8_t contents[]; // 元素数组
} intset;
升级和降级
当数组中存放的三个元素1,2,3时, encoding是 int16_t, 当插入65536之后, encoding就将变成 int32_t, 然后数组长度从 163 变成 324, 这个过程称之为升级.
当数组中存放的四个元素1,2,3, 65536时, encoding是 int32_t, 当删除65536之后, encoding不改变, 然后数组长度从 324 变成 323, 这个过程称之为降级.
升级操作为整数集合带来了操作上的灵活性, 并且节约了内存; 整数集合只支持升级不支持降级.
压缩列表是 list和hash 的底层实现之一. 当list 中只包含少量项并且 每个列表项都是小整数或较短的字符串, 就会使用压缩表. 主要是为了节约内存而开发的顺序型结构.
Redis 数据结构高效的真正原因就是因为数据结构 对象的原因.
Redis 设计与实现