这篇文章关于 Redis 的基础数据。阅读这篇文章你能够了解:java
三个数据结构 Redis 是怎么实现的。redis
SDS (Simple Dynamic String) 是 Redis 最基础的数据结构。直译过来就是"简单的动态字符串"。Redis实现了一个动态字符串,而不是直接使用了C语言中的字符串。数据库
sds数据结构:数组
struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[]; };
因此一个 SDS 的就以下图:安全
因此咱们看到,sds 包含3个参数。buf 的剩余长度,buf 的长度 len,以及buf。数据结构
为何这么设计呢?函数
能够直接获取字符串长度。
C 语言中,获取字符串的长度须要用指针遍历字符串,时间复杂度为 O(n),而 SDS 的长度,直接从len 获取复杂度为 O(1)。性能
杜绝缓冲区溢出。
因为C 语言不记录字符串长度,若是增长一个字符传的长度,若是没有注意就可能溢出,覆盖了紧挨着这个字符的数据。对于SDS 而言增长字符串长度须要验证 free的长度,若是free 不够就会扩容整个 buf,防止溢出。ui
减小修改字符串长度时形成的内存再次分配。
redis 做为高性能的内存数据库,须要较高的相应速度。字符串也很大几率的频繁修改。 SDS 经过未使用空间这个参数,将字符串的长度和底层buf的长度之间的额关系解除了。buf的长度也不是字符串的长度。基于这个分设计 SDS 实现了空间的预分配和惰性释放。spa
二进制安全。
C 语言中的字符串是以 ”\0“ 做为字符串的结束标记。而 SDS 是使用 len 的长度来标记字符串的结束。因此SDS 能够存储字符串以外的任意二进制流。由于有可能有的二进制流在流中就包含了”\0“形成字符串提早结束。也就是说 SDS 不依赖 “\0” 做为结束的依据。
兼容C语言
SDS 按照惯例使用 ”\0“ 做为结尾的管理。部分普通C 语言的字符串 API 也可使用。
C语言中并无链表这个数据结构因此 Redis 本身实现了一个。Redis 中的链表是:
typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value; } listNode;
很是典型的双向链表的数据结构。
同时为双向链表提供了以下操做的函数:
/* * 双端链表迭代器 */ typedef struct listIter { // 当前迭代到的节点 listNode *next; // 迭代的方向 int direction; } listIter; /* * 双端链表结构 */ 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;
链表的结构比较简单,数据结构以下:
总结一下性质:
字典数据结构极其相似 java 中的 Hashmap。
Redis的字典由三个基础的数据结构组成。最底层的单位是哈希表节点。结构以下:
typedef struct dictEntry { // 键 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下个哈希表节点,造成链表 struct dictEntry *next; } dictEntry;
实际上哈希表节点就是一个单项列表的节点。保存了一下下一个节点的指针。 key 就是节点的键,v是这个节点的值。这个 v 既能够是一个指针,也能够是一个 uint64_t
或者 int64_t
整数。*next 指向下一个节点。
经过一个哈希表的数组把各个节点连接起来:
typedef struct dictht { // 哈希表数组 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩码,用于计算索引值 // 老是等于 size - 1 unsigned long sizemask; // 该哈希表已有节点的数量 unsigned long used; } dictht;
dictht
经过图示咱们观察:
实际上,若是对java 的基本数据结构了解的同窗就会发现,这个数据结构和 java 中的 HashMap 是很相似的,就是数组加链表的结构。
字典的数据结构:
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;
其中的dictType 是一组方法,代码以下:
/* * 字典类型特定函数 */ 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;
字典的数据结构以下图:
这里咱们能够看到一个dict 拥有两个 dictht。通常来讲只使用 ht[0],当扩容的时候发生了rehash的时候,ht[1]才会被使用。
当咱们观察或者研究一个hash结构的时候偶咱们首先要考虑的这个 dict 如何插入一个数据?
咱们梳理一下插入数据的逻辑。
计算Key 的 hash 值。找到 hash 映射到 table 数组的位置。
若是数据已经有一个 key 存在了。那就意味着发生了 hash 碰撞。新加入的节点,就会做为链表的一个节点接到以前节点的 next 指针上。
若是 key 发生了屡次碰撞,形成链表的长度愈来愈长。会使得字典的查询速度降低。为了维持正常的负载。Redis 会对 字典进行 rehash 操做。来增长 table 数组的长度。因此咱们要着重了解一下 Redis 的 rehash。步骤以下:
渐进式的 rehash 。
其实若是字典的 key 数量很大,达到千万级以上,rehash 就会是一个相对较长的时间。因此为了字典可以在 rehash 的时候可以继续提供服务。Redis 提供了一个渐进式的 rehash 实现,rehash的步骤以下:
这样保证数据可以平滑的进行 rehash。防止 rehash 时间太久阻塞线程。