一、Redis 数据结构与对象
redis 有SDS、链表、字典、跳跃表、整数集合与压缩列表等数据结构。这些数据结构并不是我们平时操作 redis 所使用的,操作 redis 使用的是对象,包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象。数据结构是这些对象的底层实现结构。例如: 哈希对象的底层实现可能是压缩列表或者是hashtable。
1、数据结构
(1)SDS:简单动态字符串。除了用于保存数据库中的字符串外,SDS还被用作缓冲区:AOF模块中的 AOF 缓冲区,以及客户端状态中的缓冲区。
-
SDS 的实现
// redis 中的 SDS 由 sds.h/sdshdr 结构定义 typedef struct sdshdr { int len; // 已使用的空间 int free; // 未使用的空间 char buf[]; // 字节数组,用于保存字符串,同 C 语言 } sdshdr; -
SDS 与 C 字符串相比的优点
1、常数复杂度获取字符串的长度; 2、杜绝缓冲区溢出; 3、减少修改字符串时带来的内存充分配的次数; 4、二进制安全
(2)链表:链表在 redis 中的使用十分广泛。比如列表键的底层实现之一就是链表。当列表中的元素比较多或者列表中的元素是比较长的字符串时,列表底层就会采用链表实现。除了列表键之外,发布与订阅、慢查询、监视器等功能也都用到了链表。redis 服务器使用链表保存多个客户端的状态信息,以及使用链表来构建客户端的输出缓冲区。
-
链表的实现
// redis 中的链表由 adlist.h/list 结构定义 typedef struct list { listNode *head; // 头节点 listNode *tail; // 尾节点 unsigned long len; // 链表长度 void*(*dup)(void *ptr); // 复制函数 void*(*free)(void *ptr); // 释放函数 int(*match)(void *ptr, void *key); // 比较函数 } list; typedef struct listNode { struct listNode *prev; struct listNode *next; void *value; } listNode;
(3)字典:字典在 redis 中的使用十分广泛。redis 数据库的底层实现就是基于字典实现的。字典出了可以实现数据库,同时也是哈希键的底层实现之一。字典的底层采用哈希表实现。
-
字典的实现
// redis 中的字典由 dict.h/dict 结构定义 typedef struct dict { dictType *type; // 类型特定函数 void *privdata; // 私有数据 dictht ht[2]; // 字典一般使用ht[0],只有对 ht[0] 进行 rehash 时,才会用到 ht[1] int rehashidx; // 记录目前 rehash 的进度,当 rehash 不进行时,值为 -1; } dict; // redis 使用的哈希表由 dict.h/dictht 结构定义 typedef struct dictht { dictEntry **table; // 哈希表数据 unsigned long size; // 哈希表大小 unsigned long sizemask; // 掩码=size - 1,rehash 时用于计算负载因子 unsigned long used; // 哈希表已有节点的数量 } dictht; // 哈希表节点结构 typedef struct dictEntry { void *key; union { void *val; uint64_tu64; int64_ts64; }v; struct dictEntry *next; } dictEntry; -
哈希算法
// 使用 hash 函数计算哈希值与索引值,一般 redis 采用 MurmurHash2 算法, 该算法在输入有规律的情况下,仍能给出一个很好的随机分不行 hash = dict->type->hashFunction(key); index = hash & dict->ht[x].sizemask; // 散列冲突时,redis 采用链地址法解决冲突。采用头插法,速度快。 -
rehash 扩容与收缩
rehash 扩容的时机 1)服务器目前没有执行 BGSAVE 或者 BGREWRITEAOF 命令,并且负载因子大于等于1; 2)服务器目前正在执行 BGSAVE 或者 BGREWRITEAOF 命令,并且负载因子大于等于5; rehash 收缩时机 1)负载因子小于等于 0.1 负载因子计算:load_factor = ht[0].used / ht[0].size rehash 过程 1)为哈希表 ht[1] 分配空间,空间大小取决于 rehash 的操作与 ht[0].used 的大小。如果 是扩容操作,ht[1] 大小为第一个大于等于 ht[0].used * 2 的 2的 n 次幂。如果是收缩操作,ht[1] 大小为第一个大于等于 ht[0].used 的2的 n 次幂。 2)将 ht[0] 上的所有键 rehash 到 ht[1] 上,同时释放ht[0], 将 ht[1] 设置为 ht[0],重新为 ht[1] 创建一个空的哈希表。 rehash 的过程不是集中式的,是渐进式的。注意在 dict 结构体中有一成员 rehashidx, 记录了当前rehash的进度,所以渐进式rehash的过程如下: 1) 为 ht[1] 分配空间; 2)将 rehashidx 设置为 0,表示 rehash 正式开始; 3)每次对字典进行增删改查的操作时,除了执行本次操作,同时将 rehashidx 对应的所有键值对 rehash 到 ht[1],当全部 rehash 后,将 rehashidx 置为 -1。
(4)跳跃表(skiplist):是一种有序数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问的目的。Redis 使用跳跃表作为有序集合的底层实现之一。如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis 就会使用跳跃表作为有序集合键的底层实现。除了用于实现有序集合,跳跃表还在集群节点中用作内部数据结构,除此之外,跳跃表在 redis 中再没有其他的用途。
// redis 中跳跃表由 redis.h/zskiplist 结构定义
typedef struct zskiplist {
struct skiplistNode *head, *tail; // 头尾指针
unsigned int length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
}
// 跳跃表节点的实现由 redis.h/zskoplistNode 定义
typedef struct zskiplistNode {
struct zkskiplistLevel {
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
struct zskiplistNode *backward; // 后退指针
double score; // 分值
robj *obj; // 成员对象
}
// 注:每个跳跃表的层高都是1 到 32 之间的随机数。
(5)整数集合:是集合键的底层实现之一。当一个集合只包含整数值元素,并且这个集合的元素数量不多时,redis 就会使用整数集合作为集合键的底层实现。
(6)压缩列表(ziplist):压缩列表是列表键和哈希键的底层实现之一。当一个列表键值包含少量的列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么 redis 就会使用压缩列表作为列表键的底层实现。
2、对象:Redis 并没有用之前的数据结构去实现键值对数据库,而是基于这些数据结构创建了一个对象系统。这个系统包括字符串对象、列表对象、哈希对象、集合对象和有序集合对象这五种类型的对象。
// redis 中对象结构由 redisObject 结构定义
typedef struct redisObject {
unsigned type:4; // 类型
unsigned encoding:4 // 编码
void *ptr; // 指向底层实现数据结构的指针
unsigned *lru:22; // 空转时长
...
}
type 属性记录了对象的类型。键对象总是字符串对象,所以 type key,返回的值对象的类型。
encoding 属性记录了对象底层使用哪种数据结构作为对象的底层实现。可以使用命令 object encoding key
来进行查看。
(1)字符串对象:编码可以是 int、raw 或者 embstr。三者具体的区别可以参考 《redis设计与实现(第二版)》一书。
(2)列表对象:编码可以是 ziplist(压缩列表) 或者是 linkedlist(双端链表)。
-
ziplist 实现示意图
-
linkedlist 实现示意图
-
编码转换:当列表对象可以同时满足以下两个条件时,列表对象使用 ziplist 编码。
1、列表对象保存的所有字符串元素的长度都小于 64 字节; 2、列表对象保存的元素数量小于512 个
(3)哈希对象:编码可以是 ziplist 或者是 hashtable。
- ziplist 实现示意图
- hashtable 实现示意图
- 编码转换同列表对象
(4)集合对象:编码可以是 intset 或者是 hashtable;
-
intset 实现示意图
-
hashtable 实现示意图
-
编码转换:满足以下两个条件使用 ziplist 编码
1、集合保存的所有元素都是整数值; 2、集合对象保存的元素数量不超过 512 个。
(5)有序集合对象:编码可以是 ziplist 或者是 skiplist(实际上是跳表 + 字典)。
-
ziplist 实现示意图
-
skiplist 实现示意图
-
编码转换:满足以下两个条件使用 ziplist 编码。
1、有序集合保存的元素数量小于 128 个。 2、有序集合保存的所有元素成员的长度都小于 64 字节。 以上两个条件的上限值可以修改,具体查看配置文件 zset-max-ziplist-entried 选项和 zset-max-ziplist-value 选项。