*Redis プロトコルの仕様

RedisのクライアントはRedisサーバとRESP (REdis Serialization Protocol) と呼ばれるプロトコルを使って通信をします。プロトコルは特にRedisのために設計されましたが、他のクライアント-サーバ ソフトウェア プロジェクトのために使うことができます。

RESP は以下の事について妥協して解決します:

  • 実装が簡単。
  • 解析が早い。
  • 人が読める。

RESP は整数、文字列、配列のような様々なデータ型をシリアライズ化することができます。エラーについても特定の型があります。リクエストは実行するコマンドの引数を表す文字列の配列として、クライアントからRedisサーバに送信されます。Redis はコマンド固有のデータ型で応答します。

RESP はバイナリ セーフであり、一括データを送信するために事前に決められた長さを使うため、あるプロセスから別のプロセスに転送される一括データを処理する必要はありません。

注意: ここで説明されるプロトコルはクライアント-サーバ通信のためにのみ使われます。Redis クラスタはノード間でメッセージを交換するために異なるバイナリプロトコルを使用します。

*ネットワーク層

クライアントはポート6379へのTCP接続を作成してRedisサーバに接続します。

RESP は技術的にはTCPだけのものではありませんが、RedisのコンテキストではプロトコルではTCP接続(あるいはUnixソケットのような同等のストリーム指向の接続)だけが使われます。

*リクエスト-応答 モデル

Redis は様々な引数からなるコマンドを受け付けます。コマンドを受信するとそれを処理し、応答をクライアントに返します。

これは可能な限り最も単純なモデルですが、2つの例外があります:

  • Redis はパイプラインをサポートします (このドキュメントの公判で説明します)。つまりクライアントは複数のコマンドを一度に送信して、後で応答を待つことができます。
  • Redisクライアントが Pub/Sub チャンネルを購読すると、プロトコルはセマンティクスを変更しpush プロトコルになります。受信するとすぐにサーバはクライアントに(クライアントが購読しているチャンネルについて)新しいメッセージを自動的に送信するため、クライアントはコマンドを送信する必要がなくなります。

上記の2つの例外を除いて、Redisプロトコルは単純なリクエスト-応答プロトコルです。

*RESP プロトコルの説明

RESPプロtコルはRedis 1.2で導入されましたが、Redis 2.0ではRedisサーバと通信するための標準的な方法になりました。これはRedisクライアントに実装する必要があるプロトコルです。

RESPは実際には以下のデータ型をサポートするシリアライズ化プロトコルです: 単純な文字列、エラー、整数、一括文字列 および配列。

リクエスト-応答プロトコルとしてRedisでRESPが使われる方法は以下の通りです:

  • クライアントはRedisサーバに一括文字列のRESP配列としてコマンドを送信します。
  • サーバはコマンドの実装に応じてRESP型のいずれかで応答します。

RESPでは、一部のデータの型は最初のバイトに依存します:

  • 単純な文字列については、応答の最初のバイトは "+" です
  • エラーについては、応答の最初のバイトは "-" です
  • 整数については、応答の最初のバイトは ":" です
  • 一括文字列 については、応答の最初のバイトは "$" です
  • 配列については、応答の最初のバイトは "*" です

さらに、RESPは後で説明するように一括文字列あるいは配列の特別な変形を使ってNull値を表すことができます。

RESPではプロトコルの異なる部分は常に "\r\n" (CRLF) を使って終了します。

*RESPの単純な文字列

単純な文字列は以下の方法でエンコードされます: プラス記号に、CR あるいは LF 文字 (改行は許可されません)を含むことができない文字が続き、CRLF (つまり "\r\n") で終了します。

単純な文字列は最小のオーバーヘッドで非バイナリセーフ文字列を転送するために使われます。例えば、多くのRedisコマンドは成功時に単に "OK" を応答します。RESPの単純な文字列は以下の5バイトでエンコードされます:

"+OK\r\n"

バイナリセーフな文字列を送信するには、RESPの一括文字列が代わりに使われます。

Redisが単純な文字列で応答する場合、クライアントライブラリは呼び出し側に最後のCRLFバイトを除いて '+' の後の最初の文字から文字列の最後の文字までで構成される文字列を返す必要があります。

*RESPのエラー

RESPはエラーについて特定のデータ型を持ちます。実際には、エラーはRESPの単純な文字列とまったく同じですが、最初の文字がプラスの代わりにマイナス '-' 文字です。RESPでの単純な文字列とエラーとの実際の違いは、エラーはクライアントによって例外として扱われ、エラー型を構成する文字列がエラーメッセージそのものであるということです。

基本的な形式は以下の通りです:

"-Error message\r\n"

