redis学习笔记
基于 Redis 6.0.4
1 2 redis / src / sds.h redis / src / sds.c
介绍 简单动态字符串 (simple dynamic string, SDS) 是 Redis 基本的底层抽象数据结构之一,用来替代 c 字符串作为默认的字符串使用。
结构 struct sdshdr 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 struct sdshdr { uintx_t len; uintx_t alloc; unsigned char flags; char buf[]; } struct sdshdr { int len; int free ; char buf[]; } struct __attribute__ ((__packed__ )) sdshdr5 { unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr8 { uint8_t len; uint8_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr16 { uint16_t len; uint16_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr32 { uint32_t len; uint32_t alloc; unsigned char flags; char buf[]; }; struct __attribute__ ((__packed__ )) sdshdr64 { uint64_t len; uint64_t alloc; unsigned char flags; char buf[]; };
__attribute__ ((__packed__)) 作用是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐——即取消 4k 对齐而是以 1byte 来对齐。这是 GCC 特有的语法,保证 header 和 sds 的数据部分紧紧前后相邻,可以按照固定向低地址方向偏移 1 个字节的方式来获取 flags 字段。
在各个 header 的定义中最后有一个 char buf[]。注意到这是一个没有指明长度的字符数组,这是 C 语言中定义字符数组的一种特殊写法,它在这里只是起到一个标记的作用,表示在 flags 字段后面就是一个字符数组,而程序在为 header 分配的内存的时候,它并不占用内存空间。如果计算 sizeof(struct sdshdr16) 的值,那么结果是 5 个字节,其中没有 buf 字段。
sdshdr5 与其它几个 header 结构不同,它不包含 alloc 字段,而长度使用 flags 的高 5 位来存储。因此,它不能为字符串分配空余空间,如果字符串需要动态增长,那么它就必然要重新分配内存才行。所以这种类型的 sds 字符串更适合存储静态的短字符串 (长度小于 32)。主要目的是减小内存使用开销。
sds sds 就是 char*,指向字符串数据的开始。
部分函数实现 SDS_HDR_VAR() 创建一个 struct sdshdrX* sh 变量,并指向 sds s 的 sdshdrX
1 #define SDS_HDR_VAR(T,s) struct sdshdr##T *sh = (void*)((s)-(sizeof(struct sdshdr##T)));
SDS_HDR() 返回一个指向 sds s 的 sdshdrX 的指针
1 #define SDS_HDR(T,s) ((struct sdshdr##T *)((s)-(sizeof(struct sdshdr##T))))
SDS_TYPE_5_LEN() flags 右移 3 位得到数据部分长度大小
1 #define SDS_TYPE_5_LEN(f) ((f)>>SDS_TYPE_BITS)
sdslen() 获取 sds 对应的数据部分的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 ; }
sdsavail() 获取 sds 剩余可使用空间大小。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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 ; }
sdssetlen() 设置 sdshdr 的 len。
注意这个函数并不安全,因为它没有检测 s 的大小,如果不同步和 s 相关内容一起设置可能会导致溢出问题。sdsinclen()、sdssetalloc()也一样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 ; } }
sdsnewlen() 创建新的 sds。
这个函数不会检测输入的 init 的有效性,因为 init 可以为 NULL,此时会创建一个长度为 initlen 的被初始化的空 sds。init 还可以是 SDS_NOINIT,此时 init 也代表为 NULL,不过这时 sdsnewlen() 不会对申请出来的内存进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 sds sdsnewlen (const void *init, size_t initlen) { void *sh; sds s; char type = sdsReqType(initlen); if (type == SDS_TYPE_5 && initlen == 0 ) type = SDS_TYPE_8; int hdrlen = sdsHdrSize(type); unsigned char *fp; sh = s_malloc(hdrlen+initlen+1 ); if (sh == NULL ) return NULL ; if (init==SDS_NOINIT) init = NULL ; else if (!init) memset (sh, 0 , hdrlen+initlen+1 ); s = (char *)sh+hdrlen; 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 ; } } if (initlen && init) memcpy (s, init, initlen); s[initlen] = '\0' ; return s; }
sdsfree() 释放 sds。
1 2 3 4 5 6 void sdsfree (sds s) { if (s == NULL ) return ; s_free((char *)s-sdsHdrSize(s[-1 ])); }
辅助函数 sdsReqType() 根据输入的大小计算必要的数据类型,不同的数据类型有不同的最大长度限制,可以做到提高内存使用率的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 }
总结 SDS 遵循 C 字符串以空字符结尾的惯例,可以在有需要时重用 <string.h> 函数库,从而避免了不必要的代码重复。
SDS
C 字符串
获取长度的时间复杂度
O(1)
O(n)
避免溢出机制
有
没有
二进制安全
是
否
注: 二进制安全是指 SDS 会记录信息的长度,因此在除结尾字符外允许有‘\0’的出现,而 C 字符串不行。
SDS 相当于 C 字符串而做的优化:
SDS 中的很多函数会在调用的过程中将原来的对象给释放,并返回一个新的对象地址,原来的旧的变量会失效,需要进行替换。