Redis核心设计原理(深入底层C源码)

🚀 优质资源分享 🚀

学习路线指引(点击解锁) 知识定位 人群定位
🧡 Python实战微信订餐小程序 🧡 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
💛Python量化交易实战💛 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

Redis 基本特性

1. 非关系型的键值对数据库,可以根据键以O(1) 的时间复杂度取出或插入关联值  2. Redis 的数据是存在内存中的  3. 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的  4. 键值对中的值类型可以是string,hash,list,set,sorted set 等  5. Redis 内置了复制,磁盘持久化,LUA脚本,事务,SSL, ACLs,客户端缓存,客户端代理等功能  6. 通过Redis哨兵和Redis Cluster 模式提供高可用性

Redis高性能的原因

1.图示(换算时间:1s =1000 ms ,1ms=1000 us ,1us =1000 ns):

2.对于内存数据库来说,本身数据就存在于内存里,避免了磁盘 I/O 的限制,无疑访问速度会远大于磁盘数据库。

3.其次Redis,默认是采用一个线程执行指令任务的,既减少了线程上下文切换带来的开销,也避免并发问题。

4.而且Redis中有多种数据类型,每种数据类型的底层都由一种或多种数据结构来支持。正是因为有了这些数据结构,Redis 在存储与读取上的速度才不受阻碍。

深入底层C源码分析Redis

1.Redis是基于键值对存储数据的,像我们平时会使用的时候很容易觉得Redis的键值是多种数据类型的,其实不然,Redis的键值是String类型的,数据变成字节流(byte)基于网络传输的过程,传到Redis服务转成SDS(Simple Dynamic String【简单动态字符串】) String(Redis自定义的数据类型)。既然Redis是基于C语言写的,那么为什么不用原生的?

//如果我们想存储字符串:myname
C: char data[]="myname\0"; //而C语言中对于字符串是默认采用\0作为结尾的

而对于Redis,它是面向多种语言的,对于传过来的数据是不可控的:
 如果传输的视频流或者音频的流文件,大概率会出现"name\0orxxx"这种
 那么C语言只能读到“name”这部分遇到“\0”,则会视为结束了。(这明显是不合适,容易导致数据丢失)
 故,Redis采用sds结构:
 struct sdshdr {
 int len;    //存储的长度
        int free;  //剩余的空闲空间
        char buf[]; //数据存储的地方
 };

 这种数据结构的好处是:
 1.对于存储数据的准确性更高了,依靠len字段来标明准确数据的位置。【二进制安全的数据结构】
 2.采用以空间换时间的方式,每次扩容的时候可以适当分配大一点的空间,记录剩余时间是否够下一次的修改或者追加。(减少对象的销毁与创建的步骤)【提供了内存预分配机制,避免了频繁的内存分配】
 3.会在数据末尾依旧采用\0作为结尾【兼容C语言的函数库】

说明:

Redis自定义sdshdr数据结构具备三大特性:

【1】二进制安全的数据结构

【2】提供了内存预分配机制,避免了频繁的内存分配

【3】兼容C语言的函数库

2.String类型的数据结构

1)代码展示

//redis 3.2 以前
struct sdshdr {
 int len;
 int free;
 char buf[];
};
//redis 3.2 后
//redis\deps\hiredis\sds.h文件
typedef char *sds;

//存在注释:sdshdr5 is never used, we just access the flags byte directly.However is here to document the layout of type 5 SDS strings. 
//意思大概是:sdshdr5从未使用过,我们只是直接访问标志字节。然而,这里是为了记录类型5 SDS字符串的布局
struct \_\_attribute\_\_ ((\_\_packed\_\_)) sdshdr5 {  // 对应的字符串长度小于 1<<5
    unsigned char flags; 
 char buf[];
};

//\_\_attribute\_\_ ((packed)) 的作用就是告诉编译器取消结构体在编译过程的优化对齐,按照实际占用字节数进行对齐

