一 实现原理
1.1 c语言字符串
以空字符串结尾的字符数组,比如hello在C语言中,经过一系列算法分配内存后,再产生出图中结构代表字符串'hello!~'。
使用长度为N+1的字符数组来表示字符串,最后总加一个'\0'代表结尾。
1.2 php字符串
底层为C,结构如代码所示:
1.3 redis字符串
和php的字符串有类似之处,redis作者封装了一个名为SDS的结构体来表示字符串,如图所示:
那它为啥要这样做呢,我们在下面小结探讨~,先说一下结构中各个属性的作用:
-
len:
标识该字符串长度( 结合PHP的字符串结构体想想有什么好处~ )
-
free:
标识该redis字符串未使用空间大小,它有着自己的分配策略(下面简单介绍)
-
buf:
类似一个C语言的字符串,以'\0'结尾,方便重用C语言的一些字符串函数(说白了就是当做C语言的字符串用,但是\0后面会有长度free的空间,对于C函数来说不影响)
二 对比
2.1 查询复杂度
2.1.1 c语言
由于C语言的字符串结构使然,它只能通过遍历加计数来获取字符串长度,很简单,O(N)
2.1.2 PHP、Redis
这两个一起说,是因为它们都在自己的字符串结构体中内置了长度的属性,直接O(1)获取该属性的值即可得到字符串长度
2.2 抠门C之申请与释放
C作为可操作内存的一门语言,它是比较抠门的,不会动态为我们分配内存。我们需要手动去申请内存储存数据,在我们不需要用到某些内存的时候,也要去手动将其释放(垃圾回收、GC)。
2.2.1 缓冲区溢出 忘记申请
redis为什么要这么设计呢,我们先简单了解一下C语言中的 缓冲区溢出 问题
我们想在hello!~后追加 得到 hello!~ world~! 但是str2意外被修改
2.2.2 内存泄漏 忘记释放
标题我们说忘记释放,和申请分配相对应,如果我们在C中想要做到缩短字符串的操作,比如我们把str1改为hello之后,没有去释放~!所占用的空间,就会造成 内存泄漏 ,产生空占内存。
2.2.3 Redis、PHP如何做
PHP作为一门“高级语言”,有自己的动态分配和GC,Redis也一样。上面我们了解到,C语言的字符串操作相对比较麻烦,那么它的优点呢?比较明显的是节省内存,我们也从图片中得知,PHP和Redis的字符串结构相对于C来说有其他附加的属性,它们都会占用一定的空间。反之,我占用这么多空间,图啥?一句话:空间换时间。
Redis作为一种高性能的缓存服务,它需要对客户端的请求做到快速响应。字符串的更改操作对于Redis来说也比较频繁,如果像C那样每次都需要申请内存和GC,对性能来说影响是比较大的,更不用说像strlen之类的操作了,直接将O(N)变成了O(1),典型的实现空间换时间。
那么它具体是怎么做的呢?
2.3 空间换时间
2.3.1 预分配
当我们对某个string类型的key执行set后,如果字符串的长度增长,Redis不仅对字符串分配相应长度的空间,还会进行预分配给一定的未使用空间,该未使用空间的长度由free属性标识。机制如下:
-
如果修改之后len长度小于1MB,就分配和len长度一样的空间。
-
如果修改之后len长度大于1MB,就分配1MB空间。
举例:如果str修改之后字符串大小为30MB,那么实际分配长度为30MB(len)+1MB(free)+1byte(\0),假如我们继续修改str 长度为30MB+500KB,那Redis就不需要重新申请内存空间,直接利用free的1MB空间即可。嗯~ 空间换来了时间
2.3.2 懒释放
当我们对某个string类型的key执行set后,如果字符串的长度减少,Redis将字符串直接放进当前key对应的空间中,多出来的空间不会被立即释放,多出来的空间由free属性标识,以便即将到来的字符串增长操作。举个例子:
set str helloworld;
set str hello;
set str helloredis;
执行第二、三个命令发生了什么呢
helloworld\0 占用11个字符空间,此时free为0
hello\0 占用6个字符空间,此时free为5
hellophp\0 占用9个空间,php直接利用free的空间,不需要重新申请内存,此时free为2
2.4 二进制安全
讲这点肯定是和C有区别滴,C语言的字符串结尾用\0 空字符来判定。假设有个文本 redis\0!,在C中!是会被忽略的。所以C中的字符串并不能保存比较特殊的数据。得益于Redis的字符串结构体,我们用len属性来判断字符串是否结束,所以数据是存的什么样,取出来还是原来的样子。
三 总结
C字符串 | Redis字符串 |
计算长度需要遍历 | 直接取len的值 |
手动申请释放内存 | len和free配合Redis机制不需要考虑C中此类问题 |
二进制不安全 | 二进制安全 |
频繁操作慢,每次内存分配 | 频繁操作快,不需要每次分配内存 |