なぜredisは速いのですか?

コンテンツ

まず、Redisの全体的な機能

2. Redisが高速なのはなぜですか?

1.完全にメモリ実装に基づく

2.効率的なデータ構造

3.シングルスレッドモデル

4. I/O多重化モデル

3.速くて壊れないという原則の要約


まず、Redisの全体的な機能

パノラマは、次の2つの緯度を中心に拡大できます。

1.アプリケーションの緯度

キャッシュの使用、クラスターの使用、データ構造の巧妙な使用

2.システムの緯度

3つの高に分類することができます

  1. 高性能:スレッドモデル、ネットワークIOモデル、データ構造、永続性メカニズム。
  2. 高可用性:マスタースレーブレプリケーション、センチネルクラスター、クラスターシャードクラスター。
  3. 高拡張:負荷分散

2. Redisが高速なのはなぜですか?

 

公式データによると、RedisのQPSは約100,000(1秒あたりのリクエスト数)に達する可能性があります。関係者は、公式ベンチマークテスト「Redisの速度はどれくらいですか?」を参照できます。"、アドレス:https ://redis.io/topics/benchmarks

 

横軸は接続数、縦軸はQPSです。現時点では、この写真は桁違いに反映されています。インタビューの際に、皆さんが正しく説明できることを願っています。質問しない場合、回答の桁は大きく異なります。

1.完全にメモリ実装に基づく

Redisはインメモリデータベースです。ディスクデータベースと比較すると、ディスクの速度が完全に妨げられます。ディスクデータベースの場合、最初に行うことは、IO操作を介してデータをメモリに読み込むことです。

そうです、読み取りと書き込みの操作がメモリ内で行われるかどうかに関係なく、メモリ操作とディスク操作の違いを比較してみましょう。

ディスク呼び出しスタック図

 

メモリ操作:

メモリはCPU、つまりCPU内に統合されたメモリコントローラによって直接制御されるため、メモリはCPUに直接接続され、CPUとの通信に最適な帯域幅を享受します。

Redisはデータをメモリに保存し、読み取りおよび書き込み操作はディスクのIO速度によって制限されないため、速度は飛んでいるように感じます。

最後に、画像を使用して、システムのさまざまな遅延時間を定量化します(Brendan Greggが引用したデータの一部)

 

2.効率的なデータ構造

MySQLはB+ツリーデータ構造を使用して取得速度を向上させるため、Redisの速度もデータ構造に関連している必要があります。

ここで説明するデータ構造は、Redisが提供する5つのデータ型(文字列、リスト、ハッシュ、セット、SortedSet)ではありません。

Redisで一般的に使用される5つのデータ型とアプリケーションシナリオは次のとおりです。

  • 文字列:キャッシュ、カウンター、分散ロックなど。
  • リスト:リンクリスト、キュー、Weiboフォロワータイムラインリストなど。
  • ハッシュ:ユーザー情報、ハッシュテーブルなど。
  • セット:重複排除、好き、嫌い、相互の友達など。
  • Zset:トラフィックランキング、クリックランキングなど。

速度を追求するために、さまざまなデータ型がさまざまなデータ構造を使用して速度を向上させます。各データ型は1つ以上のデータ構造によってサポートされており、6つの基礎となるデータ構造があります。

 

1)Redisハッシュテーブル

Redisは全体として、データ型が5つの型のいずれであっても、すべてのキーと値のペアを格納するためのハッシュテーブルです。ハッシュテーブルは基本的に配列であり、各要素はハッシュバケットと呼ばれます。データ型に関係なく、各バケットのエントリは実際の特定の値へのポインタを保持します。

 

データベース全体がグローバルハッシュテーブルであり、ハッシュテーブルの時間計算量はO(1)です。対応するハッシュバケットの場所を知るために各キーのハッシュ値を計算するだけで、バケット内のエントリを見つけて見つけることができます。対応するデータ。これが、Redisが高速である理由の1つです。

