你好,是我琉忆。
今天我们一起来探讨一下Redis的5种数据类型底层是如何实现的。今天我们先来探讨字符串和列表的底层实现。
作为后端人员,对于Redis一定不会模式。但是我敢说90%的程序员会用,但是只有10%的人知道它的底层构造。
如果你只是停留在用的阶段,那么你多数没有办法和别人拉开差距。今天这节课我们就来聊聊Redis的底层实现原理(由于Redis是C语言编写的,看源码时需要一定的C/C++基础)。
01
String类型的结构
我们都知道Redis是基于内存的 key-value 数据库,并且具有良好的扩展性,能够进行水平拆分、主从复制,拥有极高的吞吐量和响应性能。
Redis 是基于内存的 key-value 数据库,并且具有良好的扩展性,能够进行水平拆分、主从复制,拥有极高的吞吐量和响应性能。
Redis不像传统的数据库一样,传统数据库有表模型,所以操作数据库都是操作表,而Redis主要是通过键值对的方式进行存储数据,所以我们主要的操作都是对key操作。
那么作为我们经常用的字符串类型,Redis是如何构造的呢?
String 型 value 数据内部以 int、SDS 作为内部从存储结构。
1、字符串对象的内部编码:
- REDIS_ENCODING_INT
- REDIS_ENCODING_EMBSTR
2、字符串的SDS内部存储结构如下:
/* * 保存字符串对象的结构 */struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[];};
buf[] 数组中存储了字符串的内容,存储的字符串长度可以通过 O(1) 的复杂度得出。
例如 char buf[] = new {'h' ,'e','l','l','o'}
,C 语言同上会加一个 '\0' 作为字符串的定界符。
字符串类型 的Value 初始化时的结构:
robj *createEmbeddedStringObject(char *ptr, size_t len) { robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1); struct sdshdr *sh = (void*)(o+1); o->type = REDIS_STRING; o->encoding = REDIS_ENCODING_EMBSTR; o->ptr = sh+1; o->refcount = 1; o->lru = LRU_CLOCK(); sh->len = len; sh->free = 0; if (ptr) { memcpy(sh->buf,ptr,len); sh->buf[len] = '\0'; } else { memset(sh->buf,0,len+1); } return o;}
3、从上面知道Redis字符串的结构,但是它是怎么实现扩容的?
Redis字符串的扩容源码是存储在sds.c/sds.h中的,它实现的扩容流程如下:
Redis扩容的流程是这样的:当字符串长度小于 1M,扩容是加倍现有的空间。如果超过 1M 字节,扩容时一次只会多扩 1M 的空间,并且字符串最大长度为 512 M,扩容的复杂度是 O(N)。
对应的扩容源码:
// 最大预分配长度#define SDS_MAX_PREALLOC (1024*1024)/* - 对 sds 中 buf 的长度进行扩展,确保在函数执行之后, - buf 至少会有 addlen + 1 长度的空余空间 - (额外的 1 字节是为 \0 准备的) - 5. 返回值 - sds :扩展成功返回扩展后的 sds - 扩展失败返回 NULL - 9. 复杂度 - T = O(N) */sds sdsMakeRoomFor(sds s, size_t addlen) { struct sdshdr *sh, *newsh; // 获取 s 目前的空余空间长度 size_t free = sdsavail(s); size_t len, newlen; // s 目前的空余空间已经足够,无须再进行扩展,直接返回 if (free >= addlen) return s; // 获取 s 目前已占用空间的长度 len = sdslen(s); sh = (void*) (s-(sizeof(struct sdshdr))); // s 最少需要的长度 newlen = (len+addlen); // 根据新长度,为 s 分配新空间所需的大小 if (newlen < SDS_MAX_PREALLOC) // 如果新长度小于 SDS_MAX_PREALLOC // 那么为它分配两倍于所需长度的空间 newlen *= 2; else // 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC newlen += SDS_MAX_PREALLOC; // T = O(N) newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1); // 内存不足,分配失败,返回 if (newsh == NULL) return NULL; // 更新 sds 的空余长度 newsh->free = newlen - len; // 返回 sds return newsh->buf;}
02
List类型的结构
Redis中的List类型也是我们常用的数据结构,我们可以用来插入数据、删除数据、查看数据等操作。它主要被用来存储字符串数列。
1、List的常用操作:
- rpush/lpush:将 String 内容添加到对应 key 列表 value 的爱投或者末尾
- rpop/lpop:取出给定 key 对应列表的 value 的开头或者末尾元素并删除
- lindex:取出给定 key 对应列表的 value 的某个元素
2、List 类型的 value 数据结构内部实现有 linkedList 和 zipList 来承载的,zipList 主要是用来减少内存占用的。
3、接下来我们看看linkedList的内部结构是如何实现的?
首先,我们需要知道LinkedList 实现是使用双端链表实现的。
对应的源码文件是:adlist.c/adlist.h
/* * 双端链表节点 */typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value;} /* * 双端链表结构 */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 的列表相当于 Java 语言里面的 LinkedList,List 的插入和删除操作非常快,时间复杂都是 O(1),但是查找特别慢,时间复杂度是 O(n),当弹出最后一个元素时,释放 list。
4、List类型是如何实现插入数据的?
List类型的操作流程图:
对应的源码分析:
list *listAddNodeHead(list *list, void *value){ listNode *node; // 为节点分配内存 if ((node = zmalloc(sizeof(*node))) == NULL) return NULL; // 保存值指针 node->value = value; // 添加节点到空链表 if (list->len == 0) { list->head = list->tail = node; node->prev = node->next = NULL; // 添加节点到非空链表 } else { node->prev = NULL; node->next = list->head; list->head->prev = node; list->head = node; } // 更新链表节点数 list->len++; return list;}
它将一个包含有给定值指针 value 的新节点添加到链表的表头,如果为新节点分配内存出错,那么不执行任何动作,仅返回 NULL。如果执行成功,返回传入的链表指针。T = O(1)。
5、查看List中的数据内部是如何实现的?
查找的复杂度是 O(N) ,它内部有个循环匹配的过程。实现的流程图:
实现源码:
listNode *listSearchKey(list *list, void *key){ listIter *iter; listNode *node; // 迭代整个链表 iter = listGetIterator(list, AL_START_HEAD); while((node = listNext(iter)) != NULL) { // 对比 if (list->match) { if (list->match(node->value, key)) { listReleaseIterator(iter); // 找到 return node; } } else { if (key == node->value) { listReleaseIterator(iter); // 找到 return node; } } } listReleaseIterator(iter); // 未找到 return NULL;}
主要操作是,查找链表 list 中值和 key 匹配的节点。对比操作由链表的 match 函数负责进行,如果没有设置 match 函数,那么直接通过对比值的指针来决定是否匹配。
如果匹配成功,那么第一个匹配的节点会被返回。如果没有匹配任何节点,那么返回 NULL 。T = O(N)
6、删除List中的数据内部如何实现的?
删除的复杂度是 O(1) ,List只要找到相关节点删除皆可。
实现流程图:
内部实现源码:
void listDelNode(list *list, listNode *node){ // 调整前置节点的指针 if (node->prev) node->prev->next = node->next; else list->head = node->next; // 调整后置节点的指针 if (node->next) node->next->prev = node->prev; else list->tail = node->prev; // 释放值 if (list->free) list->free(node->value); // 释放节点 zfree(node); // 链表数减一 list->len--;}
它的操作为:从链表 list 中删除给定节点 node。对节点私有值(private value of the node)的释放工作由调用者进行。T = O(1)
由于篇幅较长,今天主要介绍字符串和列表的数据结构实现方法。后续的3种数据类型将在下一篇进行讲解介绍。
后话
如果想要和别人拉开差距,不要只停留在表面哦。只有深入核心知道其所以然才能用得更得心应手。让我们成为更好的自己,每天进步1%。
如果你觉得好,可以点个赞哦~