Redis動的文字列

Q:SDSとは何ですか

A: SDSは、実装プロセスでRedisが使用する一種の「動的文字列」です。Redisコードは基本的にC言語で実装されているため、SDSは依然として最下層にchar buf[]データ格納することに依存しています。SDSオブジェクトのデータ構造は、おおよそ下図のようになります。

SDS構造体メンバーには、len、free、bufの3つの属性があることがわかります。その中で、lenはSDSオブジェクトによって管理される文字列内の有効な文字数を識別し、freeはSDSがスペースを拡張せずに格納できる有効な文字数を表し、bufはchar[]連続セグメントメモリスペースを指すポインタの一種です。 、これは文字列が実際に格納される場所です(有効な文字列は、\ 0以外の文字列のコレクションを指します)。

Q:C文字列の場合、なぜSDSが必要なのですか?

A:関連データを読み、Redisのドキュメントを確認することで、ネイティブC文字列の代わりにSDSを使用する利点の次のポイントを要約できます。

* 更高效的获取一个 SDS 对象内保存的字符串的长度
* 杜绝缓冲区溢出
* 减少因字符串的修改导致的频繁分配和回收内存空间操作
* 二进制安全
* 和 C 语言有关字符串的库函数有一个更高的兼容性

実際、これを見ると、以前に他の言語で「通常の配列」を使用して「動的配列」を実装したことがある場合は、理解できない「バイナリセキュリティ」の利点は別として、残りはよく知っているはずです。これらの利点については、以下で個別に説明しましょう。

Q:文字列の長さをより効率的に取得するにはどうすればよいですか?

A:この問題は、従来のCストリングの問題点です。線形データ構造では、データ構造内のすべての有効な要素をトラバースすることによってのみ正確な長さを取得できます。この操作の時間計算量はO(N)レベルです。ただし、C文字列をSDSデータ構造のメンバーとして使用するlen場合は、別のメンバーを追加することで、文字列の正確な長さをリアルタイムで計算できます。計算方法も非常に単純です。つまり、文字列に対して「要素の追加」操作を実行する場合はlen+1 、「要素の減少」操作を実行する場合はlen-1です。このようlenに、SDSに格納されている文字列の長さは、accessを介して取得できますこれに似た実装:

void add(char a){
    buf[len++] = a;
}

void sub(char a){
    len--;
}

int length(char a){
    return len;
}

Q:バッファオーバーフローを防ぐ方法は?

A:バッファオーバーフローは、別のより単純なステートメントに置き換えられます。つまり、自分のものではないメモリ内のデータを改ざんすることです。この現象は、文字列のスプライシングおよび文字列への文字の追加の操作でより一般的です。この問題に対処する方法も非常に簡単です。メモリ容量が許す場合、文字列がより多くのメモリスペースを必要とする場合、「より大きな」連続スペースを再割り当てして、過去の元のスペースコピーの有効なデータを置き換えます。その中で、残りのスペースを超えているかどうかを検出するためfreeに、属性の値を使用できます。これは、配列でまだ使用可能なスペースの量を表すためです。前の段落の内容を注意深く読むと、バッファオーバーフローを防ぐプロセスにいくつかの「醜い」ステップがあることがわかります。

  1. メモリ内の連続したスペースを複数回割り当てる可能性があります
  2. 元のスペースの有効なデータを新しいスペースに複数回コピーすることができます
  3. 割り当てられたスペースが再利用されておらず、継続的に割り当てられている場合、メモリリークが発生する可能性があります

新たな問題に対応するため、以下の方法で解決しました。

  1. 特定の戦略に従って新しいメモリスペースを割り当て、割り当ての数を最小限に抑えます
  2. 空き領域が特定のしきい値に達したら、余分なメモリ領域を再利用します

Redisでは、「事前に割り当てられた」スペースのサイズは2つのステップで決定されます。

  1. 変更された文字列の長さ(len)が1MB未満の場合は、必要なスペースを割り当てるだけでなくlenサイズと同じ空きスペースを割り当てる必要がありますたとえば、変更された文字列の長さが10(len = 10)の場合、変更後の新しいメモリスペースのサイズは= 10 + 10 + 1 = 21になります。
  2. 変更された文字列の長さ(len)が1MBを超える場合は、必要なスペースを割り当てることに加えて、1MBに等しい空きスペースを割り当てる必要があります。

