Redis字符串类型实现内幕

摘要

Redis不只仅是一个key-value存储,它更是一个数据结构服务,支持不一样类型的值。这意味着在传统的key-value存储中,咱们用string的key关联string的value。而在Redis中,咱们能够存储的值不受限于string,咱们还能够存储复杂的数据结构。string是咱们在使用Redis过程当中能接触到的最简单的数据类型,也是Memcached中仅有的类型,所以对于Redis新手来讲,首先选择使用string类型是理所固然的。这篇文章主要介绍Redis的string类型的实现内幕。api

 

初识:简单动态字符串

Redis中使用的字符串是经过包装的,基于c语言字符数组实现的一个抽象数据结构,后文中提到的sds指的就是简单动态字符串,它的定义和实如今sds.h和sds.c中,结构是这样的:数组

struct sdshdr {
    int len;
    int free;
    char buf[];
};

Redis中定义了这样一个结构体来表示字符串,字段含义以下:安全

  • len表示buf中存储的字符串的长度。
  • free表示buf中空闲空间的长度。
  • buf用于存储字符串内容。

举个例子:数据结构

图1app

假设上面图1是当前buf中存储的内容,那么这个时候len为8,free为2,sds的内存占用量能够用下面公式表示:函数

sizeof(struct sdshdr) + len + free + 1

初识了sds以后,咱们下面分别从使用字符串的时候最关心的几个点来继续认识sds:性能

  1. 存储内容
  2. 长度计算
  3. 字符串拼接
  4. 字符串截断

 

存储内容:二进制安全字符串

Redis keys是二进制安全的,对因而不是二进制安全,简单理解就是对于字符串结构,咱们能不能用它来存储二进制。咱们都知道传统的C字符串是zero-terminated的,也就是C语言字符串函数库认为字符串是以'\0'结尾的,所以对于用来表示字符串的C语言字符数组中中间不能有'\0',否则在处理的过程当中会出错,好比下面这段:ui

图2spa

咱们申请了length为9的char数组,将每一个字母都放到对应的位置,咱们指望获得的是"Float Lu"这样的字符串,而实际C字符串函数处理的过程当中会觉得这个字符串是"Float",而这并非咱们指望的结果。设计

而二进制安全的字符串,Redis中给的术语是binary-safe,它容许咱们把图2中表示的数据当作字符串来使用,那这个二进制有什么关系呢,由于二进制数据一般会有中间某个字节存储'\0'的这种状况,好比咱们存储一个JPEG格式图片,所以二进制安全的字符串结构容许咱们存储像JPEG格式图片的这种数据。从而在Redis中咱们不只仅可使用传统字符串来当作key,使用二进制来做为key也是被容许的,好比图片、视频、音频……whatever,然而你不要高兴太早,Redis对key的长度是有限制的,最大长度是512MB。

 

长度计算:O(1)时间复杂度

c语言中strlen的实现

strlen在c语言中用来计算c语言字符串的长度,strlen的实现很简单,从内存中字符串开始的位置开始扫描并计数,知道碰到第一个'\0'为止,这也是为何c语言字符串是zero-terminated的缘由。很显然,strlen的时间复杂度是O(N)。

sds中sdslen的实现

sds中用于对字符串长度计算的函数为sdslen,咱们看一下它的实现:

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

咱们想要获取的sds的长度就是sdshdr中定义的len的值,时间复杂度是O(1)。

 

字符串拼接:动态扩容机制

一般状况咱们对于字符串的拼接不只仅是一次,而是不少次,咱们写JAVA的一般颇有感触,好比咱们要根据用户名来拼接一个字符串,又考虑到执行效率,咱们一般会借助于StringBuilder像下面这样写:

public String makeWelcomeStr(String username) {
     StringBuilder sb = new StringBuilder();
     sb.append("welcome ");
     sb.append(username);
     sb.append("!");
     return sb.toString();
}