ハッシュ競合はどうですか?

ますます多くのデータがRedisに書き込まれると、ハッシュの衝突は避けられず、異なるキーが同じハッシュ値を計算します。

Redisは、連鎖ハッシュによって競合を解決します。つまり、同じバケット内の要素がリンクリストに保存されます。ただし、リンクリストが長すぎると検索パフォーマンスが低下する可能性があるため、Redisは速度を追求するために2つのグローバルハッシュテーブルを使用します。既存のハッシュバケットの数を増やし、ハッシュの衝突を減らすための再ハッシュ操作に使用されます。

デフォルトでキーと値のペアのデータを保存するためにハッシュテーブル1の使用を開始し、ハッシュ2は現時点ではスペースを割り当てません。より多くのデータが再ハッシュ操作をトリガーする場合は、次の操作を実行します。

  1. ハッシュテーブル2により多くのスペースを割り当てます。
  2. ハッシュテーブル1のデータをハッシュテーブル2に再マップします。
  3. ハッシュテーブル1のスペースを解放します。

ハッシュテーブル1のデータをハッシュテーブル2に再マッピングするプロセスは、1回限りのプロセスではないため、Redisがブロックされ、サービスの提供に失敗することに注意してください。

代わりに、プログレッシブ再ハッシュが使用されます。クライアント要求が処理されるたびに、ハッシュテーブル1の最初のインデックスから開始し、この位置にあるすべてのデータをハッシュテーブル2にコピーして、再ハッシュを複数回に分散します。要求プロセス中に、時間のかかるブロッキングが回避されます。

2)SDSシンプルダイナミックキャラクター

RedisはC言語で実装されているのに、なぜ新しいSDS動的文字列を作成する必要があるのでしょうか。

文字列構造は最も広く使用されており、通常、ログイン後にユーザー情報をキャッシュするために使用されます。key= userId、value=ユーザー情報JSONは文字列にシリアル化されます。

C言語で「文字列」の長さを取得するには、最初から「\ 0」までトラバースする必要があります。Redisは、速くて壊れないだけの男として、それに耐えることができません。

C言語の文字列構造とSDSの文字列構造の比較表は次のとおりです。

 

SDS文字列とC文字列の違い:

  • 文字列の長さを取得するためのO(1)時間計算量

C言語の文字列ブギー道路長情報。文字列全体をトラバースする時間計算量はO(n)であり、「\0」に遭遇するとC文字列のトラバースは終了します。

SDSのlenは、この文字列の長さ、O(1)時間計算量を格納します。

  • スペースの事前割り当て

SDSが変更された後、プログラムはSDSに必要なスペースを割り当てるだけでなく、追加の未使用スペースも割り当てます。

割り振り規則は次のとおりです。SDSに変更した後、lenの長さが1M未満の場合、プログラムはlenと同じ長さの未使用スペースを割り振ります。たとえば、len = 10の場合、再割り当て後、bufの実際の長さは10(使用済みスペース)+10(余分なスペース)+1(ヌル文字)=21になります。SDSへの変更後のlenの長さが1Mを超える場合、プログラムは1Mの未使用スペースを割り当てます。

  • 不活性空間の解放

SDSが短縮されると、プログラムは余分なメモリスペースを再利用しませんが、空きフィールドを使用してバイト数を記録し、解放しません。後で追加操作が必要になった場合、空きの未使用スペースは次の目的で直接使用されます。バイト数を減らします。メモリ割り当て。

  • バイナリセーフ

Redisでは、文字列型のデータだけでなく、一部のバイナリデータも保存できます。

バイナリデータは通常の文字列形式ではなく、「\ 0」などの特殊文字が含まれます。Cでは「\0」に遭遇すると文字列の終わりを意味しますが、SDSでは文字列の終わりはlenでマークされます財産。

3)zipListはリストを圧縮します

圧縮リストは、リスト、ハッシュ、およびソートされたセットの3つのデータ型の基礎となる実装の1つです。