struct \_\_attribute\_\_ ((\_\_packed\_\_)) sdshdr8 { // 对应的字符串长度小于 1<<8
    uint8\_t len;                              //目前字符串的长度
    uint8\_t alloc;                            //分配的内存总长度
    unsigned char flags;                      //flag用3bit来标明类型,类型后续解释,其余5bit目前没有使用
    char buf[];                               //柔性数组,以'\0'结尾
};
struct \_\_attribute\_\_ ((\_\_packed\_\_)) sdshdr16 { // 对应的字符串长度小于 1<<16
 uint16\_t len; 
 uint16\_t alloc; 
 unsigned char flags; 
 char buf[];
};
struct \_\_attribute\_\_ ((\_\_packed\_\_)) sdshdr32 { // 对应的字符串长度小于 1<<32
 uint32\_t len; 
 uint32\_t alloc; 
 unsigned char flags; 
 char buf[];
};
struct \_\_attribute\_\_ ((\_\_packed\_\_)) sdshdr64 { // 对应的字符串长度小于 1<<64
 uint64\_t len; 
 uint64\_t alloc; 
 unsigned char flags; 
 char buf[];
};

#define SDS\_TYPE\_5 0
#define SDS\_TYPE\_8 1
#define SDS\_TYPE\_16 2
#define SDS\_TYPE\_32 3
#define SDS\_TYPE\_64 4

static inline char sdsReqType(size\_t string\_size) {
 if (string\_size < 1<<5)
 return SDS\_TYPE\_5;
 if (string\_size < 1<<8)
 return SDS\_TYPE\_8;
 if (string\_size < 1<<16)
 return SDS\_TYPE\_16;
#if (LONG\_MAX == LLONG\_MAX) 
 if (string\_size < 1ll<<32)
 return SDS\_TYPE\_32;
 return SDS\_TYPE\_64;
#else
    return SDS\_TYPE\_32;
#endif
}

2)发现说明

【1】为什么要对原本的数据结构进行修改?(改版后的优化在哪里)

因为int占据4个字节(8bit),也就是能存42亿左右的,但是在我们实际上,存储的数据大概率都是小数据,所以它存在浪费资源的嫌疑。

所以进行优化的思维就是根据不同的数据范围,设置不同容量,如,uint8_t 表示占据1字节(8bit,在二进制中最大可以表示255),uint16_t 表示占据2字节(16bit,在二进制中最大可以表示65535)

【2】官网上说String类型限制大小512M,是怎么限制的?

//位于t\_string.c文件中
//为什么要限制,要知道512M已经是一个很大的值了(已经是一个bigkey了),在redis单线程操作中已经很容易阻塞线程
//故在追加命令appendCommand和设置命令setrangeCommand中都会进行校验
static int checkStringLength(client *c, long long size) {
 if (size > 512*1024*1024) {
 addReplyError(c,"string exceeds maximum allowed size (512MB)");
 return C\_ERR;
 }
 return C\_OK;
}

3)分析是怎么创建的