エラー応答は何か問題が起きた時にのみ送信されます。例えば、間違ったデータ型に対して操作を行おうとした場合、あるいはコマンドが存在しない場合などです。エラー応答が受信されると、ライブラリ クライアントによって例外が発生します。

以下はエラー応答の例です:

-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

"-" の後の最初の単語から、最初の空白あるいは改行までは、返されるエラーの種類を表します。これはRedisで使用される単なる規則であり、RESPエラー形式の一部ではありません。

例えば、ERR は一般的なエラーで、WRONGTYPE は間違ったデータ型に対してクライアントが操作を行おうとしたことを意味する、より特定のエラーです。これはError Prefix と呼ばれ、クライアントが時間の経過とともに変わるかもしれない指定された正確なメッセージに依存せずサーバから返されるエラーの種類を理解できるようにするための方法です。

クライアントの実装は、様々な種類のエラーに対して様々な例外を返すか、エラー名を文字列として呼び出し元に直接提供することでエラーをトラップする一般的な方法を提供するかもしれません。

ただし、このような機能はほとんど有用ではないため、重要とみなすべきではありません。また限定されたクライアントの実装は単純にfalseのような一般的なエラー条件を返すかもしれません。

*RESPの整数

この型は ":" バイトが先頭に付く整数を表すCRLFで終了する文字列です。例えば、":0\r\n" あるいは ":1000\r\n" が整数の応答です。

多くのRedisコマンドは INCR, LLEN および LASTSAVE のようなRESPの整数を返します。

返される整数には特別な意味はありません。INCR についてはインクリメントの数、LASTSAVE についてはUNIX時間などです。ただし、返される整数は符号付き64ビット整数の範囲にあることが保証されます。

整数の応答はtrueあるいはfalseを返すために広く使用されます。例えばEXISTS あるいは SISMEMBER のようなコマンドはtrueについては1を、falseについては0を返します。

SADD, SREM および SETNX のような他のコマンドは、操作が実際に行われた時に1を返し、そうでなければ0を返します。

以下のコマンドは整数の応答を返します: SETNX, DEL, EXISTS, INCR, INCRBY, DECR, DECRBY, DBSIZE, LASTSAVE, RENAMENX, MOVE, LLEN, SADD, SREM, SISMEMBER, SCARD

*RESPの一括文字列

一括文字列は長さが512MBまでの1つのバイナリセーフ文字列を表すために使われます。

一括文字列は以下の方法でエンコードされます:

  • CRLFで終了する、文字列を構成するバイト数(プリフィックス長)が続く "$"。
  • 実際の文字列データ。
  • 最後の CRLF。

従って文字列 "foobar" は以下のようにエンコードされます:

"$6\r\nfoobar\r\n"

空の文字列の場合は単に以下のようになります:

"$0\r\n\r\n"

RESP一括文字列はNull値を表すために使われる特別な形式を使って、値が存在しないことを通知するためにも使うことができます。この特別な形式では長さが-1で、データが無いため、Nullは以下のように表現されます:

"$-1\r\n"

これはNull 一括文字列と呼ばれます。

サーバがNull一括文字列を返す場合は、クライアント ライブラリAPIは空の文字列ではなくnilオブジェクトを返すべきです。例えば、Rubyライブラリは 'nil' を返し、一方で C ライブラリは NULL (あるいは応答オブジェクトに特別なフラグを設定)を返す必要があります。

*RESP 配列

クライアントはRESP配列を使ってRedisサーバにコマンドを送信します。同様に、要素のコレクションをクライアントに返す特定のRedisコマンドは、応答の型としてRESP配列を使います。その例としてリストの要素を返す LRANGE コマンドがあります。

RESP配列は以下の形式を使って送信されます:

  • 最初のバイトとして * 文字、続いて10進数としての配列内の要素の数、CRLFが続きます。
  • 配列の全ての要素に対する追加のRESP型。

従って空の配列は以下の通りです:

"*0\r\n"

一方で2つのRESP一括文字列 "foo" と "bar" は次のようにエンコードされます:

"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

ご覧の通り、配列の前の*<count>CRLF部分の後に、配列を構成する他のデータ型が次々に連結されます。例えば、3つの整数の配列は以下のようにエンコードされます:

"*3\r\n:1\r\n:2\r\n:3\r\n"

配列は混合型を含むことができ、要素が同じ型である必要はありません。例えば、4つの整数と一括文字列のリストは以下のようにエンコードすることができます:

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

(応答は分かり易くするために複数の行に分割されています)。

サーバが送信した最初の行は*5\r\nで5つの応答が続くことを指定します。その後、マルチ一括応答の項目を構成する全ての応答が転送されます。

Null配列の概念も同様に存在し、Null値を指定する代替方法です (通常はNull一括応答が使われますが、歴史的な理由から2つの形式があります)。