リストに含まれるデータの量が少なく、各リストアイテムが小さな整数値または比較的短い長さの文字列である場合、Redisは圧縮リストをリストキーの基盤となる実装として使用します。

ziplistは、特別にコード化された一連の連続したメモリブロックで構成されるシーケンシャルデータ構造です。ziplistには、整数または文字列を格納できる複数のエントリノードを含めることができます。

ziplistには、ヘッダーにzlbytes、zltail、zllenの3つのフィールドがあります。これらは、リストが占めるバイト数、リストの最後のオフセット、およびリスト内のエントリの数を表します。圧縮リストには、リストの終わりを示すテーブルの終わり。

struct ziplist<T> {
    int32 zlbytes; // 整个压缩列表占用字节数
    int32 zltail_offset; // 最后一个元素距离压缩列表起始位置的偏移量,用于快速定位到最后一个节点
    int16 zllength; // 元素个数
    T[] entries; // 元素内容列表,挨个挨个紧凑存储
    int8 zlend; // 标志压缩列表的结束,值恒为 0xFF
}

 

最初の要素と最後の要素を見つけて見つけたい場合は、テーブルヘッダーの3つのフィールドの長さで直接見つけることができ、複雑さはO(1)です。他の要素を検索する場合、それほど効率的ではなく、1つずつしか検索できず、このときの複雑さはO(N)です。

4)両端リスト

Redisリストのデータ型は通常、キュー、Weiboフォロワーのタイムラインリストなどのシナリオで使用されます。FIFOキューであろうとFIFOスタックであろうと、ダブルエンドリストはこれらの機能を非常によくサポートします。

 

Redisのリンクリスト実装の特徴は次のように要約できます。

  • ダブルエンド:リンクリストノードにはprevとnextポインターがあり、ノードのpre-nodeとpost-nodeを取得する複雑さはO(1)です。
  • 非巡回:ヘッドノードの前のポインタとテールノードの次のポインタは両方ともNULLを指し、リンクリストへのアクセスはNULLで終了します。
  • ヘッドポインタとテールポインタの場合:リスト構造のヘッドポインタとテールポインタを介して、リンクリストのヘッドノードとテールノードを取得するプログラムの複雑さはO(1)です。
  • リンクリスト長カウンターあり:プログラムはリスト構造のlen属性を使用して、リストが保持するリンクリストノードをカウントします。リンクリスト内のノード数を取得するプログラムの複雑さはO(1)です。
  • ポリモーフィズム:リンクリストノードはvoid *ポインターを使用してノード値を格納し、リスト構造のdup、free、およびmatch属性を介してノード値にタイプ固有の関数を設定できるため、リンクリストを使用してさまざまなものを格納できます値のタイプ。

以降のバージョンでは、ziplistとlinkedlistの代わりにquicklistを使用して、リストのデータ構造を作り直しました。

クイックリストは、ziplistとlinkedlistを組み合わせたもので、linkedlistをセグメントに分割し、各セグメントはziplistを使用してコンパクトに保存し、複数のziplistは双方向ポインターを使用して連結されます。

 

これは、Redisが高速であり、パフォーマンスを向上させることができる詳細を見逃さない理由でもあります。

5)スキップリストスキップリスト

ソートされたセットタイプのソート機能は、「ジャンプリスト」データ構造を介して実装されます。

スキップリストは、ノードにすばやくアクセスするために、各ノード内の他のノードへの複数のポインターを維持する順序付けられたデータ構造です。

スキップテーブルは、平均O(logN)、最悪のO(N)ノードルックアップをサポートし、順次操作を通じてノードをバッチ処理することもできます。

次の図に示すように、リンクリストに基づいて、ジャンプテーブルはマルチレベルのインデックスを追加し、インデックス位置のいくつかのジャンプを通じてデータの迅速な配置を実現します。

 

40を見つける必要がある場合、この要素は3回の検索を実行する必要があります。

6)整数配列(intset)

