Redis【2】- SDS源码分析
1 简介&基础用法
Redis 中用得最多的就是字符串,在 C 语言中其实可以直接使用 char*
字符数组来实现字符串,也有很多可以直接使用得函数。但是 Redis 并没有使用 C 语言原生的字符串,而是自己实现了一个 SDS(简单动态字符串,Simple Dynamic String) 。
Redis 的 SDS 兼容了 C 语言的字符串类型的用法,
下面是 Redis 中 string 类型最常用的用法:
本地:0>set hello world
OK
本地:0>get hello
world
本地:0>type hello
string
本地:0>strlen hello
5
2 为什么 Redis 自实现字符串?
2.1 存储二进制的限制
C 语言的 char*
是以 \0
作为结束字符串的标识,如果需要存储的数据中本身就含有 \0
,那就没有办法正确表示,而像图像这种数据,一般存储下来都是二进制格式的,所以 Redis 不能直接使用 char*
。
下面是 C 语言的 \0
对字符串长度判断的影响:
#include "stdio.h"
#include "string.h" int main(void) { char *a = "hello\0Wolrd"; char *b = "helloWolrd\0"; printf("字符串的长度:%lu\n", strlen(a) ); printf("字符串的长度:%lu\n", strlen(b) );}
输出结果则会不一样,\0
后面的数据会被截断:
字符串的长度:5
字符串的长度:10
在 SDS 结构中却能保证二进制安全,因为 SDS 保存了 len 属性,这就可以不适用 \0
这个标识来判断字符串是否结束。
2.2 操作效率问题
2.2.1 空间效率
2.2.1.1 预分配内存
原生的 C 语言字符串,在添加的时候,可能会因为可用空间不足,无法添加,而 Redis 追加字符串的时候,使用了预分配的策略,如果内存不够,先进行内存拓展,再追加,有效减少修改字符串带来的内存重新分配的次数。
类似于 Java 中的 ArrayList,采取预分配,内部真实的容量一般都是大于实际的字符串的长度的,当字符串的长度小于 1MB 的时候,如果内存不够,扩容都是加倍现在的空间;如果字符串的长度已经超过了 1MB,扩容的时候也只会多扩 1MB 的空间,但是最大的字符串的长度是 512MB。
2.2.1.2 惰性空间释放
惰性空间释放用于优化 SDS 的字符串缩短操作,当 SDS 的 API 需要缩短字符串保存的字符串的时候,程序并不会立即使用内存重新分配来回缩短多出来的字节,而是使用 free 属性将这些字节的数量记录下来,并等待将来使用。
当然 SDS 也提供了 SDS 显式调用,真正的释放未使用的空间。
2.2.2 操作效率
原生的 C 语言在获取字符的长度的时候,底层实际是遍历,时间复杂度是 O(n)
,String 作为 Redis 用得最多的数据类型,获取字符串的长度是比较频繁的操作,肯定不能这么干,那就用一个变量把 String 的长度存储起来,获取的时候时间复杂度是 O(1)
。
2.2.3 兼容性较好
Redis 虽然使用了 \0
来结尾,但是 sds 字符串的末端还是会遵循 c 语言的惯例,所以可以重用一部分<string. h> 的函数。比如对比的函数 strcasecmp
,可以用来对比 SDS 保存的字符串是否和另外一个字符串是否相同。
strcasecmp(sds->buf,"hello world");
3 源码解读
3.1 简单指针介绍
数组的指针操作:
#include <stdio.h>
int main() {printf("Hello, World!\n");char t[] = {'a','b','c','d'};char* s = t+1; // 指针前进一位char bb = s[0];char cc = s[1];char dd = s[2];char flag = s[-1]; // 指针后退一位等价于 char flag = *(s - 1); 或者 char *flag = s - 1; printf("%c %c %c %c", *flag, bb, cc ,dd);printf("%c %c %c %c", flag, bb, cc ,dd);return 0;
}
最终输出结果:
Hello, World!
a b c d
3.1.1 sdshdr 巧妙的结构设计
SDS 的相关代码主要在下面两个文件:
- sds. h:头文件
- sds. c:源文件
SDS 定义在 sds. h
中,为了兼容 C 风格的字符串,给 char 取了个别名叫 sds
:
typedef char *sds;
《Redis 设计与实现》中,解释的是 Redis 3.0 的代码,提到 sds 的实现结构 sdshdr 是这样的:
struct sdshdr {// 记录buf数组已使用字节的数量// 等于SDS所保存字符串的长度int len;// 记录buf数组中未使用的字节数int free;// 字节数组,用于保存字符串char buf[];
};
但是实际上 7.0 版本已经是长这样:
-
- Sdshdr5 从未被使用,我们只是直接访问标志字节。
- 然而,这里文档标识 sdshdr5 的结构。
-
- 结构定义使用了__attribute__ ((packed))声明为非内存对齐, 紧凑排列形式(取消编译阶段的内存优化对齐功能)
-
- 如果定义了一个
sds *s
, 可以非常方便的使用s[-1]
获取到 flags 地址,避免了在上层调用各种类型判断。
- 如果定义了一个
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 低 3 位存储类型,高 5 位存储字符串长度 */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* 已使用 */uint8_t alloc; /* 总分配的,不包括头部和空的终止符*/unsigned char flags; /* 低 3 位存储类型,高 5 位预留,还没使用 */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* 已使用 */uint16_t alloc; /* 总分配的,不包括头部和空的终止符*/unsigned char flags; /* 低 3 位存储类型,高 5 位预留,还没使用 */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* 已使用 */uint32_t alloc; /* 总分配的,不包括头部和空的终止符*/unsigned char flags; /* 低 3 位存储类型,高 5 位预留,还没使用 */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* 总分配的,不包括头部和空的终止符*/unsigned char flags; /* 低 3 位存储类型,高 5 位预留,还没使用 */char buf[];
};// 类型定义一共占用了 0,1,2,3,4 五个数字,也就是三位就可以标识,
// 那么我们可以使用 flags&SDS_TYPE_MASK 来获取动态字符串对应的字符串类型#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
#define SDS_TYPE_MASK 7
#define SDS_TYPE_BITS 3
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
上面定义了 4 种结构体,**Redis 根据不同的字符串的长度,来选择合适的结构体,每个结构体有对应数据部分和头部。
类型一共有这些:
#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
用二进制表示,只需要 3 位即可,这也是为什么上面的结构体 sdshdr5
里面的 flags
字段注释里写的:前三位表示类型,后 5 位用于表示字符串长度。
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
而其他的 sds
结构体类型,因为长度太长了,存不下,所以后 5 位暂时没有作用,而是另外使用属性存储字符串的长度。
uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */
3.2 3.2 attribute 的作用是什么?
__attribute__ ((packed))
的作用就是告诉编译器取消结构在编译过程中的优化对齐, 按照实际占用字节数进行对齐,是 GCC 特有的语法。这个功能是跟操作系统没关系,跟编译器有关,gcc 编译器不是紧凑模式的。
attribute__关键字主要是用来在函数或数据声明中设置其属性。给函数赋给属性的主要目的在于让编译器进行优化。函数声明中的__attribute((noreturn)),就是告诉编译器这个函数不会返回给调用者,以便编译器在优化时去掉不必要的函数返回代码。
__attribute__书写特征是:__attribute__前后都有两个下划线,并且后面会紧跟一对括弧,括弧里面是相应的__attribute__参数,其语法格式为:
__attribute__ ((attribute-list))
下面是实验的一些代码,实验环境为 Mac:
#include "stdio.h" struct One{ char ch; int a;} one;
struct __attribute__ ((__packed__)) Tow{ char ch; int a;} tow; int main(void) { printf("int 的内存大小:%lu\n", sizeof(int) ); printf("新结构体one的大小(不压缩):%lu\n", sizeof(one) ); printf("新结构体tow的大小(压缩):%lu\n", sizeof(tow) );}
运行结果:
int 的内存大小:4
新结构体one的大小(不压缩):8
新结构体tow的大小(压缩):5
编译器压缩优化(内存不对齐)后,确实体积从 8 变成了 5,缩小了不少,别看这小小的变化,其实在巨大的数量面前,就是很大的空间优化。
3.3 宏操作
redis 基于前面 sds 设计,定义了一些十分巧妙的宏操作:
3.3.1 通过 sds 获取不同类型 sdshdr 变量
/*
* 宏操作
* SDS_HDR_VAR(8,s);
* 下面是对应宏定义翻译的产物
* struct sdshdr8 *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
* 可以根据指向 buf 的sds变量s得到 sdshdr8 的指针,sh 是创建出来的变量
*/
#define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));/**
* 和上面类似
* 根据指向buf的sds变量s得到sdshdr的指针,只不过这里是获取的是指针地址
*/
#define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
3.3.2 获取 sdshdr5 字符串类型的长度
/**
* 该函数就是获取sdshdr5字符串类型的长度,由于根本不使用sdshdr5类型,所以需要直接返回空,
* 而flags成员使用最低三位有效位来表示类型,所以让f代表的flags的值右移三位即可
*/
#define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
3.3.3 通过 sds 获取 len 的值
/**
* 使用到了取消编译阶段的内存优化对齐功能,直接使用s[-1]获取到flags成员的值,
* 然后根据flags&&SDS_TYPE_MASK来获取到动态字符串对应的类型进而获取动态字符串的长度。
* SDS_TYPE_5_LEN 比较特殊一点,因为结构有点不一样
*/
static inline size_t sdslen(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)->len;
case SDS_TYPE_16:
return SDS_HDR(16,s)->len;
case SDS_TYPE_32:
return SDS_HDR(32,s)->len;
case SDS_TYPE_64:
return SDS_HDR(64,s)->len;
}
return 0;
}
3.4 创建新字符串
创建新的字符串一般是传递初始化的长度:
sds sdsnewlen(const void *init, size_t initlen) { // 内部封装的函数,最后一个参数是是否尝试分配return _sdsnewlen(init, initlen, 0);
}
下面我们看具体的函数实现:
创建的返回的是指针,指向的是结构体中 buf 开始的位置,
sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) { // sh 指向sds分配开始的地方void *sh;// s 也是指针,指向 buf 开始的位置 sds s;// 不同长度返回不同的类型sdschar type = sdsReqType(initlen); /* Empty strings are usually created in order to append. Use type 8 * since type 5 is not good at this. */// 空字符串经常被创建出来之后,就会执行append操作,所以用type 8替换掉它,type 5 太短了。if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8; // 获取整个struct的长度int hdrlen = sdsHdrSize(type);// flag 指针,标识sds 是哪一个类型的 unsigned char *fp; /* flags pointer. */// 可用大小 size_t usable; // 防止溢出assert(initlen + hdrlen + 1 > initlen); /* Catch size_t overflow */ // 分配内存,其中s_trymalloc_usable是调整内存,s_malloc_usable是新分配内存,是两种内存分配的方式,通过参数trymalloc控制(+1 是为了处理 \0)sh = trymalloc? s_trymalloc_usable(hdrlen+initlen+1, &usable) : s_malloc_usable(hdrlen+initlen+1, &usable); // 分配不成功,提前结束if (sh == NULL) return NULL;// 如果需要完全为空的字符串,直接返回null if (init==SDS_NOINIT) init = NULL; else if (!init)memset(sh, 0, hdrlen+initlen+1); // 初始化 // s 指向数组 buf 的位置(从结构体往后加上hdrlen就是buf数组开头的位置) s = (char*)sh+hdrlen;// buf数组的位置-1,就是flags字段的位置 fp = ((unsigned char*)s)-1;// 可用空间减去hdrlen(已用空间),再减1(‘\0‘) usable = usable-hdrlen-1;// 如果可用空间大于当前结构体中alloc字段的大小,就使用alloc的最大值 if (usable > sdsTypeMaxSize(type)) usable = sdsTypeMaxSize(type);// 初始化不同类型的数组,字符串长度,可用大小和类型 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 = usable; *fp = type; break; } case SDS_TYPE_16: { SDS_HDR_VAR(16,s); sh->len = initlen; sh->alloc = usable; *fp = type; break; } case SDS_TYPE_32: { SDS_HDR_VAR(32,s); sh->len = initlen; sh->alloc = usable; *fp = type; break; } case SDS_TYPE_64: { SDS_HDR_VAR(64,s); sh->len = initlen; sh->alloc = usable; *fp = type; break; }}if (initlen && init) memcpy(s, init, initlen); s[initlen] = '\0'; return s;
}
3.5 获取可用空间
SDS 和我平常所用到的 C 语言的原生字符串有差别,因为从获取可用空间的计算方法来看,并未考虑到字符串需要以 \0
结尾,结构体本身带有长度的成员 len,不需要 \0
来做字符串结尾的判定,而且不使用 \0
作为结尾有很多好处, 分配的减去使用的即可。
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;
}
3.6 设置 & 增加 sds 的长度
// 设置 sds 的长度
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;}
}// 增加 sds 的长度
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;}
}
3.7 设置 & 获取已分配空间大小
/* sdsalloc() = sdsavail() + sdslen() */
// 获取 sds 已经分配的空间的大小
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;
}// 设置 sds 已经分配的空间的大小
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;}
}
3.8 扩大 sds 空间
/*** 扩大sds字符串末尾的空闲空间,以便调用者确信在调用此函数后可以覆盖到字符串末尾 addlen字节,再加上null term的一个字节。* 如果已经有足够的空闲空间,这个函数返回时不做任何操作,如果没有足够的空闲空间,它将分配缺失的部分,甚至更多:* 当greedy为1时,放大比需要的更多,以避免将来在增量增长时需要重新分配。* 当greedy为0时,将其放大到足够大以便为addlen腾出空间。* 注意:这不会改变sdslen()返回的sds字符串的长度,而只会改变我们拥有的空闲缓冲区空间。*/
// 扩大sds空间
sds _sdsMakeRoomFor(sds s, size_t addlen, int greedy) {void *sh, *newsh;// 获取剩余可用的空间size_t avail = sdsavail(s);size_t len, newlen, reqlen;// 获取sds 具体数据类型char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t usable;/* Return ASAP if there is enough space left. */// 可用空间足够直接返回if (avail >= addlen) return s;// 已用字符长度len = sdslen(s);// sh 回溯到sds起始位置sh = (char*)s-sdsHdrSize(oldtype);// newlen 为最小需要的长度reqlen = newlen = (len+addlen);assert(newlen > len); /* Catch size_t overflow */// 在newlen小于SDS_MAX_PREALLOC(1M),对newlen进行翻倍,在newlen大于SDS_MAX_PREALLOC的情况下,让newlen加上SDS_MAX_PREALLOC。if (greedy == 1) {if (newlen < SDS_MAX_PREALLOC) // 小于1Kb 预分配2倍长度 = newlen + newlennewlen *= 2;elsenewlen += SDS_MAX_PREALLOC; // 多余1Mb 预分配 = newlen + 1Mb}// 获取新长度的类型type = sdsReqType(newlen);/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;// 新类型头部长度hdrlen = sdsHdrSize(type);// 校验是否溢出assert(hdrlen + newlen + 1 > reqlen); /* Catch size_t overflow */if (oldtype==type) {/*** 本质上是 使用 zrealloc_usable函数,指针ptr必须为指向堆内存空间的指针,即由malloc函数、calloc函数或realloc函数分配空间的指针。* realloc函数将指针p指向的内存块的大小改变为n字节。* 1.如果n小于或等于p之前指向的空间大小,那么。保持原有状态不变。* 2.如果n大于原来p之前指向的空间大小,那么,系统将重新为p从堆上分配一块大小为n的内存空间,同时,将原来指向空间的内容依次复制到新的内存空间上,p之前指向的空间被释放。* relloc函数分配的空间也是未初始化的。*/newsh = s_realloc_usable(sh, hdrlen+newlen+1, &usable);// 申请空间失败if (newsh == NULL) return NULL;// s指向新sds结构的buf开始位置s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */// 数据结构发生变更,协议头部变更,需要从堆上重新申请数据空间newsh = s_malloc_usable(hdrlen+newlen+1, &usable);if (newsh == NULL) return NULL;// 系统copy,越过头部结构长度,复制s的有效数据集合memcpy((char*)newsh+hdrlen, s, len+1);// 释放旧空间s_free(sh);// s执行新的空间,buf起始位置s = (char*)newsh+hdrlen;// flag 赋值 头部的第三个有效字段s[-1] = type;// 更新有效数据长度sdssetlen(s, len);}// 实际可用数据空间usable = usable-hdrlen-1;if (usable > sdsTypeMaxSize(type))usable = sdsTypeMaxSize(type);// 更新分配的空间值sdssetalloc(s, usable);return s;
}
3.9 释放多余空间
/* 对sds中多余的空间进行释放
* 重新分配sds字符串,使其末尾没有空闲空间。所包含的字符串保持不变,
* 但下一个连接操作将需要重新分配。
* 调用之后,传递的sds字符串不再有效,所有引用必须用调用返回的新指针替换。
*/
sds sdsRemoveFreeSpace(sds s, int would_regrow) { return sdsResize(s, sdslen(s), would_regrow);
}
/*** 调整分配的大小,这可以使分配更大或更小,如果大小小于当前使用的len,数据将被截断。* 当将d_regrow参数设置为1时,它会阻止使用SDS_TYPE_5,这是在sds可能再次更改时所需要的。* 无论实际分配大小如何,sdsAlloc大小都将被设置为请求的大小,这样做是为了避免在调用者检测到它有多余的空间时重复调用该函数*/
sds sdsResize(sds s, size_t size, int would_regrow) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen, oldhdrlen = sdsHdrSize(oldtype);size_t len = sdslen(s);sh = (char*)s-oldhdrlen;/* Return ASAP if the size is already good. */if (sdsalloc(s) == size) return s;/* Truncate len if needed. */if (size < len) len = size;/* Check what would be the minimum SDS header that is just good enough to* fit this string. */type = sdsReqType(size);if (would_regrow) {/* Don't use type 5, it is not good for strings that are expected to grow back. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;}hdrlen = sdsHdrSize(type);/* If the type is the same, or can hold the size in it with low overhead* (larger than SDS_TYPE_8), we just realloc(), letting the allocator* to do the copy only if really needed. Otherwise if the change is* huge, we manually reallocate the string to use the different header* type. */int use_realloc = (oldtype==type || (type < oldtype && type > SDS_TYPE_8));size_t newlen = use_realloc ? oldhdrlen+size+1 : hdrlen+size+1;int alloc_already_optimal = 0;#if defined(USE_JEMALLOC)/* je_nallocx returns the expected allocation size for the newlen.* We aim to avoid calling realloc() when using Jemalloc if there is no* change in the allocation size, as it incurs a cost even if the* allocation size stays the same. */alloc_already_optimal = (je_nallocx(newlen, 0) == zmalloc_size(sh));#endifif (use_realloc && !alloc_already_optimal) {newsh = s_realloc(sh, newlen);if (newsh == NULL) return NULL;s = (char*)newsh+oldhdrlen;} else if (!alloc_already_optimal) {newsh = s_malloc(newlen);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;}s[len] = 0;sdssetlen(s, len);sdssetalloc(s, size);return s;
}
3.10 拼接字符串
将一个字符串拼接到原 sds 后面:
sds sdscatlen(sds s, const void *t, size_t len) {// 现在长度size_t curlen = sdslen(s);// 扩容s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;// 复制memcpy(s+curlen, t, len);// 设置长度sdssetlen(s, curlen+len);// 结尾'\0's[curlen+len] = '\0';return s;
}
3.11 拷贝
sds sdscpylen(sds s, const char *t, size_t len) {// 长度不够需要扩容if (sdsalloc(s) < len) {s = sdsMakeRoomFor(s,len-sdslen(s));if (s == NULL) return NULL;}// 复制memcpy(s, t, len);// 末尾 '\0's[len] = '\0';// 设置长度sdssetlen(s, len);return s;
}
4 SDS 的优点
-
- 获取字符串的时间效率为
O (1)
- 获取字符串的时间效率为
-
- 杜绝缓冲区的溢出,复制或者追加字符串之前,会对空间进行检查与拓展,并且预分配一些容量,减少分片内存的次数。
-
- 可以存储二进制数据,含有
\0
则在读取时不会被截断。
- 可以存储二进制数据,含有
-
- 可以复用一部分 c 原生字符串的函数。
作者: 秦怀,个人站点 秦怀杂货店,纵使缓慢,驰而不息。
相关文章:
Redis【2】- SDS源码分析
1 简介&基础用法 Redis 中用得最多的就是字符串,在 C 语言中其实可以直接使用 char* 字符数组来实现字符串,也有很多可以直接使用得函数。但是 Redis 并没有使用 C 语言原生的字符串,而是自己实现了一个 SDS(简单动态字符串&…...
力扣打卡8:最长上升子序列
链接:300. 最长递增子序列 - 力扣(LeetCode) 本题我开始想到的是dp,复杂度为O(n^2),这也是很经典的解法。 看到进阶解法可以O(nlogn),想到可能是要用到二分,但是,我想到的是和map排…...
记录一次老平台改造通知用户刷新页面,纯前端实现
记录一次老平台改造通知用户刷新页面,纯前端实现 方案概述背景现状问题本质 方案设计前提设计实现 其他补充写在最后的话抛出一个问题 方案概述 背景 前端构建完上线,用户还停留还在老页面,用户不知道网页重新部署了,跳转页面的时…...
ubuntu22.04 使用可以用的镜像源获取你要的镜像
默认的是不行的 不管pull啥镜像 仍然会出现这个错误 Error response form daemon:Get "https://registry-1.docker.io/v2": net/http: request canceled while waiting for connection (Client.Timeout exceeded while await) 操作方法是 如果在目录没有/etc/docker…...
Chrome扩展程序开发示例
项目文件夹内文件如下: manifest.json文件内容: {"manifest_version": 3,"name": "我的法宝","description": "我的有魔法的宝贝","version": "1.0","icons": {"…...
Linux 下使用飞鸽传书实现与Windows飞秋的通信
最近把单位的办公电脑换成Linux系统,但是其他同事们都使用飞秋2013进行局域网通信和文件传输,经过一番尝试,发现飞鸽传书For Linux 2014能够实现两者的互相通信。 飞鸽传书ForLINUXLinux版下载_飞鸽传书ForLINUX免费下载_飞鸽传书ForLINUX1.2…...
docker批量创建cloudstack虚拟主机脚本
批量创建cloudstack脚本 #!/bin/bash # 配置变量 container_prefix"cloudworker-" base_ip"192.168.1." start_ip2 #开始ip start_container2 #上同 end_container4 #结束ip 包括 network_name"my_macvlan_network" image_name"dockedahi:…...
SpringBoot项目集成MinIO
最近在学习MinIO,所以想让自己的SpringBoot项目集成MinIO,在网上查阅资料,并进行操作的过程中遇到一些问题,所以想把自己遇到的坑和完成步骤记录下来供自己和各位查阅。 一. MinIO的下载安装以及基本使用 1. 下载地址:https://d…...
【Flutter】常用样式、方法、组件(长期更新中)
一、样式设置 设置颜色透明度:color: Color(0xff4B9E32).withOpacity(0.08) 二、常用方法 数组排序:list.sort(); **升序**:(obj1, obj2) > obj1.compareTo(obj2) **降序**:(obj1, obj2) > obj2.compareTo(obj1)obj1.co…...
dbus接口方法的variant类型传参详解
python实现c++中so库调用及dbus服务开发-CSDN博客 之前写的这篇博文介绍了如何创建一个dbus服务,但是注册的接口方法的入参还是比较简单的,实际上dbus的参数类型有很多种,调用方式也有多种,我们来逐一介绍下。 其实基础数据类型,如字符串、整型、浮点型、布尔型等大多数…...
【时时三省】(NIT计算机考试)Word的使用方法
山不在高,有仙则名。水不在深,有龙则灵。 ----CSDN 时时三省 一、软件简介 Microsoft Word,简称Word,是微软公司开发的一款文字处理软件,广泛应用于文档编辑、排版、打印等领域。无论是撰写论文、报告、简历…...
spring技术点
引入对象 Autowired 和 Resource的区别 Autowired 和 Resource的区别 valid 参数校验 jarkata进行SpringMVC校验 常规当前进行校验的配置操作,参考文档如下进行操作。 SpringMVC校验注解不生效 List类型参数校验 由于list类型默认不能进行标注校验实现&#x…...
工业—使用Flink处理Kafka中的数据_ChangeRecord1
使用 Flink 消费 Kafka 中 ChangeRecord 主题的数据,当某设备 30 秒状态连续为 “ 预警 ” ,输出预警 信息。当前预警信息输出后,最近30...
实验日志——DETR
DETR训练日志 1. 代码来源 代码源自作者的Github: https://github.com/facebookresearch/detr?tabreadme-ov-file 2. 数据来源 在DETR中只使用了COCO2017数据集,其中训练集有118288张图像,验证集有5001张数据,测试集有40671张数据&#…...
前端常用缓存技术深度剖析
🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…...
汽车IVI中控开发入门及进阶(三十七):基于HFP协议的蓝牙电话
概述: HFP全称Hands-free Profile,是一款让蓝牙设备控制电话的软件,多用于汽车上。此类设备最常见的例子是车载免提装置与蜂窝电话或可穿戴无线耳机一起使用。该配置文件定义了支持免提配置文件的两个设备如何在点对点的基础上相互交互。免提模式的实现通常使耳机或嵌入式免…...
分布式系统架构1:共识算法Paxos
1.背景 今天开始更新分布式的文章,工作几年后还没系统的学习分布式的内容,趁着还有时间学习沉淀的时候多输出些文章 2.为什么需要分布式共识算法 思考:现在你有一份随时变动的数据,需要确保它正确存储在网络的几台不同机器上&a…...
大语言模型应用Text2SQL本地部署实践初探
自从两年前OpenAI公司发布ChatGPT后,大模型(Large Language Model,简称LLM)相关技术在国内外可谓百家争鸣,遍地开花,在传统数据挖掘、机器学习和深度学习的基础上,正式宣告进入快速发展的人工智能(Artificial Intellig…...
C# WPF抽奖程序
C# WPF抽奖程序 using Microsoft.Win32; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.…...
linux运维命令
防火墙相关命令 防火墙规则查看 firewall-cmd --list-all 禁ping firewall-cmd --permanent --add-rich-rulerule protocol valueicmp drop firewall-cmd --reload 执行完以上命令后,通过firewall-cmd --list-all查看规则生效情况 firewall-cmd --list-all 其…...
环境兼容: Vue3+ELement-plus
题目:环境兼容: Vue3ELement-plus 前言 身为小白的我也在负责一个项目咯,开发的是Vue3项目,然后就搜阅多篇文章,整理了这个。内容很多是转载的,拼成的我这个文章。 Element-plus简介 Element-plus 是基于…...
解决 PyTorch 中的 AttributeError: ‘NoneType‘ object has no attribute ‘reshape‘ 错误
这里写目录标题 一、错误分析二、错误原因三、解决方案1. 检查损失函数2. 检查前向传播3. 检查 backward 函数4. 检查梯度传递 四、前向传播与反向传播1. 前向传播2. 反向传播3. 自定义 backward 函数示例反向传播过程:常见的错误:1:损失函数…...
Unity 设计模式-命令模式(Command Pattern)详解
命令模式(Command Pattern)是一种行为型设计模式,它将请求封装成对象,从而使得可以使用不同的请求、队列或日志请求,以及支持可撤销的操作。命令模式通常包含四个主要角色:命令(Command…...
如何解决maven项目使用Ctrl + /添加注释时的顶格问题
一、问题描述 相信后端开发的程序员一定很熟悉IDEA编译器和Maven脚手架,使用IDEA新建一个Maven工程,通过SpringBoot快速构建Spring项目。在Spring项目pom.xml文件中想添加注释,快捷键Ctrl /,但是总是顶格书写。 想保证缩进统一…...
网络安全信息收集(总结)更新
目录 重点: 前言: 又学到了,就是我们什么时候要子域名收集,什么时候收集域名,重点应该放前面 思考: 信息收集分为哪几类,什么是主域名,为什么要收集主域名,为什么要收…...
微服务-seata分布式事务
1.简述 1.1.什么是分布式事务 事务:是应用程序中一系列严密的操作,所有操作必须成功完成,要么全部失败,ACID 特性。本地事务:关系型数据库中,由一组SQL组成的一个执行单元,该单元要么整体成功,要么整体失败ÿ…...
(亲测好用)YOLO格式txt数据集转COCO格式json
1、数据集结构形式 YOLO格式数据集: b文件夹下有images和labels两个文件夹,分别存放图片和标签格式的数据。 两个文件夹下分别有train、val、test三个文件夹,里面存放对应的数据。 COCO数据集格式: COCO格式数据文件夹下有三个…...
LVS的DR模式是否依赖内核的数据包转发
LVS的DR模式是否依赖内核的数据包转发 是的,LVS(Linux Virtual Server) 的 DR(Direct Routing)模式 依赖于 内核的数据包转发。在 DR 模式下,数据包的转发行为是由 Linux 内核进行的,因此正确配…...
沿着数组的指定轴对每行(列)应用一个函数np.apply_along_axis
【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 沿着数组的指定轴 对每行(列)应用一个函数 np.apply_along_axis [太阳]选择题 根据题目代码,执行的结果是? import numpy as np array np.array([[1, 2, 3],…...
BEPUphysicsint定点数3D物理引擎使用
原文:BEPUphysicsint定点数3D物理引擎使用 - 哔哩哔哩 上一节給大家介绍了BEPUphysicsint的一些基本的情况,这节课我们来介绍它的基本使用,本节主要从以下5个方面来介绍: (1) 创建一个物理世界Space,并开启模拟迭代; (2) 添加一个物理物体…...
LinuxUDP编程
由于UDP是无连接、尽力传输的,所以Server端绑定完IP、端口号后,使用recvfrom可以阻塞等待客户端的数据,而且Client端通过sendto发送的数据包直接发送到互联网(也是基于IP、端口号)这种操作是不担保Server端是否收到的&…...
Elasticsearch vs 向量数据库:寻找最佳混合检索方案
图片来自Shutterstock上的Bakhtiar Zein 多年来,以Elasticsearch为代表的基于全文检索的搜索方案,一直是搜索和推荐引擎等信息检索系统的默认选择。但传统的全文搜索只能提供基于关键字匹配的精确结果,例如找到包含特殊名词“Python3.9”的文…...
Android显示系统(07)- OpenGL ES - 纹理Texture
Android显示系统(02)- OpenGL ES - 概述 Android显示系统(03)- OpenGL ES - GLSurfaceView的使用 Android显示系统(04)- OpenGL ES - Shader绘制三角形 Android显示系统(05)- OpenGL…...
C#加速Bitmap存图
如果希望大幅提高图像保存速度,特别是在处理非常大的图像时,可以尝试以下更直接、更高效的方法: 1. 避免使用 Bitmap 类的 Save 方法 Bitmap.Save 方法的速度受限于 GDI 库的操作,尤其是对于非常大的图像,它可能会经历…...
打通Vue3+Flask(python3)+Mysql-实现简单数据交互
一、需要准备的工具 下载python3,Vscode,pycharm(这里用的社区版),phpstudy_pro,Node.js(建议下载长期支持版本,版本不宜过低,比如18,20),Vue.js…...
PT8M2102 触控型 8Bit MCU
1 产品概述 ● PT8M2102 是一款基于 RISC 内核的8位 MTP 单片机,内部集成了电容式触摸感应模块、TIMER,PWM、LVR、LVD、WDT等外设,其主要用作触摸按键开关,广泛适用于触控调光、电子玩具、消费电子、家用电器等领域,具…...
【PyQt5教程 一】Qt Designer 安装及其使用方法说明,附程序源码
目录 一、PyQt5介绍: (1)PyQt简介: (2)PyQt API: (3)支持的环境: (4)安装: (5)配置环境变量…...
Spark on Yarn安装配置,大数据技能竞赛(容器环境)
Spark on Yarn模式,即把Spark作为一个客户端,将作业提交给Yarn服务,由于在生产环境中,很多时候都要与Hadoop使用同一个集群,因此采用Yarn来管理资源调度,可以有效提高资源利用率。 环境说明: 服…...
★ 数据结构 ★ 排序
Ciallo~(∠・ω< )⌒☆ ~ 今天,我将和大家一起学习数据结构中的各种排序~ ❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️❄️ 澄岚主页:椎名澄嵐-CSDN博客 数据结构专栏:https://blog.csdn.net/2302_80328146/categ…...
数据结构 (26)图的遍历
前言 数据结构中的图遍历是指从图中的任一顶点出发,按照某种方法访问图中的所有顶点,且每个顶点只访问一次。 一、遍历方法 遍历主要有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS)。 1.深度…...
用vue框架写一个时钟的页面
你可以使用Vue框架来创建一个简单的时钟页面。首先,你需要在HTML文件中引入Vue框架的CDN: <script src"https://cdn.jsdelivr.net/npm/vue"></script>然后,创建一个包含时钟功能的Vue实例: <div id&qu…...
【Android】View的工作流程——measure
1.View的工作流程入口 1.1DecorView被加载到Window中 看到这里你对Activity的构成有一定的了解,每个 Activity 都有一个与之关联的 Window 对象,而 DecorView 是这个 Window 的根视图。当DecorView被创建以及加载资源的时候,此时它的内容还…...
day35—蓝桥杯2024年第16届校赛模拟第二期-T4(最小花费)
【问题描述】 小蓝有一个整数,初始值为 1 ,他可以花费一些代价对这个整数进行变换。 小蓝可以花费 1 的代价将整数增加 1 。 小蓝可以花费 3 的代价将整数增加一个值,这个值是整数的数位中最大的那个(1 到 9)。 小蓝可…...
Java 中 List 接口的学习笔记
1. 什么是 List? 在 Java 中,List 是一个接口,属于 Java Collections Framework。它表示一个有序的集合,可以包含重复元素。List 接口允许通过索引访问元素,提供了多种实现方式,如 ArrayList 和 LinkedLis…...
朗新科技集团如何用云消息队列 RocketMQ 版“快、准、狠”破解业务难题?
作者:邹星宇、刘尧 朗新科技集团:让数字化的世界更美好 朗新科技集团股份有限公司是领先的能源科技企业,长期深耕电力能源领域,通过新一代数字化、人工智能、物联网、电力电子技术等新质生产力,服务城市、产业、生活中…...
hive hms和hs2的sql执行日志分析
HMS日志: 2024-12-02 15:39:20,811 INFO org.apache.hadoop.hive.metastore.HiveMetaStore: [pool-8-thread-114]: 114: source:10.11.17.999 get_all_databases 2024-12-02 15:39:20,812 INFO org.apache.hadoop.hive.metastore.HiveMetaStore.audit: [pool-8-thre…...
Mybatis-plus 多租户插件
前言 本篇主要分析Mybatis-plus 多租户插件,然后根据多租户插件在延伸到其他场景 案例 Mybatis-plus官网对多租户插件已有详细讲解,这里就不在附上使用案例。 源码分析 MybatisPlus官方是由TenantLineInnerInterceptor这个拦截器进行多租户功能处理…...
浅谈新能源汽车感应钥匙一键启动的步骤和特点
随着汽车智能化技术的发展,无钥匙启动系统还可以与其他智能系统进行集成,如智能车载系统、远程控制系统等。这使得车主可以通过智能手机等智能设备远程控制车辆的启动、解锁、上锁等操作,进一步提升了使用的便捷性和智能化水平。新能源汽车…...
012 路由信息协议RIP
路由信息协议RIP 作为度量(Metric)来衡量到达目的网络的距离 RIP是一种基于距离矢量D-V(Distance-Vector)算法的协议,它使用跳数(Hop Count)作为度量(Metric)来衡量到达目的网络的距离。 默认情况下,路由器到与它直接相连网络的跳数为0,因此…...
008.精读《Apache Paimon Docs - Table w/o PK》
文章目录 1. 引言2. 基本概念2.1 定义2.2 使用场景 3. 流式处理3.1 自动小文件合并3.2 流式查询 4. 数据更新4.1 查询4.2 更新4.3 分桶附加表 5 总结 1. 引言 通过本文,上篇我们了解了Apache Paimon 主键表,本期我们将继续学习附加表(Append…...