SDS関連の変更操作では、使用可能なスペースが実際に必要なスペースと比較されます。超過した場合は新しいスペースが割り当てられ、それ以外の場合は古いスペースが使用されます。上記の戦略により、基本的に「メモリ空間の再割り当て」と「元の空間の有効なデータの新しい空間へのコピー」の回数を、発生するたびに最大N回まで減らすことができます(Nは変更操作です)。 。回数)。

著者からの洞察は次のとおりです。多くのプログラマーは、問題を解決するときに完璧な解決策を見つける傾向があります。著者が著者である場合、彼はこの問題を見て、完璧な解決策があるかどうかを考えるかもしれません。上記の問題を解決してください。ただし、Redisのような産業グレードのプロジェクトでは、Redisが採用するソリューションは、演習を行うときに通常使用する「実装」でさえ、依然として非常に一般的であることがわかります。一見「リスクを発生させる遅延」アプローチは、最も「完璧な」アプローチである場合があります。プログラマーは、問題を「完全に」解決する方法よりも、問題を解決する方法にもっと注意を払う必要があります。

「予約スペース」を割り当てることで「割り当て」操作の回数を減らすだけでなく、割り当てが無制限になると、最終的にメモリが使い果たされるのではないかと心配しています。これは、私たちがよく話すメモリリークの問題です。それを解決することも非常に簡単です。それは、特定の戦略に従って割り当てられたメモリスペースを再利用することです。例:SDSにバインドされたメモリスペースの使用率が25%未満の場合、そのメモリスペースを元の半分に減らします。すべての空き領域を再利用するのではなく、元の将軍だけを縮小する理由については、慎重に検討してください。リサイクル方法が極端すぎると、「事前に割り当てられた」領域のすべての利点が失われることがわかります(メモリ割り当ての数を増やします)。

したがって、SDS関連の変更(主に要素の削除)操作では、空き領域はすぐには再利用されませんが、「予約領域」として使用されます。「メモリリーク」を防ぐために、Redisはメモリスペースを真に解放するための特別なAPIを提供します。

Q:SDSがバイナリセーフであることを確認するにはどうすればよいですか?

A:「バイナリセキュリティ」は比較的馴染みのない言葉のように聞こえますが、C言語の文字列の特性とバイナリコンテンツの特性を組み合わせると、バイナリセキュリティは主に\0このような特殊文字がコンテンツに表示されないようにすることがわかります。元の文字列の正しい解釈。比較的高く聞こえる問題であり、多くの場合、その解決策は比較的単純です。Redisでは、「バイナリセキュリティ」を確保するため\0に、C言語の文字列の文字を格納された文字列の境界として使用する代わりにlen この属性を使用して文字列内の有効な文字数識別します。

ただし、「バイナリセキュリティ」を確保するために\0C言語の文字列が文字列として終了するという事実は無視できます。ただし、ほとんどの場合、人々は依然としてRedisを使用して「テキスト情報」を保存します(C言語の文字列ルールに準拠するコンテンツは含まれていません\0)。現時点では、それらの操作はC言語および文字列関連のライブラリ関数に依存している可能性があるため、SDSの実装では2つの規則が維持されます。

  1. 文字列にメモリスペースを割り当てる場合、さらに1バイトのスペースを割り当てることを検討します。\0
  2. 文字列の内容を変更すると、最後に\0文字が追加されます

Redisの文字列

