「Redisの設計と実装」-シンプルな動的文字列

単純な動的文字列

Redisは抽象的なタイプの動的文字列(単純な動的文字列、SDS)を構築し、このタイプをRedisのデフォルトの文字表現として使用しますが、
C言語の文字列リテラルは操作する必要のない一部の文字列としてのみ使用されます。ログを印刷する場所の場合は変更します。

SDSの定義

struct sdshdr {
    // 记录buf数组中已使用字符的数量,等于SDS保存字符串的长度
    int len;
    // 记录的是buf数组中未使用的字节的数量
    int free;
    // 字节数组,用于保存字符串
    char buf[];
};
复制代码

SDSは、C文字列のnullで終了する規則に従います。この規則に従うと、SDSはC文字列の一部の関数を直接再利用できます。

SDSとc文字列の違い

文字列の長さを取得します

c文字列は文字列自体の長さ情報を記録しないため、c文字列の長さを取得する場合は、トラバーサル操作を実行する必要があります。この操作の複雑さはO(N)です。SDSの場合、 SDS自体の長さ、文字列の長さ情報を取得する場合は、O(1)の複雑さのみが必要です。

バッファオーバーフロー

C文字列はそれ自体の長さを記録しないため、文字列を変更するときにバッファオーバーフローが発生しやすくなります。たとえば、メモリ内に2つの隣接する文字列S1、S2、S1 = "Redis"、S2="MongoDB"があります。

image.pngextract(S1、 "Cluster")操作が呼び出されると、S1はRedis Clusterに変更されますが、メソッドが呼び出される前にS1に十分なスペースが割り当てられていない場合、S1のデータはS2が存在するスペースにオーバーフローします。に配置され、S2が変更されます。
C文字列とは異なり、SDSは、変更時にSDSスペースが変更されたコンテンツと一致するかどうかを最初にチェックします。一致しない場合は、スペースを必要なサイズに自動的に拡張してから、実際の変更操作を実行します。

文字列の変更によって発生したメモリ再割り当ての数

C文字列はそれ自体の長さを記録しないため、N文字を含むC文字列の場合、下部にN + 1の長さの文字配列を格納します(空の文字列を格納するために余分なスペースが使用されます))。このような関連付けにより、C文字列を増減するたびに、常にメモリの再割り当て操作が必要になります。

  • 如果是增加字符的操作,那么在这个操作的之前需要通过重新分配内存来扩展底层数组空间的大小,不然就会产生缓冲的溢出
  • 如果是缩短字符串的操作,那么在这个操作之后就需要通过内存分配来释放不再使用的那部分的空间,如果忘记了就会产生内存的泄漏。

但Redis做为一个数据库,经常被使用在一些速度快,数据被频繁修改的场景里面,如果每次对于字符串的操作都需要进行一个内存的重分配的操作这是需要占用大量的时间的,且对性能造成影响。
所以为了避免这种缺陷,基于SDS的结构,SDS通过未使用空间解除了字符串长度与底层数组空间的关系:在SDS的底层,buf数组的长度就并不一定是字符串的长度加1,数组里面可以包含没有使用的字节,而这些字节的长度就由SDS的fee属性来记录。
基于SDS的未使用空间,SDS实现了空间预分配和惰性空间释放策略:

  • 空间预分配,这个操作简而言之就是当SDS进行修改需要扩张的时候,程序不仅会为SDS分配修改所需要的空间,同时也会为SDS分配额外的未使用空间。
    • 如果SDS进行修改之后,SDS的长度(len属性)如果小于1MB,那么程序分配和len同样大小的未使用空间,这个时候len属性将会与fee属性的值一样。
    • 如果SDS进行修改之后,SDS的长度大于等于1MB,那么程序会分配1MB的未使用空间。
  • 惰性空间释放,当SDS保存的字符串缩短的时候,程序并不会马上进行内存的重分配来回收缩短之后多出来的字节,而是使用fee这个属性将这些字节数量记录下来,并等待使用。

二进制安全

C字符串中字符必须符合某一种规则(比如ASCII),并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符会被误认为是字符串结尾。而这些限制使得C字符串只能保存文本内容。
但Redis需要的不仅仅是对文本内容的保存场景,有时候也需要对一些二进制数据保存的场景,为了使得Redis适用于不同的场景。SDS的所有API操作都是二进制安全的,所有SDS的API操作都会以二进制的方式来处理SDS存放在buf数组里面的数据,程序并不会对其中的数据做任何的限制,过滤和假设。数据被写入的时候是什么样的,它被读取的时候就是什么样的。

总结

  • Redis只会使用C字符串做字面量,在大多数的情况下Redis使用SDS作为字符串的表示。
  • 比起C字符串,SDS有以下的优点
    • 常数复杂度获取字符串的长度
    • 杜绝了缓冲区的溢出
    • 减少了修改字符串长度所需要的内存分配次数
    • 二进制安全
    • 兼容部分C字符串的函数

补充

Redis源码中关于SDS结构定义:

struct __attribute__ ((__packed__)) sdshdr5 {
    unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; /* used */
    uint8_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint16_t len; /* used */
    uint16_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint32_t len; /* used */
    uint32_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint64_t len; /* used */
    uint64_t alloc; /* excluding the header and null terminator */
    unsigned char flags; /* 3 lsb of type, 5 unused bits */
    char buf[];
};
复制代码

おすすめ

転載: juejin.im/post/7079779869168500744