2024年Go最全redis源码阅读—sds_redis sdshdr5,sdshdr8,2024年最新字节大牛教你手撕Golang学习

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];

};
struct attribute ((packed)) sdshdr16 {
uint16_t len; /* used /
uint16_t alloc; /
excluding the header and null terminator /
unsigned char flags; /
3 lsb of type, 5 unused bits /
char buf[];
};
struct attribute ((packed)) sdshdr32 {
uint32_t len; /
used /
uint32_t alloc; /
excluding the header and null terminator /
unsigned char flags; /
3 lsb of type, 5 unused bits /
char buf[];
};
struct attribute ((packed)) sdshdr64 {
uint64_t len; /
used /
uint64_t alloc; /
excluding the header and null terminator /
unsigned char flags; /
3 lsb of type, 5 unused bits */
char buf[];
};


**这部分源码没啥特别,就不多说了**


#### sds逻辑图


假设我们设置某个字符串为hello,那么他SDS的可用长度len为8,已用长度len为6,如下图。注意:Redis会根据具体的字符长度,选择相应的sdshdr,但是各个类型都差不多,所以下图加简单画了。  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/20201226184128454.png)


#### sds优势


##### 更快的获取字符串长度


在开发语言中,一般会提供方法直接获取大小。但是C却不一样,更偏向底层实现,所以没有直接的方法使用。这样就带来一个问题,如果我们想要获取某个数组的长度,就只能从头开始遍历,当遇到第一个’\0’则表示该数组结束。这样的速度太慢了,不能每次因为要获取长度就变量数组。所以设计了SDS数据结构,在原来的字符数组外面增加总长度,和已用长度,这样每次直接获取已用长度即可。复杂度为O(1)。


通过使用 SDS 而不是 C 字符串, Redis 将获取字符串长度所需的复杂度从 O(N) 降低到了 O(1) , 这确保了获取字符串长度的工作不会成为 Redis 的性能瓶颈。比如说, 因为字符串键在底层使用 SDS 来实现, 所以即使我们对一个非常长的字符串键反复执行 STRLEN 命令, 也不会对系统性能造成任何影响, 因为 STRLEN 命令的复杂度仅为 O(1) 。


##### 杜绝缓冲区溢出


SDS 的空间分配策略完全杜绝了发生缓冲区溢出的可能性: 当 SDS API 需要对 SDS 进行修改时, API 会先检查 SDS 的空间是否满足修改所需的要求, 如果不满足的话, API 会自动将 SDS 的空间扩展至执行修改所需的大小, 然后才执行实际的修改操作, 所以使用 SDS 既不需要手动修改 SDS 的空间大小, 也不会出现前面所说的缓冲区溢出问题。


举个例子, SDS 的 API 里面也有一个用于执行拼接操作的 sdscat 函数, 它可以将一个 C 字符串拼接到给定 SDS 所保存的字符串的后面, 但是在执行拼接操作之前, sdscat 会先检查给定 SDS 的空间是否足够, 如果不够的话, sdscat 就会先扩展 SDS 的空间, 然后才执行拼接操作。


##### 减少修改字符串时带来的内存重分配次数


* 空间预分配

 空间预分配用于优化 SDS 的字符串增长操作: 当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。

 其中, 额外分配的未使用空间数量由以下公式决定:


	+ 如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len 将变成 13 字节, 那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
	+ 如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间。 举个例子, 如果进行修改之后, SDS 的 len 将变成 30 MB , 那么程序会分配 1 MB 的未使用空间, SDS 的 buf 数组的实际长度将为 30 MB + 1 MB + 1 byte 。通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。
* 惰性空间释放

 惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。


##### 二进制安全


如果传统字符串保存图片,视频等二进制文件,中间可能出现’\0’,如果按照原来的逻辑,会造成数据丢失。所以可以用已用长度来表示是否字符数组已结束。


#### 关键代码


##### 获取常见值


在sds.h中写了一些常见方法,比如计算sds的长度(即sdshdr的len),计算sds的空闲长度(即sdshdr的可用长度alloc-已用长度len),计算sds的可用长度(即sdshdr的alloc)等等。但是大家有没有疑问,这不是一行代码搞定的事吗,为啥要抽象出方法呢?那么问题在于在上面,我们有将sdshdr分为五种类型,分别是sdshdr5,sdshdr8,sdshdr16,sdshdr32,sdshdr64。那么我们在实际使用的时候,想要区分当前是哪个类型,并取其相应字段或设置相应字段。