C言語では、文字列 は\0 末尾の char配列で表すことができます たとえば hello world 、C言語のように表現できます "hello world\0" 。この単純な文字列表現は、ほとんどの場合要件を満たすことができますが、長さの計算と追加の2つの操作を効率的にサポートしていません。

  • 毎回文字列の長さ(strlen(s))を計算する複雑さはθ(N)θ(N)です。
  • 文字列をN回追加するには、文字列Nメモリ(reallocを再割り当てする必要があります

Redisでは、文字列の追加と長さの計算が非常に一般的であり、  APPEND と STRLEN がこれら2つの操作です。Redisコマンドでの直接マッピングでは、これら2つの単純な操作がパフォーマンスのボトルネックになることはありません。さらに、Redisは、C文字列の処理に加えて、単純なバイト配列とサーバープロトコルも処理する必要があります。したがって、便宜上、Redisの文字列表現もバイナリセーフである必要があります。プログラムは文字列を保存しないでください。データについての仮定を行う。データは、 \0 最後のC文字列、単純なバイト配列、または他の形式のデータにすることができます。

これらの2つの理由を考慮して、Redisはsdsタイプを使用して、C言語のデフォルトの文字列表現を置き換えます。sdsは、追加と長さの計算を効率的に実装でき、バイナリセーフです。

sdsの実装

前のコンテンツでは、sdsを抽象的なデータ構造として説明してきましたが、実際、その実装は次の2つの部分で構成されています。

typedef char *sds;

struct sdshdr 
{
    int len;    // buf 已占用长度
    int free;   // buf 剩余可用长度
    char buf[]; // 实际保存字符串数据的地方
};

前記タイプが sds ある char * エイリアス(別名)、および構造が sdshdr 維持され len 、 free そして buf 3つの属性。

例として、以下が新しく作成され、hello world 文字列のsdshdr 構造も保存され ます 

struct sdshdr {
    len = 11;
    free = 0;
    buf = "hello world\0";  // buf 的实际长度为 len + 1
};

len 属性を 介し sdshdr て、複雑さθ(1)θ(1)の長さ計算操作を実現できます。

一方、 buf 余分なスペースを割り当て free 、未使用スペースのサイズを記録sdshdr することで、追加操作の実行に必要なメモリ再割り当ての 数を大幅に減らすことができます。これについては、次のセクションで詳しく説明します。

もちろん、sdsは、操作を正しく実装するための要件も提示します。処理sdshdr されたすべての 関数len とfree 属性を正しく更新する必要が あり ます。そうしないと、バグが発生します。

追加操作の最適化

前述のように、このsdshdr 構造を使用する と、複雑さθ(1)θ(1)の文字列の長さを取得できるだけでなく、追加操作に必要なメモリ再配布の数を減らすこともできます。詳細は次のとおりです。この最適化の原理を説明してください。

理解を容易にするために、例としてRedis実行インスタンスを使用して、次のコードが実行されたときにRedis内で何が起こるかを説明しましょう。

redis> SET msg "hello world"
OK

redis> APPEND msg " again!"
(integer) 18

redis> GET msg
"hello world again!"

まず、 SET コマンドが作成されhello world て1つsdshdr に保存 さ sdshdr れます。この 値は、次のとおりです。

struct sdshdr {
    len = 11;
    free = 0;
    buf = "hello world\0";
}

APPEND コマンドが実行される と、対応するもの sdshdr が更新され、文字列" again!" が元の文字列 に追加されます "hello world" 。

struct sdshdr {
    len = 18;
    free = 18;
    buf = "hello world again!\0                  ";     // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
}

ときに注意 SET コマンドが作成され sdshdr 、  属性がある  のRedisがなかったこと  と、実行した後に-追加のスペースを作成 APPENDを 、Redisのは、  必要なスペースの2倍以上のサイズを作成しました。sdshdrfree0bufbuf

この例では"hello world again!" 、保存するため 18 + 1 に合計バイトが必要です が、プログラム18 + 18 + 1 = 37 はバイトを割り当て ます-このようにして、同じものsdshdr が将来再び追加された場合 、追加されたコンテンツの長さがを超えない限り free 、属性の値の場合、buf メモリ再割り当てする必要はありません 

たとえばbuf 、新しく追加された文字列の長さが以下であるため、次のコマンドを実行してもメモリの再割り当て は発生しません 18 。

redis> APPEND msg " again!"
(integer) 25

APPEND コマンドを再度 実行 msg すると、値に対応するsdshdr 構造は 次のようになります。

struct sdshdr {
    len = 25;
    free = 11;
    buf = "hello world again! again!\0           ";     // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
}

sds.c/sdsMakeRoomFor この関数は、sdshdr このメモリ事前割り当て最適化戦略について説明し ています。以下は、この関数の擬似コードバージョンです。

def sdsMakeRoomFor(sdshdr, required_len):

    # 预分配空间足够,无须再进行空间分配
    if (sdshdr.free >= required_len):
        return sdshdr

    # 计算新字符串的总长度
    newlen = sdshdr.len + required_len

    # 如果新字符串的总长度小于 SDS_MAX_PREALLOC
    # 那么为字符串分配 2 倍于所需长度的空间
    # 否则就分配所需长度加上 SDS_MAX_PREALLOC 数量的空间
    if newlen < SDS_MAX_PREALLOC:
        newlen *= 2
    else:
        newlen += SDS_MAX_PREALLOC

    # 分配内存
    newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)

    # 更新 free 属性
    newsh.free = newlen - sdshdr.len

    # 返回
    return newsh