//在sds.c文件内
//sds在创建的时候,buf数组初始大小为:struct结构体大小 + 字符串的长度+1, +1是为了在字符串末尾添加一个\0。
//在完成字符串到字符数组的拷贝之后,会在字符串末尾加一个\0,这样可以复用C语言的一些函数。
sds sdsnewlen(const void *init, size\_t initlen) {
 void *sh;
 sds s;
 // 根据长度计算sds类型
    char type = sdsReqType(initlen);
 if (type == SDS\_TYPE\_5 && initlen == 0) type = SDS\_TYPE\_8; //为空时强制用sdshdr8
 // 获取结构体大小
    int hdrlen = sdsHdrSize(type);
 unsigned char *fp; /* flags pointer. */

    // 分配内存空间,初始大小为:struct结构体大小+字符串的长度+1,+1是为了在字符串末尾添加一个\0,兼容传统C语言
    sh = s\_malloc(hdrlen+initlen+1);
 // sh在这里指向了这个刚刚分配的内存地址
    if (sh == NULL) return NULL;
 // 判断是否是init阶段
    if (!init)
 //init 不为空的话,将sh这块内存全部设置为0
        memset(sh, 0, hdrlen+initlen+1);
 // 指向buf数组的指针
    s = (char*)sh+hdrlen;
 //因为可以看到地址的顺序是 len,alloc,flag,buf,目前s是指向buf,那么后退1位,fp 正好指向了flag对应的地址
    fp = ((unsigned char*)s)-1;
 // 类型选择
    switch(type) {
 case SDS\_TYPE\_5: {
 *fp = type | (initlen << SDS\_TYPE\_BITS);
 break;
 }
 case SDS\_TYPE\_8: {
 SDS\_HDR\_VAR(8,s);
 sh->len = initlen;
 sh->alloc = initlen;
 *fp = type;
 break;
 }
 case SDS\_TYPE\_16: {
 SDS\_HDR\_VAR(16,s);
 sh->len = initlen;
 sh->alloc = initlen;
 *fp = type;
 break;
 }
 case SDS\_TYPE\_32: {
 SDS\_HDR\_VAR(32,s);
 sh->len = initlen;
 sh->alloc = initlen;
 *fp = type;
 break;
 }
 case SDS\_TYPE\_64: {
 SDS\_HDR\_VAR(64,s);
 sh->len = initlen;
 sh->alloc = initlen;
 *fp = type;
 break;
 }
 }
 //如果两者都不为空,则init 这个对应的字符串,赋值给s
    if (initlen && init)
 memcpy(s, init, initlen); // 将字符串拷贝到buf数组
    s[initlen] = '\0';  // 字符串末尾添加一个\0
    return s;
}

// 获取结构体大小
static inline int sdsHdrSize(char type) {
 switch(type&SDS\_TYPE\_MASK) {
 case SDS\_TYPE\_5:
 return sizeof(struct sdshdr5);
 case SDS\_TYPE\_8:
 return sizeof(struct sdshdr8);
 case SDS\_TYPE\_16:
 return sizeof(struct sdshdr16);
 case SDS\_TYPE\_32:
 return sizeof(struct sdshdr32);
 case SDS\_TYPE\_64:
 return sizeof(struct sdshdr64);
 }
 return 0;
}

4)怎么防止操作时缓冲区溢出

//先检查 SDS 的空间是否满足修改所需的要求
//如果不满足要求的话,API 会自动将 SDS 的空间扩展到执行修改所需的大小
//最后才是返回,去执行实际的修改操作
sds sdscatlen(sds s, const void *t, size\_t len) {
 size\_t curlen = sdslen(s);  //获取s已经使用过的空间字符数

 s = sdsMakeRoomFor(s,len);  //扩大s的空闲空间
    if (s == NULL) return NULL; 
 memcpy(s+curlen, t, len);  //拷贝数据
    sdssetlen(s, curlen+len);  //设置s的len
    s[curlen+len] = '\0'; //最后加上空字符串
    return s;
}

5)分析是怎么扩容的

代码展示

// 扩容sds
sds sdsMakeRoomFor(sds s, size\_t addlen) {
 void *sh, *newsh;
 //获取剩余可用的空间
    size\_t avail = sdsavail(s);
 size\_t len, newlen;
 char type, oldtype = s[-1] & SDS\_TYPE\_MASK;
 int hdrlen;

 //如果可用空间大于需要增加的长度,那么直接返回
    if (avail >= addlen) return s;

 //len 已使用长度
    len = sdslen(s);
 //sh 回到指向了这个sds的起始位置。
    sh = (char*)s-sdsHdrSize(oldtype);
 // newlen 代表最小需要的长度
    newlen = (len+addlen);
 //Redis认为一旦被扩容了,那这个字符串被再次扩容的几率就很大,所以会在此基础上多加一些空间,防止频繁扩容
    if (newlen < SDS\_MAX\_PREALLOC)
 newlen *= 2;
 else
 newlen += SDS\_MAX\_PREALLOC;

 //获取新长度的类型
    type = sdsReqType(newlen);

 //如果是SDS\_TYPE\_5会被强行转为SDS\_TYPE\_8
    if (type == SD
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值