对于C语言来讲咱们并不能这么潇洒,咱们须要先苦逼的申请一块内存区域将"welcome "放入,当咱们须要拼接username的时候,咱们须要苦逼的再申请一块内存,长度为原有内容长度加上username的长度,而后再将原有内容拷贝到新的内存区域,而后再放心的将username放入新的内存区域的后面……还有"!"没有拼接呢,我天!

苦逼!

sds中咱们不须要考虑拼接的时候要不要扩容,扩多少容等,这些sds都为咱们作了,咱们只须要简单的调用sdscat便可(sds中用来拼接字符串的函数是sdscat),sdscat的核心实如今sdscatlen和sdsMakeRoomFor中,假设咱们正在拼接字符串:

图3

个人名字是"Float Lu",我将它拼接在"welcome"后面,我不须要考虑buf的free长度是多少,能不能放下"Float Lu",咱们将要放的字符串长度为8,看看sds是怎么作的:

在拼接新的字符串以前会检查当前free是否够用,若是当前的free空间大于等于8,则不须要申请内存,直接将字符串放入,修改len和free。

若是空间不够用,sds有一套扩容规则,接着上面的例子,老的内容长度为len=9,新的内容长度newlen=len+8,为16:

  1. 若是newlen小于1024(byte) * 1024(byte)=1(MB)则新的长度为二倍的newlen。
  2. 若是newlen的长度大于等于1MB,则新的newlen的长度为newlen的长度加上1MB。

(这让我想起了Netty的内存扩容规则),接着上面的例子,扩容完以后的len为16,free为16,加上1字节的'\0'。

这个时候咱们再继续拼接"!"的时候能够直接将"!"放入刚才申请多余的内存区域内同时将len加1,将free减1便可。

sds经过预分配一些内存区域来减小内存申请,拷贝的次数,虽然预分配规则很简单,可是是颇有效的。

 

字符串截断:内存空间懒释放

考虑到咱们要清理字符串中的一些内容,传统的作法是新申请一块内存区域,将须要保留的内容放入新的区域而后释放原始区域,这其中必然会涉及内存的申请,拷贝。加入这个时候又有往刚才保留的字符串后面拼接一个字符串又要涉及一些重操做,好比内存申请,拷贝。。。

咱们来看看sds是怎么作的,在sds中提供了sdstrim这样的一个方法,它的定义:

sds sdstrim(sds s, const char *cset)

即清除s中全部在cset中出现过的字符,看一个例子:

s = sdsnew("AA...AA.aHelloWorld::");
s = sdstrim(s,"A. :");
printf("%s\n", s);

结果是"Hello World"。

对于上面的状况,原来的len为21,假如free为0,清理完成以后不涉及内存的申请操做,len为10,free为11,加入这个时候有字符串拼接需求,直接将内容放到free的11个字节内便可,固然是若是放的下的话。

sds并不会当即释放掉不须要的已经申请的内存,实际中,这些内存后续极可能还能会被用到,若是你担忧内存浪费的话,能够手动调用sds提供的接口释放这些空间,好比sdsfree函数。

 

sds VS c语言字符串

上面咱们分别字符串操做最常涉及到的一些问题认识了sds,最后咱们经过将sds和c语言字符串进行比较一下来总结sds的优缺点:

C语言     sds
占用内存一般为内容长度 占用内存包括结构体和free的长度
非二进制安全 二进制安全
长度计算时间复杂度为O(N) 长度计算时间复杂度为O(1)
须要掌握字符串的长度 sds帮助咱们把握长度和内存申请
字符串拼接每次要进行内存申请和拷贝 不必定内次都要申请内存和拷贝

 

总结

sds在Redis中做为字符串基础服务,为Redis的keys和其余涉及string操做的地方提供服务,sds的设计不只考虑到api使用的安全性,更多的是为了提升性能,为高性能Redis奠基基础。字符串操做方面提升性能的核心点在于尽可能减小内存的申请和内存拷贝,在设计的时候容许利用必定的内存空间换取时间效率。

 

参考文献

《Redis Documentation》

《Redis2.8.13源码》

《Redis设计与实现》

 

注:本文由博主原创,欢迎转载,若有问题还请多多包涵。

相关文章
相关标签/搜索