Redis常见数据结构
前言
- 下面会根据redis源码看看我们常用的数据结构内部的实现原理。
数据结构
动态字符串
- redis内部没有使用普通的C语言字符串,而是将其封装了一层,使得其不仅兼容C字符串还能自由动态变化, 我们看下内部数据结构:
struct sdshdr {
// 当前字符串长度
int len;
// 可用空间
int free;
// 兼容C语言字符串,末尾'\0'结束
char buf[];
};
核心策略
- 空间预分配策略:
1 如果需要增加的字符串长度小于free长度,那么不需要分配额外空间
2 如果修改时free不够用并且修改后的字符串长度小于1MB,那么len和free长度保持相同,字符串长度为len+free+1
其中1个字节为末尾的’\0’。
2 如果修改后字符串长度大于等于30MB那么每次free长度动态扩大1MB,计算同上 - 惰性空间释放:
1 SDS的API提供了一套内存释放的功能,但是sds不会立刻释放空间,会先保留在free中使用,如果后期需要增长的话就立刻能派上用场。
对比
- 最后我们讲一下redis使用sds结构而不是普通C字符串的几个原因:
序号 | SDS | C字符串 |
---|---|---|
1 | 求长度的复杂度为O(1),性能提升 | 复杂度为O(n) |
2 | 由于动态分配空间,不存在缓冲区溢出的可能 | 存在缓冲区溢出 |
3 | 二进制安全,不仅能保存文本数据还支持二进制数据 | 非二进制安全 |
4 | 修改N次字符串,内存重分配N次 | 修改N次字符串,内存分配最多N次 |
链表
- c语言并没有内置链表这种结构,redis中自己设计了一种结构,在很多地方如: 发布订阅,慢查询,监视器等都用到了链表,redis服务器还用链表保存了多个客户端,所以链表在内部的使用必不可少,下面看下链表的相关数据结构:
/*
* 双端链表结构
*/
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表所包含的节点数量
unsigned long len;
} list;
/*
* 双端链表节点
*/
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
相关特性
- 获取节点的头部和尾部的复杂度都是O(1)
- 不存在环,因为表头节点的prev和表尾节点的next都是NULL
- 能从任意一个节点发现前后节点的时间复杂度O(1)
- 链表长度获取复杂度O(1)
- 多态,结构中有dup,free,match三个属性为节点设置了特定类型函数, 所以redis链表可以保存不同的值.
字典
- redis内部使用哈希表作为底层实现,我们由上到下看下数据结构:
/*
* 字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
/*
* 字典类型特定函数
*/
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
核心点
- hash算法:计算不同的key存储的位置,计算伪代码如下:
// 算出key对应存储的hash
hashValue = dict->type->hasFunction(key);
// 通过hash和sizemask进行与操作,这里x可以是0或者1 看是否正在进行rehash
index = hashValue & ht[x].sizemask;
- 渐进式rehash: 这一块有点复杂,简单来说就是保持负载因子在一个合适的范围内,维持程序的高可用性。具体可以参考这里
跳跃表
- 首先看下数据结构
/* ZSETs use a specialized version of Skiplists */
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
- 跳跃表是有序集合的底层实现之一
- 具体细节略复杂,具体可以参考这里
整数集合
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
压缩列表
对象
字符串对象
列表对象
哈希对象
集合对象
有序集合对象
其他
参考资料
[1] Redis设计与实现
[2] Redis官方网站
[3] Redis源码