Redisの現在のバージョン(作成者のバージョンはredis-6.0.9)では、 

#ifndef __SDS_H
#define __SDS_H

#define SDS_MAX_PREALLOC (1024*1024)
extern const char *SDS_NOINIT;

#include <sys/types.h>
#include <stdarg.h>
#include <stdint.h>

typedef char *sds;

....

void *sds_malloc(size_t size);
void *sds_realloc(void *ptr, size_t size);
void sds_free(void *ptr);

#ifdef REDIS_TEST
int sdsTest(int argc, char *argv[]);
#endif

#endif

可以看到,SDS_MAX_PREALLOC の値はです 1024 * 1024 。つまり1MB 、文字列サイズが追加操作よりも小さい場合 sdsMakeRoomFor 、必要なサイズの2倍以上が割り当てられます。文字列のサイズが大きい 1MB 場合は、 sdsMakeRoomFor 追加の1MB スペースが割り当てられます 

この割り当て戦略はメモリを浪費しますか?

  • APPEND コマンドを実行した文字列には、 追加の事前割り当てスペースがあり、文字列に対応するキーが削除されるか、Redisをシャットダウンして再起動したときに再ロードされる文字がない限り、事前割り当てスペースは解放されません。オブジェクトには事前に割り当てられたスペースはありません。
  • APPEND コマンドを実行するための文字列キーの 数は通常多くなく、メモリフットプリントも通常は大きくないため、これは通常問題にはなりません。
  • 一方、APPEND 操作を実行する キーが多数あり、文字列のボリュームが非常に大きい場合は、Redisサーバーを変更して、文字列キーの事前に割り当てられたスペースを定期的に解放する必要があります。メモリをより効率的に使用するため。

まとめ

1.文字列の長さを取得する場合、C文字列は「\ 0」が見つかるまで文字列をトラバースする必要があります。その複雑さはO(n)であり、SDSはlen属性に直接アクセスして、の長さと複雑さを直接取得できます。文字列O(1)です。

2. SDS APIは、バッファオーバーフローを防止します。SDSがSdsCatを呼び出すと、最初にSDSスペースが十分かどうかを判断します。十分でない場合は、最初にSDSを展開してから、文字列スプライシングを実行します。

3.メモリ再配布のパフォーマンスへの影響を減らすために、SDS文字列の増加は、事前割り当て戦略を通じてメモリ事前割り当て操作を実行し、メモリのredis割り当ての数を効果的に減らすことができます。

4. SDSはバイナリセーフです。C文字列は「\ 0」であるかどうかを判断して文字列の終わりを検出し、SDSはlen属性で文字列の終わりを検出するため、「\ 0」の心配はありません。 '文字列の途中。

加えて、

  • Redis文字列はsds 、C文字列(\0 口の終わり) ではなく、 として表され char*ます。
  • C文字列と比較すると sds 、次の特徴があります。
    • 長さの計算を効率的に実行できます(strlen);
    • 追加操作を効率的に実行できます(append);
    • バイナリセキュリティ;
  • sds 追加操作を最適化します。より多くのメモリを占有するという犠牲を払って、追加操作を高速化し、メモリ割り当ての数を減らします。これらのメモリはアクティブに解放されません。

おすすめ

転載: blog.csdn.net/u013318019/article/details/110691642