//计算sds对应的字符串长度,其实上取得是字符串所对应的哪种sdshdr的len值
static inline size_t sdslen(const sds s) {
// 柔性数组不占空间,所以倒数第二位的是flags
unsigned char flags = s[-1];
//flags与上面定义的宏变量7做位运算
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5://0
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8://1
return SDS_HDR(8,s)->len;//取上面结构体sdshdr8的len
case SDS_TYPE_16://2
return SDS_HDR(16,s)->len;
case SDS_TYPE_32://3
return SDS_HDR(32,s)->len;
case SDS_TYPE_64://5
return SDS_HDR(64,s)->len;
}
return 0;
}

//计算sds对应的空余长度,其实上是alloc-len
static inline size_t sdsavail(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5: {
return 0;
}
case SDS_TYPE_8: {
SDS_HDR_VAR(8,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_16: {
SDS_HDR_VAR(16,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_32: {
SDS_HDR_VAR(32,s);
return sh->alloc - sh->len;
}
case SDS_TYPE_64: {
SDS_HDR_VAR(64,s);
return sh->alloc - sh->len;
}
}
return 0;
}

//设置sdshdr的len
static inline void sdssetlen(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char fp = ((unsigned char)s)-1;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len = newlen;
break;
}
}

//给sdshdr的len添加多少大小
static inline void sdsinclen(sds s, size_t inc) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
{
unsigned char fp = ((unsigned char)s)-1;
unsigned char newlen = SDS_TYPE_5_LEN(flags)+inc;
*fp = SDS_TYPE_5 | (newlen << SDS_TYPE_BITS);
}
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->len += inc;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->len += inc;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->len += inc;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->len += inc;
break;
}
}

//获取sdshdr的总长度
static inline size_t sdsalloc(const sds s) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
return SDS_TYPE_5_LEN(flags);
case SDS_TYPE_8:
return SDS_HDR(8,s)->alloc;
case SDS_TYPE_16:
return SDS_HDR(16,s)->alloc;
case SDS_TYPE_32:
return SDS_HDR(32,s)->alloc;
case SDS_TYPE_64:
return SDS_HDR(64,s)->alloc;
}
return 0;
}

//设置sdshdr的总长度
static inline void sdssetalloc(sds s, size_t newlen) {
unsigned char flags = s[-1];
switch(flags&SDS_TYPE_MASK) {
case SDS_TYPE_5:
/* Nothing to do, this type has no total allocation info. */
break;
case SDS_TYPE_8:
SDS_HDR(8,s)->alloc = newlen;
break;
case SDS_TYPE_16:
SDS_HDR(16,s)->alloc = newlen;
break;
case SDS_TYPE_32:
SDS_HDR(32,s)->alloc = newlen;
break;
case SDS_TYPE_64:
SDS_HDR(64,s)->alloc = newlen;
break;
}
}


##### 创建对象


我们通过sdsnew方法来创建对象,显示通过判断init是否为空来确定初始大小,接着调用方法sdsnew(这边方法名一样,但是参数不一样,其为方法的重载),先根据长度确定类型(上面有提过五种类型,不记得的可以往上翻),然后根据类型分配相应的内存资源,最后追加C语言的结尾符’\0’。



sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}

sds sdsnewlen(const void *init, size_t initlen) {
void *sh;
sds s;
char type = sdsReqType(initlen);//根据长度确定类型
/*空字符串,用sdshdr8,这边是经验写法,当想构造空串是为了放入超过32长度的字符串 */
if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
int hdrlen = sdsHdrSize(type);//到下一个方法,已经把他们放在一起了
unsigned char fp; / flags pointer. */

//分配内存 
sh = s_malloc(hdrlen+initlen+1);
if (!init)
    memset(sh, 0, hdrlen+initlen+1);
if (sh == NULL) return NULL;
s = (char*)sh+hdrlen;
fp = ((unsigned char*)s)-1;
//根据不同的类型,创建不同结构体,调用SDS_HDR_VAR函数
//为不同的结构体赋值,如已用长度len,总长度alloc 
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;
    }
}
if (initlen && init)
    memcpy(s, init, initlen);
//最后追加'\0' 
s[initlen] = '\0';
return s;

}

//根据实际字符长度确定类型
static inline char sdsReqType(size_t string_size) {
if (string_size < 1<<5)
return SDS_TYPE_5;

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

中…(img-lR6Y66Kg-1715706967902)]
[外链图片转存中…(img-oUsMi7P3-1715706967902)]
[外链图片转存中…(img-P4ZzvW9J-1715706967902)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值