コレクションに整数値の要素のみが含まれ、コレクションに少数の要素がある場合、Redisはコレクションキーの基盤となる実装として整数コレクションを使用します。構造は次のとおりです。

typedef struct intset{
     //编码方式
     uint32_t encoding;
     //集合包含的元素数量
     uint32_t length;
     //保存元素的数组
     int8_t contents[];
}intset;

コンテンツ配列は、整数コレクションの基礎となる実装です。整数コレクションの各要素は、コンテンツ配列の配列アイテム(アイテム)であり、アイテムは値の昇順で配列に配置され、配列には含まれません。重複。lengthプロパティは、整数コレクションに含まれる要素の数を記録します。これは、コンテンツ配列の長さです。

7)合理的なデータエンコーディング

Redisはオブジェクト(redisObject)を使用してデータベース内のキー値を表します.Redisでキー値ペアを作成すると、少なくとも2つのオブジェクトが作成され、1つのオブジェクトはキー値ペアとして使用されるキーオブジェクトであり、もう1つはキーと値のペア。値オブジェクト。

例:SET MSG XXXを実行すると、キーと値のペアのキーは文字列 "MSG"を含むオブジェクトであり、キーと値のペアの値オブジェクトは文字列"XXX"を含むオブジェクトです。

redisObject

typedef struct redisObject{
    //类型
   unsigned type:4;
   //编码
   unsigned encoding:4;
   //指向底层数据结构的指针
   void *ptr;
    //...
 }robj;

タイプフィールドには、文字列オブジェクト、リストオブジェクト、ハッシュオブジェクト、コレクションオブジェクト、順序付けられたコレクションオブジェクトなどのオブジェクトのタイプが記録されます。

データ型ごとに、基になるサポートは複数のデータ構造である可能性があり、どのデータ構造をいつ使用するかは、エンコード変換の問題を伴います。

次に、さまざまなデータ型がどのようにエンコードおよび変換されるかを見てみましょう。

文字列:数値が格納されている場合はint型のエンコーディングが使用され、数値以外の場合は生のエンコーディングが使用されます。

List:Listオブジェクトのエンコーディングは、ziplistまたはlinkedlistで、文字列の長さは64バイト未満、要素数は512未満で、ziplistエンコーディングを使用します。それ以外の場合は、linkedlistエンコーディングに変換します。

注:これらの2つの条件は、redis.confで変更できます。

list-max-ziplist-entries 512
list-max-ziplist-value 64

ハッシュ:Hashオブジェクトのエンコーディングは、ziplistまたはhashtableにすることができます。

Hashオブジェクトが同時に次の2つの条件を満たす場合、Hashオブジェクトはziplistエンコーディングを採用します。

  • Hashオブジェクトによって格納されるすべてのキーと値のペアの文字列の長さは64バイト未満です。
  • Hashオブジェクトによって格納されるキーと値のペアの数は512未満です。

それ以外の場合は、ハッシュテーブルエンコーディングです。

Set:Setオブジェクトのエンコードは、intsetまたはhashtableにすることができます。intsetによってエンコードされたオブジェクトは、基になる実装として整数コレクションを使用し、すべての要素を整数コレクションに格納します。

要素を整数として保存し、要素の数が特定の範囲未満の場合は、intsetエンコーディングを使用します。条件が満たされない場合は、ハッシュテーブルエンコーディングを使用します。

Zset:Zsetオブジェクトのエンコーディングはziplistまたはzkiplistにすることができます。ziplistエンコーディングで保存される場合、各セット要素は、隣り合った2つの圧縮リストを使用して保存されます。

Ziplist圧縮リストの最初のノードは要素のメンバーを格納し、2番目のノードは要素のスコアを格納し、スコアに従って昇順で配置されます。

Zsetオブジェクトが次の2つの条件を同時に満たす場合は、ziplistエンコーディングを使用します

  • Zsetによって保存される要素の数は128未満です。
  • Zset要素のメンバーはすべて64バイト未満の長さです。

