c++ 删除string开头的0_Redis的String和List底层是如何实现的?

7933e519e230d077e26fc4ed7090b60a.png

你好,是我琉忆。

今天我们一起来探讨一下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中的,它实现的扩容流程如下:

97adf26fcb403946d782bd361478407f.png

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类型的操作流程图:

d181742cddb897cc8b71f25df7ffb2d6.png

对应的源码分析:

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) ,它内部有个循环匹配的过程。实现的流程图: 0151387a89ca7c6f0c655f1774326505.png实现源码:
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只要找到相关节点删除皆可。 实现流程图: 164a8229a5fd5ef6521360d1da75802b.png 内部实现源码:
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)

5f01e480e68e33171e20673505a9a546.png

由于篇幅较长,今天主要介绍字符串和列表的数据结构实现方法。后续的3种数据类型将在下一篇进行讲解介绍。

后话

    如果想要和别人拉开差距,不要只停留在表面哦。只有深入核心知道其所以然才能用得更得心应手。让我们成为更好的自己,每天进步1%。

2ab768863f84efd2591a062599c95101.gif

如果你觉得好,可以点个赞哦~

97766d63f99bdfc2cace4898cc0089c1.png 97766d63f99bdfc2cace4898cc0089c1.png

往期精选

  • PHP面试常考内容之Memcache和Redis(1)

  • PHP面试常考内容之Memcache和Redis(2)

  • PHP面试之面向对象(1)

  • PHP面试常考内容之面向对象(2)

  • PHP面试常考内容之面向对象(3)

3f55d0512d6ba30adc7809c323d01b87.png

如有疑问或想跟我交流,

可以加我的个人微信:leoyistar

关注琉忆编程库,PHP面试资料都在这

aaa8eec3fd62403d322f837a40d23c32.png c04272e06336a0bfb354558c65348d53.gif

点击“好看”,让更多人看到~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值