02 | 键值对中字符串的实现,用char*还是结构体?
蒋德钧
你好,我是蒋德钧。
字符串在我们平时的应用开发中十分常见,比如我们要记录用户信息、商品信息、状态信息等等,这些都会用到字符串。
而对于 Redis 来说,键值对中的键是字符串,值有时也是字符串。我们在 Redis 中写入一条用户信息,记录了用户姓名、性别、所在城市等,这些都是字符串,如下所示:
此外,Redis 实例和客户端交互的命令和数据,也都是用字符串表示的。
那么,既然字符串的使用如此广泛和关键,就使得我们在实现字符串时,需要尽量满足以下三个要求:
能支持丰富且高效的字符串操作,比如字符串追加、拷贝、比较、获取长度等;
能保存任意的二进制数据,比如图片等;
能尽可能地节省内存开销。
其实,如果你开发过 C 语言程序,你应该就知道,在 C 语言中可以使用 char* 字符数组来实现字符串。同时,C 语言标准库 string.h 中也定义了多种字符串的操作函数,比如字符串比较函数 strcmp、字符串长度计算函数 strlen、字符串追加函数 strcat 等,这样就便于开发者直接调用这些函数来完成字符串操作。
所以这样看起来,Redis 好像完全可以复用 C 语言中对字符串的实现呀?
但实际上,我们在使用 C 语言字符串时,经常需要手动检查和分配字符串空间,而这就会增加代码开发的工作量。而且,图片等数据还无法用字符串保存,也就限制了应用范围。
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
- 深入了解
- 翻译
- 解释
- 总结
Redis中的字符串实现方案是通过简单动态字符串(Simple Dynamic String,SDS)的结构来表示字符串。相比于传统的C语言中使用char*字符数组来实现字符串,SDS结构的设计能够提升字符串的操作效率,并且可以用来保存任意二进制数据。文章从char*的结构设计和操作函数复杂度两方面分析了传统C语言字符串实现的不足之处,指出了使用“\0”作为字符串的结束字符会导致数据截断和操作函数复杂度增加的问题。而Redis的SDS结构则通过在字符数组基础上增加额外的元数据,如字符数组现有长度len、分配给字符数组的空间长度alloc等,来提升字符串操作效率,并且可以保存任意二进制数据。文章还介绍了Redis中使用typedef给char*类型定义了一个别名sds,以及创建新的字符串时调用的sdsnewlen函数的操作逻辑。SDS结构的设计思想和实现技巧能够满足Redis对字符串操作效率和节省内存开销的需求。文章还详细介绍了SDS在编程技巧上如何实现节省内存的紧凑型字符串结构,通过设计不同类型的结构头和使用`__attribute__ ((__packed__))`编译优化来灵活保存不同大小的字符串并节省内存空间。总的来说,通过本文读者可以了解到SDS结构的优势和传统C语言字符串实现的不足之处,以及Redis中字符串的实现方式,为读者提供了深入了解字符串实现的技术知识。
仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《Redis 源码剖析与实战》,新⼈⾸单¥59
《Redis 源码剖析与实战》,新⼈⾸单¥59
立即购买
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
登录 后留言
全部留言(36)
- 最新
- 精选
- lzh2nix个人觉得这里使用__attribute__ ((__packed__))除了节省内存空间之外,还有一个很精妙的设计就是在packed之后可以通过以下的方式来获取flags字段 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; } 从而更进一步的得到struct的具体类型,如果是非1字节对齐的话,这里就不能这样操作。而sds中通过原始的char* 定位到sds的Header是设计的的**灵魂**
作者回复: 是的,这个是SDS设计得非常巧妙的地方,后续给大家加餐讲讲:)
2021-08-02214 - lzh2nix"这两个元数据占用的内存空间在 sdshdr16、sdshdr32、sdshdr64 类型中,则分别是 2 字节、4 字节和 8 字节" 这里的描述是是否有问题, sdshdr16中len, alloc这两个元数据占用的空间应该是4字节,其他两个类推。 struct __attribute__((__packed__))sdshdr16 { uint16_t len; /*2字节*/ uint16_t alloc; /* 2字节*/ unsigned char flags; /* 1字节 */ char buf[]; };
作者回复: 应该是这两个元数据各自占用的内存空间是2字节。。。不好意思,引起误解了。
2021-08-022 - Kaitochar* 的不足: - 操作效率低:获取长度需遍历,O(N)复杂度 - 二进制不安全:无法存储包含 \0 的数据 SDS 的优势: - 操作效率高:获取长度无需遍历,O(1)复杂度 - 二进制安全:因单独记录长度字段,所以可存储包含 \0 的数据 - 兼容 C 字符串函数,可直接使用字符串 API 另外 Redis 在操作 SDS 时,为了避免频繁操作字符串时,每次「申请、释放」内存的开销,还做了这些优化: - 内存预分配:SDS 扩容,会多申请一些内存(小于 1MB 翻倍扩容,大于 1MB 按 1MB 扩容) - 多余内存不释放:SDS 缩容,不释放多余的内存,下次使用可直接复用这些内存 这种策略,是以多占一些内存的方式,换取「追加」操作的速度。 这个内存预分配策略,详细逻辑可以看 sds.c 的 sdsMakeRoomFor 函数。 课后题:SDS 字符串在 Redis 内部模块实现中也被广泛使用,你能在 Redis server 和客户端的实现中,找到使用 SDS 字符串的地方么? 1、Redis 中所有 key 的类型就是 SDS(详见 db.c 的 dbAdd 函数) 2、Redis Server 在读取 Client 发来的请求时,会先读到一个缓冲区中,这个缓冲区也是 SDS(详见 server.h 中 struct client 的 querybuf 字段) 3、写操作追加到 AOF 时,也会先写到 AOF 缓冲区,这个缓冲区也是 SDS (详见 server.h 中 struct client 的 aof_buf 字段)2021-07-2915100
- 悟空聊架构课后题:使用 SDS 字符串的地方? 1. server.h 文件中的 `redisObject` 对象,key 和 value 都是对象,key (键对象)都是 SDS 简单动态字符串对象 2. cluter.c 的 clusterGenNodesDescription 函数中。这个函数代表以 csv 格式记录当前节点已知所有节点的信息。 3. client.h 的 clusterLink 结构体中。clusterLink 包含了与其他节点进行通讯所需的全部信息,用 SDS 来存储输出缓冲区和输入缓冲区。 4. server.h 的 client 结构体中。缓冲区 querybuf、pending_querybuf 用的 sds 数据结构。 5. networking.c 中的 catClientInfoString 函数。获取客户端的各项信息,将它们储存到 sds 值 s 里面,并返回。 6. sentinel.c 中的 sentinelGetMasterByName 函数。根据名字查找主服务器,而参数名字会先转化为 SDS 后再去找主服务器。 7. server.h 中的结构体 redisServer,aof_buf 缓存区用的 是 sds。 8. slowlog.h 中的结构体 slowlogEntry,用来记录慢查询日志,其他 client 的名字和 ip 地址用的是 sds。 还有很多地方用到了,这里就不一一列举了,感兴趣的同学加我好友交流:passjava。 ---------------------------------- 详细说明: (1)Redis 使用对象来表示数据库中的键和值,每次创建一个键值对时,都会创建两个对象:一个键对象,一个值对象。而键对象都是 SDS 简单动态字符串对象,值对象可以字符串对象、列表对象、哈希对象、集合对象或者有序集合对象。 对象的数据结构: server.h 文件中的 `redisObject` 结构体定义如下: ```c typedef struct redisObject { // 类型 unsigned type:4; // 编码 unsigned encoding:4; // 对象最后一次被访问的时间 unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or * LFU data (least significant 8 bits frequency * and most significant 16 bits access time). */ // 引用计数 int refcount; // 指向实际值的指针 void *ptr; } robj; ``` 再来看添加键值对的操作,在文件 db.c/ ```C void dbAdd(redisDb *db, robj *key, robj *val) ``` 第一个参数代表要添加到哪个数据库(Redis 默认会创建 16 个数据库,第二个代表键对象,第三个参数代表值对象。 dbAdd 函数会被很多 Redis 命令调用,比如 sadd 命令。 (Redis sadd 命令将一个或多个成员元素加入到集合中,已经存在于集合的成员元素将被忽略。 假如集合 key 不存在,则创建一个只包含添加的元素作成员的集合。 当集合 key 不是集合类型时,返回一个错误。2) 类似这样的命令:myset 就是一个字符串。 ```SH redis 127.0.0.1:6379> SADD myset "hello" ``` (2)集群中也会用到,代码路径: cluter.c/clusterGenNodesDescription 所有节点的信息(包括当前节点自身)被保存到一个 sds 里面,以 csv 格式返回。 (3)cluster.h 的 clusterLink 结构体中。clusterLink 包含了与其他节点进行通讯所需的全部信息 ```C // 输出缓冲区,保存着等待发送给其他节点的消息(message)。 sds sndbuf; /* Packet send buffer */ // 输入缓冲区,保存着从其他节点接收到的消息。 sds rcvbuf; ``` (4)Redis 会维护每个 Client 的状态,Client 发送的请求,会被缓存到 querybuf 中。2021-07-29313
- lzh2nix个人觉得sds有一个很优秀的设计是对外和char*保持一致,在sds外面可以像使用char*一样来使用sds,但是使用sds相关函数操作的时候又可以发挥sds的特性(通过偏移量来找到sds的header)。 我们可以看到在sdsnewlen中返回的是char* sds sdsnewlen(const void *init, size_t initlen) { sds s; sh = s_malloc(hdrlen+initlen+1); s = (char*)sh+hdrlen; return s; } 这样的实际对外面的使用着来说就很友好很友好的。2021-08-019
- frankylee既然这篇是讲解SDS的,那按道理来说 SDS内存空间分配策略,以及空间释放册罗 这块就应该讲清楚,但是通篇读下来好像并没提到这块,读完下面的精选留言部分读者可能仍然云里雾里2021-07-3036
- Milittle设计着实牛逼: 1. 使用sds这个字符数组保存所有8 16 32 64的结构体。 2. 结构体中的len alloc 对应不同类型占不同字节数,flags始终是相同的,后面char buf[]就是真实的字符串。 3. SDS_HDR 这个宏定义,一键让sds回到指针初始的地方,对变量进行设置。 4. 一开始纳闷在取flags的时候,直接使用s[-1],不会数据越界么,但是你仔细瞧一瞧,发现这个s指向的位置,刚好是char buf[]这里,-1 的位置刚好是flags。害,还是发现c牛逼。一个指针掌控的死死的。 望赐教2021-07-294
- 曾轼麟Redis设计sds的意图: 1、满足存储传输二进制的条件(避免\0歧义) 2、高效操作字符串(通过len和alloc,快速获取字符长度大小以及跳转到字符串末尾) 3、紧凑型内存设计(按照字符串类型,len和alloc使用不同的类型节约内存,并且关闭内存对齐来达到内存高效利用,在redis中除了sds,intset和ziplist也有类似的目底) 4、避免频繁的内存分配,除了sds部分类型存在预留空间,sds设计了sdsfree和sdsclear两种字符串清理函数,其中sdsclear,只是修改len为0以及buf为'\0',并不会实际释放内存,避免下次使用带来的内存开销(老师可能忘记提及了) 此外sds的使用几乎可以贯穿整个redis,在server.h文件中以redisServer 和 client 为例子(client既可以是普通客户端,也可以是slave) client: 1、querybuf(查询缓冲区使用sds,RESP的协议数据) 2、pending_querybuf(易主时候的等待同步缓冲区) 等等 redisServer: 1、aof_buf(aof缓冲区) 等等2021-07-292
- J²//将源字符串中的每个字符逐一赋值到目标字符串中,直到遇到结束字符 while((*dest++ = *src++) != '\0' ) 这里少了个分号,应该是while((*dest++ = *src++) != '\0' );2022-06-061
- ikel5年前看redis源码,当时把sds结构用到了项目中来处理字符串,也没出过啥幺蛾子,只可惜后来没有再继续看源码了2021-08-311
收起评论