上記の条件のいずれかが満たされない場合、ziplistはzkiplistエンコーディングに変換されます。注:これらの2つの条件は、redis.confで変更できます。

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

 

3.シングルスレッドモデル

CPUを最大限に活用するために、Redisがマルチスレッドの並列実行ではなくシングルスレッドで実行されるのはなぜですか?

明確にしておきたいのは、RedisのシングルスレッドはRedisのネットワークIOを指し、キーと値のペアの命令の読み取りと書き込みは1つのスレッドによって実行されるということです。Redisの永続性の場合、クラスターデータの同期、非同期削除などは他のスレッドによって実行されます。

シングルスレッドが使用される理由については、最初にマルチスレッドの欠点が何であるかを理解しましょう。

マルチスレッドのデメリット:

マルチスレッドを使用すると、多くの場合、システムスループットを向上させ、CPUリソースを最大限に活用できます。

ただし、マルチスレッドを使用した後、適切なシステム設計がないと、次の図に示すシナリオが発生する可能性があります。スレッド数を増やすと、初期段階のスループットが向上します。さらにスレッドを追加すると、システムスループットが向上します。ほとんど増加しません。、ドロップさえします!

 

各タスクを実行する前に、CPUはタスクがロードされて実行を開始した場所を知る必要があります。つまり、システムは、CPUレジスタとプログラムカウンタを事前設定するのを支援する必要があります。これは、CPUコンテキストと呼ばれます。

これらの保存されたコンテキストはシステムカーネルに保存され、タスクが再スケジュールされたときに再度ロードされます。このように、タスクの元の状態は影響を受けず、タスクは継続的に実行されているように見えます。

コンテキストを切り替えるときは、一連の作業を完了する必要があります。これは、非常にリソースを消費する操作です。

さらに、複数のスレッドが共有データを並行して変更する場合、データの正確性を確保するために、ロックメカニズムが必要になると、パフォーマンスのオーバーヘッドが増加し、共有リソースの同時アクセス制御の問題に直面します。

マルチスレッド開発の導入には、共有リソースの同時読み取りと書き込みを保護するための同期プリミティブの使用が必要であり、コードの複雑さが増し、デバッグが困難になります。

シングルスレッドの利点は何ですか?

  1. スレッド作成によるパフォーマンスの消費はありません。
  2. マルチスレッドスイッチングのオーバーヘッドなしに、コンテキストスイッチングによって引き起こされるCPU消費を回避します。
  3. さまざまなロックの問題を考慮せずに、ロックの追加、ロックの解放、デッドロックなどのスレッド間の競合を回避します。
  4. コードはより明確で、処理ロジックは単純です。

シングルスレッドはCPUリソースを十分に活用していませんか?

公式回答:Redisはメモリベースの操作であるため、CPUはRedisのボトルネックではありません。Redisのボトルネックは、マシンメモリのサイズまたはネットワーク帯域幅である可能性があります。シングルスレッドは実装が簡単で、CPUがボトルネックになることはないため、シングルスレッドソリューションを採用するのが論理的です。元のアドレス:https://redis.io/topics/faq。

4. I/O多重化モデル

Redisは、I/O多重化テクノロジーを使用して接続を同時に処理します。Epoll+自己実装のシンプルイベントフレームワークを採用。epollでの読み取り、書き込み、クローズ、および接続はイベントに変換され、epollの多重化機能を使用して、IOに少し時間を無駄にすることはありません。

IO多重化について説明する前に、まず基本的なIO操作がどのように行われるかを理解しましょう。

基本的なIOモデル:

基本的なネットワークIOモデルは、getリクエストを処理するときに、次のプロセスを実行します。

  1. クライアントを確立して受け入れます。
  2. ソケットrecvからの読み取り要求。
  3. クライアントから送信されたリクエストを解析します。
  4. get命令を実行します。
  5. クライアントデータに応答する、つまり、データをソケットに書き戻す。

