第 1 步:阅读数据结构实现
刚开始阅读 Redis 源码的时候, 最好从数据结构的相关文件开始读起, 因为这些文件和 Redis 中的其他部分耦合最少, 并且这些文件所实现的数据结构在大部分算法书上都可以了解到, 所以从这些文件开始读是最轻松的、难度也是最低的。
下表列出了 Redis 源码中, 各个数据结构的实现文件:
文件 内容
sds.h 和 sds.c Redis 的动态字符串实现。
adlist.h 和 adlist.c Redis 的双端链表实现。
dict.h 和 dict.c Redis 的字典实现。
redis.h 中的 zskiplist 结构和 zskiplistNode 结构, 以及 t_zset.c 中所有以 zsl 开头的函数, 比如 zslCreate 、 zslInsert 、 zslDeleteNode ,等等。 Redis 的跳跃表实现。
hyperloglog.c 中的 hllhdr 结构, 以及所有以 hll 开头的函数。 Redis 的 HyperLogLog 实现。
sds.h 和 sds.c Redis 的动态字符串实现。
/* * 保存字符串对象的结构 */
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
动态字符串与C字符串的区别:
①常数复杂度获取字符串长度。
strlen() sdshdr.len
②杜绝缓冲区溢出。
strcat(),sdshdr.free当需要对sds修改时,会先检查空间是否满足修改所需的要求,如果不满足,会先扩展空间,然后才执行修改。
③减少修改字符串时带来的内存重分配次数。
通过未使用空间,sds实现了空间预分配和惰性空间释放两种优化策略。
④二进制安全。
sds.buf称为“字节数组”的原因:redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。
⑤兼容部分C字符串函数。
adlist.h 和 adlist.c Redis 的双端链表实现。
链表节点
/* * 双端链表节点 */
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value; //可见,链表中保存的是指向数据的指针.void*可用来保存不同类型的值。
} listNode;
链表结构
/* * 双端链表结构 */
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;
redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典的一个键值对。(注:STL中std::map的底层实现是红黑树)
哈希表节点
/* * 哈希表节点 */
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表,将多个哈希值相同的键值对连接在一起,以此解决键冲突的问题。
struct dictEntry *next;
} dictEntry;
哈希表
/* * 哈希表 * * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。 */
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
字典:
/* * 字典 */
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;
冲突:两个或以上数量的键值散列到哈希表数组的同一个索引上面。
redis的哈希表使用“分离链接法”解决冲突。每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引的多个节点可以用这个单向链表链接起来,解决键值冲突问题。
rehash: 为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或太少时,程序需要对哈希表的大小进行扩展或者收缩。扩展或收缩哈希表的工作通过rehash(再散列)操作完成。
每个字典带有两个哈希表,一个平时使用,另一个仅在进行rehash时使用。
Redis 的跳跃表实现: redis.h 中:的 zskiplist 结构和 zskiplistNode 结构, 以及 t_zset.c 中所有以 zsl 开头的函数, 比如 zslCreate 、 zslInsert 、 zslDeleteNode ,等等。
跳跃表节点,定义在redis.h中
/* 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 zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
跳跃表的实现:http://origin.redisbook.com/internal-datastruct/skiplist.html#id3