例えば、BLPOP コマンドがタイムアウトすると、以下の例のように-1のカウントを持つNull配列を返します:

"*-1\r\n"

RedisがNull配列を応答する場合、クライアントライブラリ APIは空の配列ではなくnullオブジェクトを返さなければなりません。これは空のリストと別の条件 (例えば BLPOP コマンドのタイムアウト条件) を区別するために必要です。

RESPでは配列の配列が可能です。例えば、2つの配列の配列は以下のようにエンコードされます:

*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Foo\r\n
-Bar\r\n

(形式は読みやすくするために分割されました)。

上のRESPデータ型は3つの整数 1, 2, 3 と、単純な文字列およびエラーの配列を含む配列を構成する2つの要素の配列をエンコードします。

*配列内のNull要素

配列の唯一の要素がNullの場合があります。これは、この要素が空の文字列ではなく欠落していることを知らせるために、Redis応答の中で使われます。指定されたキーが欠落している時に、GET パターン オプションと一緒にSORTコマンドで使用すると発生する可能性があります。Null要素を含む配列応答の例:

*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

2つ目の要素がNullです。クライアント ライブラリは以下のようなものを返す必要があります:

["foo",nil,"bar"]

これは前の章で述べたことの例外ではなく、プロトコルをさらに詳細に指定するための単なる例であることに注意してください:

*Redisサーバにコマンドを送信

今ではRESPシリアライズ化形式に慣れたので、Redisクライアント ライブラリの実装を書くことは簡単です。クライアントとサーバの間の相互作用がどのように機能するのかをさらに指定することができます:

  • クライアントはRedisサーバに一括文字列のみで構成されるRESP配列を送信します。
  • Redisサーバは、有効なRESPデータ型を応答として送信するクライアントに応答します。

従って、例えば一般的な相互作用は以下のようになります。

クライアントはキーmylistに格納されているリストの長さを取得するためにコマンドLLEN mylistを送信し、サーバは以下の例のように整数の応答を使って応答します (C: クライアント、S: サーバ)。

C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n

S: :48293\r\n

いつものように平易化のためにプロトコルの異なる部分を分割しますが、実際の相互作用はクライアントが送信している全体としての *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n です。

*複数のコマンドとパイプライン

クライアントは複数のコマンドを発行するために同じ接続を使うことができます。次のコマンドを発行する前に以前のコマンドのサーバ応答を読み取る必要無しに、クライアントが単一の書き込み操作を使って複数のコマンドを送信できるように、パイプラインがサポートされます。全ての応答は最後に読むことができます。

詳細はパイプラインに関するページを調べてください。

*インライン コマンド

使えるものがtelnetだけで、コマンドをRedisサーバに送信する必要がある場合があります。Redisプロトコルは実装が簡単ですが、対話型セッションでの使用は理想的ではなく、redis-cliが常に利用可能とは限りません。このため、Redisはインライン コマンド形式と呼ばれる人間向けに設計された特別な方法でもコマンドを受け付けます。

以下はインライン コマンドを使ったサーバ/クライアント チャットの例です (サーバ チャットはS:で始まり、クライアント チャットはC:で始まります)

C: PING
S: +PONG

以下は整数を返すインライン コマンドの別の例です:

C: EXISTS somekey
S: :0

基本的にtelnetセッションの中で空白で区切られた引数を記述するだけです。統一されたリクエストプロトコルで使われる * で始まるコマンドはないため、Redisはこの条件を検知しコマンドを解析することができます。

*Redisプロトコルのための高性能パーサ

Redisプロトコルは非常に人間が読みやすく実装しやすいですが、バイナリ プロトコルと同様のパフォーマンスで実装することができます。

RESPは一括データを転送するために固定長を使うため、例えばJSONで発生するような特殊文字のペイロードを検査したり、サーバに送信する必要のあるペイロードをクォートする必要はありません。

一括およびマルチ一括の長さは以下のCコードのようにCR文字を検査しながら文字ごとに1つの操作を実行するコードで処理できます:

#include <stdio.h>

int main(void) {
    unsigned char *p = "$123\r\n";
    int len = 0;

    p++;
    while(*p != '\r') {
        len = (len*10)+(*p - '0');
        p++;
    }

    /* Now p points at '\r', and the len is in bulk_len. */
    printf("%d\n", len);
    return 0;
}

最初のCRが識別された後で、次のLFと共に処理せずにスキップすることができます。そして、一括データはペイロードを検査しない単一の読み取り操作を使って読み取ることができます。最後に、残りのCRとLF文字は処理せずに破棄されます。

パフォーマンスはバイナリ プロトコルに匹敵しますが、Redisプロトコルはほとんどの高レベル言語での実装が非常に簡単で、クライアント ソフトウェアでのバグを減らします。

TOP
inserted by FC2 system