その中で、bind / listen、accept、recv、parse、sendはネットワークIO処理に属し、getはKey-Valueデータ操作に属します。Redisはシングルスレッドであるため、最も基本的な実装は、スレッドで上記の操作を順番に実行することです。

重要な点は、acceptとrecvがブロックすることです。Redisが接続要求でクライアントをリッスンしているが、接続を正常に確立できない場合、accept()関数でブロックされ、他のクライアントがとの接続を確立できなくなります。 Redis。

同様に、Redisがrecv()を介してクライアントからデータを読み取る場合、データが到着していない場合、Redisは常にrecv()でブロックします。

 

ブロッキングの理由は、従来のブロッキングIOを使用しているためです。つまり、読み取り、受け入れ、受信などのネットワーク操作は常にブロックして待機します。以下に示すように:

IO多重化

多重化とは複数のソケット接続を指し、多重化とはスレッドの多重化を指します。多重化には、select、poll、epollの3つの主要な手法があります。epollは、利用可能な最新かつ最高の多重化テクノロジーです。

その背後にある理論的根拠は、カーネルがアプリケーション自体の接続を監視しているのではなく、アプリケーションのファイル記述子を監視しているということです。

クライアントが実行されると、さまざまなイベントタイプのソケットが生成されます。サーバー側では、I / O多重化プログラム(I / O多重化モジュール)がメッセージをキュー(つまり、下図のI / O多重化プログラムのソケットキュー)に入れて、ファイルを渡します。イベントディスパッチャは、それを別のイベントハンドラに転送します。

簡単に言うと、Redisシングルスレッドの場合、カーネルは常にソケット上の接続要求またはデータ要求を監視し、要求が到着すると、処理のためにRedisスレッドに渡されます。これにより、 1つのRedisスレッドが複数のIOストリームを処理する効果。

select / epollは、イベントベースのコールバックメカニズムを提供します。つまり、さまざまなイベントが発生した場合、対応するイベントハンドラーが呼び出されます。そのため、Redisはイベントを処理してRedisの応答パフォーマンスを向上させてきました。

 

Redisスレッドは、特定のリスニングソケットまたは接続されたソケットでブロックしません。つまり、特定のクライアント要求処理でブロックしません。このため、Redisは複数のクライアントに同時に接続してリクエストを処理できるため、同時実行性が向上します。

3.速くて壊れないという原則の要約

 

  1. 純粋なメモリ操作は、一般的に単純なアクセス操作です。スレッドは多くの時間を消費し、時間は主にIOに集中するため、読み取り速度は高速です。
  2. Redis全体がグローバルハッシュテーブルであり、その時間計算量はO(1)です。ハッシュの競合によってリンクリストが長くなりすぎるのを防ぐために、Redisは再ハッシュ操作を実行してハッシュバケットの数を増やします。ハッシュの競合を減らします。また、一度に大量の再マッピングデータが原因でスレッドがブロックされるのを防ぐために、プログレッシブリハッシュが使用されます。ブロッキングを回避するために、複数の要求プロセスの後に1回限りのコピーを合計に巧みに償却します。
  3. Redisは非ブロッキングIOを使用します:IO多重化、単一スレッドを使用して記述子をポーリングし、データベースのオープン、クローズ、読み取り、および書き込みをイベントに変換します。Redisは、より効率的で高い独自のイベントセパレーターを使用します。
  4. シングルスレッドモデルは、各操作の原子性を保証し、スレッドコンテキストの切り替えと競合を減らします。
  5. Redisは、プロセス全体でハッシュ構造を使用するため、読み取り速度が速くなります。また、テーブルの圧縮、短いデータの圧縮と保存、テーブルのスキップ、順序付けられたデータ構造を使用した高速化など、データストレージを最適化する特別なデータ構造もあります。読み取り速度。
  6. 保存されている実際のデータタイプに応じて、異なるエンコーディングを選択してください

 

おすすめ

転載: blog.csdn.net/demored/article/details/123819305