C语言字符串
1 | char *str = "redis"; // 可以不显式的添加\0,由编译器添加 |
C语言中使用char*字符数组表示字符串,’\0’来标记一个字符串的结束,不过在使用的过程中我们不需要显式的在字符串中加入’\0’。
存在问题
1.二进制安全
C语言以’\0’标记字符串的结尾,如果一个字符串本身带有’\0’,比如一些二进制数据,那么字符串就会被截断,导致无法存储二进制数据。
2.缓冲区溢出
假设内存有两个相邻的字符串s1和s2,s1保存了字符串“Redis”,s2中保存了字符串“MongoDB”,如果不对大小进行判断,直接调用strcat(s1, “Cluster”)函数在s1字符串后面追加“Cluster“,s1的数据溢出到s2所在空间,导致s2的内容被意外的修改。
3.频繁的内存分配
因为C字符串不记录自身长度,如果对字符串进行修改,需要内存重分配扩展底层数组的空间大小,比如调用strcat函数进行拼接时,需要重新分配内存,保证数组有足够的空间,否则就会发生缓冲区溢出。
由于内存重分配涉及复杂算法,并且可能需要执行系统调用,所以是一个耗时的操作。
4.获取字符串长度复杂度为O(N)
C字符串不记录自身长度,需要遍历每个字符计算字符串长度,在高并发情况下有可能称为性能的瓶颈。
参考:黄健宏《Redis设计与实现》
Redis String
String字符串是Redis中的一种数据结构,它的语法如下:
1 | SET key value |
key和value都是字符串,一个key对应一个value,通过key可以获取到对应的value:
1 | GET KEY |
简单动态字符串
Redis的字符串没有直接使用C语言的字符数组实现,而是通过SDS(Simple Dynamic String)简单动态字符串实现的。
SDS数据结构
1 | typedef char *sds; |
Redis为了灵活的保存不同大小的字符串节省内存空间,设计了不同的结构头sdshdr64、sdshdr32、sdshdr16、sdshdr8和sdshdr5。
虽然结构头不同,但是他们都具有相同的属性(sdshdr5除外):
- len:字符数组buf实际使用的大小,也就是字符串的长度
- alloc:字符数组buf分配的空间大小
- flags:标记SDS的类型:sdshdr64、sdshdr32、sdshdr16、sdshdr8和sdshdr5
- buf[]:字符数组,用来存储实际的字符数据
一些优化
Redis使用了__attribute__ (( __packed__))节省内存空间,它可以告诉编译器使用紧凑的方式分配内存,不使用字节对齐的方式给变量分配内存。
关于字节对齐可参考结构体字节对齐,C语言结构体字节对齐详解。
柔性数组
在结构体定义中,可以看到最后一个buf数组是没有设置大小的,这种放在结构体中最后一个元素位置并且没有设置大小的数组称为柔性数组,它可以在程序运行过程中动态的进行内存分配。
SDS创建
(1)sds在创建的时候,buf数组初始大小为:struct结构体大小 + 字符串的长度+1, +1是为了在字符串末尾添加一个\0。
(2)在完成字符串到字符数组的拷贝之后,会在字符串末尾加一个\0,这样可以复用C语言的一些函数。
1 | sds _sdsnewlen(const void *init, size_t initlen, int trymalloc) { |
SDS扩容
(1)判断剩余可用空间是否大于需要增加的长度,如果大于说明空间足够,直接返回即可,反之计算新的长度,进行扩容;
(2)空间预分配,扩容新的长度为已经使用的长度+需要增加的长度,然后会判断是否小于SDS_MAX_PREALLOC(1M):
- 如果小于,直接扩容为新长度的2倍
- 如果大于,扩容容量为新长度+SDS_MAX_PREALLOC的值
(3)由于扩容之后大小发生了改变,需要重新计算使用哪种SDS类型:
- 如果类型不需要改变,直接扩容即可
- 如果类型发生改变,需要重新进行内存分配,并将旧的内存释放
1 | sds sdsMakeRoomFor(sds s, size_t addlen) { |
惰性空间释放
当字符串缩短后,程序并不会立刻释放多余的空间,之后可直接复用多余的空间。
Redis SDS总结
- 直接保存了字符串的长度,不需要遍历每个字符去计算长度。
- 使用了字符数组保存数据,并且记录了字符串长度,通过长度来判断字符串的结束而不是\0,可以保存二进制数据。
- 需要改变字符串长度大小时,会通过sdsMakeRoomFor方法确保有足够的内存空间,不需要开发人员自行判断,保证了数据的安全性,不会造成缓冲区溢出。
- 在进行扩容时,会进行空间预分配,多分配一些空间,减小内存分配的次数。
- 创建了不同的结构体,保存不同大小的字符串节省内存空间。
- 使用__attribute__ (( __packed__))紧凑的方式分配内存,节省内存空间。
参考
yellowriver007 - Redis内部数据结构详解(2)——sds
偷懒的程序员-小彭 - redis 系列,要懂redis,首先得看懂sds(全网最细节的sds讲解)
CHENG Jian - C语言0长度数组(可变数组/柔性数组)详解
Redis版本:redis-6.2.5