The Little Redis Book は the Attribution-NonCommercial 3.0 Unported license の元でライセンスされます。この本にお金を払う必要はなく、またそうすべきではありません。
複製、配布、修正、提示は自由に行えますが、この本の著者が私 Karl Seguin であることを常に明らかにしてくれるようお願いします。また、この本を商用利用してはいけません。
ライセンスの全文は以下で読むことが出来ます。
http://creativecommons.org/licenses/by-nc/3.0/legalcode
Karl Seguin は様々な分野と技術に長けた開発者です。オープンソースプロジェクトの活発な貢献者であり、技術記事の著者であり、時に講演者でもあります。彼はRedisの他にもいくつかのツールについて様々な記事を書いています。 Redisは彼のカジュアルゲーム開発者向けの3つのサービス: mogade.comでランキングや統計にその力を発揮しています。
Karl は The Little MongoDB Book という本も書きました。この本はMongoDBについて書かれており、フリーで人気があります。
彼のブログは http://openmymind.net で、 Twitterアカウントは @karlseguin です
目と心と情熱で私を助けてくれた Perry Neal に特別な感謝を捧げます。貴方の助けはかけがえなのないものでした、ありがとう。
この本の最新版は以下から入手できます。 http://github.com/karlseguin/the-little-redis-book
過去何年かに渡って、データを保存し、検索する技術は信じがたいペースで成長してきました。 リレーショナルデータベースが依然としてその存在価値を示す一方、データ保存に関するエコシステムは様変わりしました。
あらゆる新しいツールやソリューションの中で、Redisは私にとってもっともエキサイティングなものです。 その理由の第一は、信じがたいほどに簡単に学べるということです。Redisに慣れるのにかかる時間といえば数時間といったところでしょう。 第二に、Redisは特定の分野の問題を解決すると同時に、きわめて汎用的です。つまりどういうことかというと、Redisはありとあらゆるデータを取り扱う完璧なツールではないということです。Redisを知るにつれ、何がRedisに適していて、何がそうでないか、すぐに明らかになっていくでしょう。Redisの得手・不得手を知ることは、開発者とても素晴らしい体験です。
Redisだけを使ってシステム全体を作り上げることはできますが、ほとんどの人はRedisを一般的なデータソリューションの補助と見なすと思います。そのソリューションが伝統的なRDBであっても、ドキュメント指向データベースあっても、それ以外のものであっても、変わらないでしょう。Redisは特定の機能を実装するために使われるようなソリューションです。その意味で、Redisはインデックスエンジンに似ています。アプリケーションをLuceneだけで作ったりはしないでしょうが、強力な検索が必要ならLuceneはユーザにとっても開発者にとってもより良い経験となります。もちろんRedisとインデックスエンジンで似ているところといえばそれだけですが。
この本の目的はRedisを使いこなすための基礎を学ぶことです。Redisの5つのデータ構造に焦点を当てて、様々なデータモデリングアプローチを検討します。また同時にいくつかの主な管理とデバッグの方法にも触れます。
我々は皆異なった学び方をします。ある人は手を動かすことを好み、またある人はビデオを見ることを好み、さらにある人は読むことを好みます。 Redisを理解するのには実際に触ってみるのが一番です。Redisのインストールは簡単で、必要なことは全部できる対話環境が使えます。ここで少し時間をとって自分のマシンでRedisを動かしてみましょう。
Redisは公式にはWindowsをサポートしていません。しかしいくつか方法があります。これらのやり方を実運用で使いたいとは思わないでしょうが、個人的には開発に使う限り、困ったたことは一度もありません。
マイクロソフト・オープンテクノロジーズ社による移植版がhttps://github.com/MSOpenTech/redis にあります。この本を書いている時点で、これは実運用向けではありません。
以前からあるもう一つの方法は https://github.com/dmajkic/redis/downloads から最新のバージョンをダウンロードすることです。 (リストの一番上にあるのがそうです) zipファイルを解凍してから、自分のアーキテクチャに合わせて32bitフォルダか64bitフォルダを開きます。
*nixとMacユーザにとってはソースからビルドするのが最善の方法です。ビルド方法はその最新のバージョン番号と共に http://redis.io/download で手に入ります。この本を書いている時点での最新版は3.0.3です。このバージョンをインストールするには以下のようにします。
wget http://download.redis.io/releases/redis-3.0.3.tar.gz
tar xzf redis-3.0.3.tar.gz
cd redis-3.0.3
make
(別の方法として、Redisはパッケージマネージャ経由でインストールすることも出来ます。例えばMacOSユーザは brew install redis
と打つことでインストールできます)
ソースからビルドした場合は、コンパイルされたバイナリは src
ディレクトリに置かれます。 src
ディレクトリに移動するには cd src
を実行します。
全てが上手くいったら、redisバイナリが利用可能になっているはずです。Redisにはいくつかの実行ファイルがあります。ここではRedisサーバとRedisコマンドラインインターフェイスに焦点を当てます。まずサーバを実行しましょう。Windowsでは redis-server
をダブルクリックします。 *nixかMacOSXでは ./redis-server
を実行します。
スタートアップメッセージを読むと、 redis.conf
ファイルが見つからないという警告が出ているのに気がつくと思います。Redisはデフォルト設定で起動しています。これからの演習にはデフォルト設定で十分です。
次にRedisコンソールを立ち上げます。Windowsでは redis-cli
をダブルクリックします。*nix/MacOSXでは ./redis-cli
を実行します。これでローカルで実行されているサーバにポート6379でつながります。
コマンドラインに info
と入力することでRedisがきちんと動いているかどうか確かめることが出来ます。正常に動いていればサーバの稼働状況を示す大量のキーと値のペアを目にすることでしょう。
ここまででなにか問題があれば 公式Redisサポートグループ で助けを求めることをお勧めします。
これから見ていくように、RedisのAPIは関数の集合として表現するのが最適です。 APIは非常に単純で順序だっており、これはつまり、コマンドラインツールを使おうが、好みの言語のドライバを使おうが、やることはほとんど同じだということです。したがって以降の内容を好みのプログラミング言語で実行しても何ら問題はないでしょう。もし必要があれば client page へ行ってドライバをダウンロードしてください。
なにがRedisを際立たせているのでしょうか?Redisはどんなタイプの問題を解決するのでしょうか?Redisを使う際に開発者が気をつけるべきことは何でしょうか?これらの問いに答える前に、Redisとは何かを理解する必要があります。
Redisはインメモリ・パーシスタント・キーバリューストアであると表現されることが多いです。これは正確な表現だとは私は思いません。Redisは確かに全てのデータをメモリ上に保持し(後ほど詳述)、そのデータを永続化のためにディスクに書き出します。しかし、Redisは単なるキーバリューストア以上の存在です。この誤解から離れることは重要です。さもなければ、RedisおよびRedisが解決する問題への見方が大変狭いものになるでしょう。
実際にはRedisは5つの異なったデータ構造を提供しており、それらのうち一つだけが、典型的なキーバリュー構造です。これら5つのデータ構造と、それらが機能する仕組み、提供するメソッド、モデル化できるものを理解することが、Redisを理解する鍵です。しかし、ます最初に、データ構造を提供するとは、一体どういうことかみてみましょう。
仮にデータ構造というコンセプトをRDBの世界に当てはめてみるなら、データベースが提供しているデータ構造は「テーブル」だけだと言えます。テーブルは複雑で柔軟です。モデルを作ったり、格納したり、データを操作したりとテーブルについてできないことはほとんどありません。しかし、テーブルの汎用性は完全無欠ではありません。特に、本来あるべき単純さや速度が実現できない場合があります。もし一つのデータ構造で何でもかんでもやろうとするのではなく、より特化した構造を使えるとしたらどうでしょうか?もしかしたらやれないこと(あるいはうまくやれないこと)もあるでしょうが、単純さと速さを手に入れるのは確かではないでしょうか?
特定の問題には特定のデータ構造を使う、これはまさに我々がプログラムを書くときにやっていることではないでしょうか?ハッシュテーブルやスカラー変数をあらゆるデータに使ったりはしないでしょう。私はこれがRedisのアプローチを上手に表現していると思います。スカラーやリストやハッシュや集合を扱うなら、それをそのまま保存すればいいのではないでしょうか?どうして値の存在のチェック自体が、 exists(key)
以上の複雑なことになったり、あるいは O(1)(項目数に関わらず一定の時間で検索できること)よりも遅くなったりするのでしょうか?
Redisの基本コンセプトは、お馴染みのデータベースの基本コンセプトと同じです。データベースはデータの集合を保持します。データベースの典型的な利用例はアプリケーションのデータをひとまとめにして他のアプリケーションから分離することです。
Redisにおいて、データベースはシンプルに番号で区別されます。デフォルトのデータベースは 0
です。別のデータベースに切り替えたいなら、select
コマンドで変更できます。コマンドラインインターフェイスで select 1
と入力してみましょう。Redisが OK
メッセージを返し、プロンプトが redis 127.0.0.1:6379[1]
のように変わるはずです。デフォルトのデータベースに戻りたければ、コマンドラインインターフェイスで select 0
と入力してください。
Redisは単なるキーバリューストア以上の存在ですが、本質的にはRedisの5つのデータ構造は全てキーと値を持ちます。Redisについてさらに学ぶ前に、ますキーと値について理解することが必須です。
キーはデータを識別する方法です。キーについては後ほど十分に取り扱いますので、今のところはキーが users:leto
のような見た目をしていると知っていれば十分です。このようなキーが leto
という名前のユーザについての情報を格納しているということは容易に推測できるでしょう。 Redisにとってコロンは何ら特別な意味を持ちません。しかしキーの意味の区切りにセパレータを用いることは一般的です。
値はキーに関連付けられた実際のデータのことです。値はどんな形でも構いません。文字列を入れることもあれば、整数を入れることもあり、また時にはシリアライズされたオブジェクトのこともあるでしょう(JSONとかXMLとか、あるいは他のフォーマットでも)。大抵の場合、Redisは値を単なるバイト列として取り扱い、中身が何であっても気にしません。しかし、ドライバ毎にシリアライゼーションの取り扱いに差異があることには注意してください(ドライバによってはシリアライゼーションは開発者の仕事になります)。この本では文字列と整数とJSONだけを取り扱うことにします。
すこし手を動かしましょう、次のコマンドを入力してください:
set users:leto '{"name": "leto", "planet": "dune", "likes": ["spice"]}'
これがRedisのコマンドの基本形です。まずコマンドを指定します、上の例ではset
です。次に引数がきます。 set
コマンドは2つの引数を取ります。これから設定しようとするキーと、キーに設定する値です。全てではありませんがほとんどの場合コマンドはキーを引数に取り、その場合たいていキーが最初にきます。今設定した値を取り出すにはどうすればいいかわかりますか?
お分かりだと思いますが、わからなくても心配ありません。
get users:leto
他の組み合わせも試してみてください。キーと値は基本となる概念です。そしてget
コマンドとset
コマンドはキーと値を扱う一番単純な方法です。もっとユーザを作り、違ったキーを試し、違った値を入れてみてください。
次に2つのことを明らかにしましょう。ことRedisにおいては、キーが全てであり、値には意味がありません。別の言い方をすれば、Redisでは値についてクエリを投げることはできません。上の例で言えば、惑星dune
に住むユーザを見つけることはできないのです。
多くの人はこれを聞いて少し不安に思うでしょう。私達にとてデータクエリが非常に柔軟で強力なことが当たり前になっています。そのためRedisのやり方は単純すぎて現実的ではないように思えるでしょう。しかしなにも心配することはありません。思い出してください、Redisは万能のソリューションではありません。クエリの制限によってRedisには適さない問題というのもあるのです。また、場合によってはデータをモデリングする別の方法を見つけることもあるでしょう。
これからもっと具体的な例を見ていくことにしますが、この基本的事実を理解しておくことは重要です。これによって、なぜ値は何でもありなのかが理解できます。なぜならRedisは決して値を読んだり理解したりする必要がないからです。また、この新しい考え方でデータをモデリングする際の助けにもなります。
先ほどRedisはインメモリ パーシスタント ストア だといいました。永続化の点について言えば、デフォルトではRedisは変更されたキーの数に基づいてデータベースのスナップショットをディスクに保存します。 X個のキーが変更されたら、Y秒ごとにデータベースを保存するという設定ができます。デフォルトでは1000以上のキーが変更された場合は60秒ごとに、9あるいはそれ以下のキーが変更された場合には15分おきにデータベースを保存します。
別の方法として、あるいはスナップショットと併せて、Redisは追加モードで動かすことができます。キーが追加/変更される度にディスク上のappend-onlyファイルが更新されます。ハードウェアなりソフトウェアなりの障害はつきものです。パフォーマンスを得るために60秒間のデータを失ってもいい場合はあるでしょうが、逆にそのようなデータの喪失が許されないこともあります。 Redisではこの2つを選ぶことができます。第6章では3つ目の選択として永続化作業をスレーブに任せる方法を紹介します。
メモリに関して言えば、Redisは全てのデータをメモリ上に保持します。これはRedisを動かすのにはお金がかかるということを意味します。RAMはいまだにサーバの最も高価なパーツです。
私はデータの取るスペースがどれほど少ないものかを理解していない開発者がいると感じます。シェイクスピアの全作品はおおよそ5.5MBです。スケーリングについては、他のソリューションはIOかCPUが制限になりがちです。 RAMあるいはIOの限界によってたくさんのマシンへとスケールアウトする必要があるかどうかは、取り扱うデータタイプと、データをどう保存するか、そしてどう検索するかに強く依存します。もし巨大なマルチメディアファイルをRedisに保存するのでなければ、メモリについてはおそらく問題にはなりません。アプリケーションにとってはメモリバウンドよりもIOバウンドのほうが問題になりがちでしょう。
Redisは仮想メモリをサポートしています。しかし、この機能はRedisの開発者たちに失敗だとみなされています。そのため、仮想メモリの利用は非推奨になっています。
(追記:5.5MBのシェイクスピア全集は約2MBに圧縮することができます。Redisは自動で圧縮を行いませんが、値はバイト列なので、あなた自身がCPUリソースを使って圧縮/解凍を行い、RAMを節約できない理由はなにもありません。)
ここまで多くのメタなトピックに触れてきました。具体的なRedisの使い方に踏み込む前に一度これらをまとめておきたいと思います。とりわけクエリの制限、データ構造、そしてRedis流メモリへのデータ保存の方法についてです。
これら3つを組み合わせると素晴らしい物を得られます。それは速さです。「そりゃ速いだろうさ、全部メモリ上にあるんだから」と思う人が居るでしょうが、それは速さの一部分にすぎません。 Redisが他のデータベースと比較して真に輝くのはその特別なデータ構造によってです。
一体どれほど速いのでしょう?それは多くの要因に依ります。使うコマンド、データタイプ、そういったことです。しかしRedisの速度は「一秒に」何万とか、何十万とかの単位で測定されます。これは redis-benchmark
コマンド(redis-serverやredis-cliと同じフォルダにあります)を実行することでユーザ自身が確認できます。
私はかつてリレーショナルデータベースをRedisに変更したことがあります。 RDBでのロードテストには5分以上かかりましたが、Redisでは150ミリ秒で終わりました。このような劇的なパフォーマンスアップはいつも得られるというわけではありませんが、私の伝えたいことは理解してもらえるのではないかと思います。
このRedisの性質を理解することは重要です。なぜならそれがRedisの使い方に大きな影響を与えるからです。 SQLを使い慣れた開発者はデータベースへの問い合わせをできるだけ少なくしようとします。 Redisを含むあらゆるシステムにとってそれはいいアドバイスです。しかしながらより単純なデータ構造を扱う前提に立つと、目的を達するために複数回Redisにアクセスする必要が生じることがあります。このようなデータアクセスパターンは最初は不自然に感じられるかもしれません。しかし現実には実際に得られるパフォーマンスに比べると無視できるほどのコストで済むことが多いのです。
Redisを使ったのはほんの少しでしたが、たくさんの話題について触れました。何かわからないこと、例えばクエリとか、があっても心配しないでください。次の章で実際に手を動かすことで、疑問が氷解することでしょう。
この章のポイント:
-
キーはデータ(値)を識別する文字列
-
値は任意のバイト列で、Redisはその中身に一切関知しない
-
Redisは5つのデータ構造を実装している
-
以上のことから、Redisは高速で容易に扱えるが、全ての課題に最適なわけではない。
Redisの5つのデータ構造について見ていきましょう。それぞれのデータ構造について解説し、どんなメソッドが利用可能で、どんな機能やデータの場合にどのデータ構造を利用するのが良いのか見ていきます。
これまでに見てきたRedisの要素はコマンドとキーと値だけでした。データ構造については詳しく触れていません。 set
コマンドを使うときに、Redisはどうやってどのデータ構造を使うか判別するのでしょうか?実は全てのコマンドはデータ構造と結びついているのです。たとえばset
コマンドを使う時、格納される値は文字列です。 hset
コマンドを使うとハッシュとして格納します。 Redisのコマンドは数が少ないので、このようにして区別することで扱いやすくなっています。
Redisのウェブサイト には大変良く出来たリファレンスがあります。同じ事を繰り返す必要はないので、データ構造の目的を理解するのに必要な最も重要なコマンドだけを取り上げます。
実際に物事に取り組んで楽しむことほど大事なことはありません。 flushdb
コマンドでいつでもデータベースを全消去できるので、ためらわずに思う存分試してみてください。
文字列はRedisで一番基本的なデータ構造です。一般にキーと値のペアというと文字列のことを思い浮かべるでしょう。しかし名前に惑わされないでください、値は何だっていいのです。私はそれらをスカラー値と呼ぶのが好きですが、たぶんこれは私だけでしょう。
文字列の使い方については既に見ました。オブジェクトのインスタンスをキーによって保存します。次のような使い方を頻繁にすることでしょう:
set users:leto '{"name": leto, "planet": dune, "likes": ["spice"]}'
加えて、いくつかありふれた操作も行えます。たとえば strlen <key>
とすると、キーが示す値の長さを得られます。getrange <key> <start> <end>
は指定した範囲の値を返します。 append <key> <value>
は既にあるキーに値を追加するか、キーが存在しなければ新しく作成して追加します。実際にやってみて下さい。次のような結果になります:
> strlen users:leto
(integer) 50
> getrange users:leto 31 48
"\"likes\": [\"spice\"]"
> append users:leto " OVER 9000!!"
(integer) 62
さて、あなたはこれを見て「なるほどわかった、しかしこんなのは意味がない」と考えているのではないでしょうか。 JSONから範囲指定で値を取り出したり、値を追加したりすることには意味がありません。ここでは文字列に限ったコマンドを実行する練習をしているだけです。とうぜん文字列を扱う場合にしか意味がありません。
先ほどRedisは値について関知しないといいました。大抵の場合はそれは本当のことですが、幾つかの文字列コマンドはある種の値や構造に特化しています。漠然とした例ですが、append
とかgetrange
とかは効率のいいシリアライゼーションをするのに便利です。より具体的な例としてはincr
incrby
decr
decrby
コマンドがあります。これらは文字列の値を増やしたり減らしたりします。
> incr stats:page:about
(integer) 1
> incr stats:page:about
(integer) 2
> incrby ratings:video:12333 5
(integer) 5
> incrby ratings:video:12333 3
(integer) 8
ご想像のとおり、Redisの文字列は分析に大変役に立ちます。試しに整数値ではない users:leto
をインクリメントしようとしてみてください、エラーになるはずです。
より進んだ例として setbit
getbit
コマンドがあります。 素晴らしい解説 があります。Spoolがこれらのコマンドを使ってどれほど効率的に一日のユニークビジター数を計算しているかの例です。 1億2800万ユーザを一台のラップトップで、50ミリ秒と16MBのメモリで処理しています。
ビットマップの仕組みを理解することは重要ではありません。同様にSpoolがどんなふうに使っているかというのも重要ではありません。しかし Redisの文字列が見た目よりずっと強力だということは理解する必要があります。しかし最も一般的な使われれ方は上に上げたようなオブジェクトを格納するとか、カウンターを作るとか、そういったことです。またキーから値を得るのは大変速いので、文字列はよくキャッシュに使われます。
なぜRedisが単なるキーバリューストアではないのか、その好例がハッシュです。多くの点でハッシュは文字列と似通っています。重要な違いはキーと値の間に追加要素があることです。それはフィールドです。したがって、ハッシュにおける set
get
は次のようになります:
hset users:goku powerlevel 9000
hget users:goku powerlevel
複数のフィールドを一度に設定したり、取得したりすることもできます。全てのフィールドを表示したり、特定のフィールドを削除するには次のようにします:
hmset users:goku race saiyan age 737
hmget users:goku race powerlevel
hgetall users:goku
hkeys users:goku
hdel users:goku age
見てわかるように、ハッシュは単なる文字列より扱いやすくなっています。ユーザ情報をシリアライズされた文字列として保存するより、ハッシュで保存したほうが理解が容易です。利点は値全部を読み書きするのではなく、値の特定の部分を取り出したり更新したり削除したりできることです。
具体的なモデル、例えばユーザ情報とかを使ってハッシュを見ることが理解の鍵です。そしてパフォーマンスの観点から、より細かなコントロールができることは有益です。しかし次の章ではデータ構造化やクエリを行うのにハッシュをどのように利用できるのか見て行きます。個人的にはハッシュが最もその実力を発揮する場面だと思います。
リストはキーに対して値の配列を格納します。リストに対して値を追加し、最初や最後の値を取得し、インデックスを使ってリスト内の値を操作できます。リストは内部に順序を保持しており、効率の良いインデックスベースの操作が可能です。 newusers
リストを使って新しくサイトに登録したユーザを追跡できます:
lpush newusers goku
ltrim newusers 0 49
まず新しいユーザをリストの先頭に追加します。次に最後の50ユーザだけを残して他を削除しています。これが一般的なパターンです。 ltrim
はO(N)操作です。Nは削除する要素の数です。この例の場合、一回の追加の後にトリムします。したがって常に実行時間O(1)がかかることになります。
次はあるキーの値を元に別のキーの値を参照するという初めての例です。もし最新10ユーザの詳細が知りたければ、次の操作の組み合わせで可能です:
ids = redis.lrange('newusers', 0, 10)
redis.mget(*ids.map {|u| "users:#{u}"})
このコードはRubyの例です。以前述べた複数回の問い合わせを行っています。
もちろんリストは他のキーへの参照を保存するのに便利なだけではありません。ログを保存したり、ユーザの行動履歴を保存したりするのにも使えます。ゲームを作っているならユーザアクションのキューに使えるでしょう。
集合は一意な値を格納し、unionのような集合を扱う操作を提供します。集合には順序がありませんが、値に基づいた効率的な操作が可能です。友人リストが典型的な利用例でしょう:
sadd friends:leto ghanima paul chani jessica
sadd friends:duncan paul jessica alia
どれほどの友人がいようとも、実行時間O(1)でユーザXがユーザYの友人であるかどうかを判断できます。
sismember friends:leto jessica
sismember friends:leto vladimir
さらに2人以上の人が共通の友人を持っているかもわかります:
sinter friends:leto friends:duncan
さらにその結果を新しいキーとして保存することもできます:
sinterstore friends:leto_duncan friends:leto friends:duncan
値の重複が特別な意味を持たない場合や、集合特有のintersectionやunionといった操作が必要であれば、集合はタグ付けをしたり追跡をしたり、あるいは交差や結合といった集合操作をおこなうのに最適の方法です。
最後の、そして最も強力なデータ構造はソート済み集合です。ハッシュがフィールドつき文字列だとしたら、ソート済み集合はスコア付きの集合です。スコアによってソートしたり順位付けをしたりできます。友人の順位リストを得たければ次のようにします:
zadd friends:duncan 70 ghanima 95 paul 95 chani 75 jessica 1 vladimir
duncan
に90以上のランクの友人が何人居るか知りたいですか?
zcount friends:duncan 90 100
chani
のランクを知りたいとしたら?
zrevrank friends:duncan chani
zrank
の代わりにzrevrank
を使います。なぜならRedisのデフォルトソートは低い方から高いほうへ並べ替えるからです。この例では高い方から順に並べ変えています。ソート済み集合の最も典型的な利用例はスコアボードでしょう。しかし現実では数値で並べ替えられるもの、スコアで操作できるものはソート済み集合にぴったりでしょう。
以上がRedisの提供する5つのデータ構造の概要です。 Redisのいいところはたくさんありますが、その内の1つは最初に思ったよりもはるかにたくさんのことができるということです。文字列とソート済み集合を使って誰も思いつかなかったようなことができるでしょう。しかし、一般的な利用例を理解しさえすれば、Redisがあらゆる問題に使えることに気がつくでしょう。 Redisが5つのデータ構造とそれらを扱う大量の方法を提供しているからといって、それらを全て使わなければならないとは考えないでください。いくつかのコマンドだけで必要な機能を作るのは珍しいことではありません。
これまでの章で、5つのデータ構造とそれらが解決しうる問題の例について解説しました。次はより複雑で実際に即した話題やデザインパターンを見ていきましょう。
この本の全体を通してビッグオー記法を用いています。ビッグオー記法は O(n) や O(1)のような記述をし、与えられた要素数に対してどのような振る舞いをするかを表します。Redisでは要素数に対するコマンド実行速度を表すのに使われます。
Redisのドキュメントには各コマンドの実行時間がビッグオー記法で書かれています。これを見ることで何が速度に影響するかを知ることができます。幾つか例を見てみましょう。
最も速いのは O(1) で、常に一定の速度で応答が返ってきます。扱う要素数が5であろうが5百万であろうが、同じ速度が得られます。たとえば値が集合に属しているかを判定するsismember
コマンドは O(1) です。sismember
はその速さ故に強力なコマンドです。Redisの多くのコマンドは O(1) です。
対数コマンド O(log(N)) はO(1)の次に速い可能性があります。なぜなら走査する対象が徐々に小さくなっていくからです。このタイプの分割して統治せよアプローチを使うことで大量の要素は幾つかの繰り返しに分解されます。zadd
コマンドはO(log(N))です。Nはソート済み集合内の要素数を表します。
次は線形コマンド O(N) です。RDBでテーブル内の索引なしの列を走査するのがこのO(N)に当たります。ltrim
コマンドはO(N)です。しかしltrim
の場合、Nはリスト内にある要素数を意味しません。Nが意味するのは取り除かれる要素数です。100万個のリストから1つの要素を取り除くのは、1000個のリストから10個の要素を取り除くのより速くなります。(しかしながらほとんどわからないくらいの違いだとは思いますが)
スコア付き集合のスコアの範囲に基づいて要素を削除する zremrangebyscore
コマンドは複雑で、O(log(N)+M)のようになります。2つの要素に影響を受けるということです。Redisのドキュメントを読むとNは集合内の要素の数であり、Mは削除される要素の数だとわかります。言い換えれば、パフォーマンスの観点からは集合内の全要素数よりも、削除される要素の数のほうがより重要な意味を持つということでしょう。
次の章で詳しく取り上げるsort
コマンドはさらに複雑で O(N+M*log(M)) となります。速度の特性からRedisで最も複雑なコマンドの1つだと言えるでしょう。
複雑なものはまだあります。残りの2つは O(N^2) と O(C^N)です。Nが大きくなればなるほど小さなNに比較してどんどんパフォーマンスが悪くなります。Redisにはこのタイプのコマンドはありません。
ビッグオー記法の一番遅いケースについて指摘しておくことは重要でしょう。何かがO(N)であるという時、それは一番最初に見つかるかもしれないし、最後に見つかるかもしれないということです。
同じ値を持つ複数のキーに対して問い合わせをしたいというのはよくあることです。たとえばログインする時メールアドレスからユーザを検索し、ログイン後はIDで検索したい、というような場合です。まずいやり方としてはユーザオブジェクトを2つの文字列に複製するというのがあります:
set users:leto@dune.gov "{id: 9001, email: 'leto@dune.gov', ...}"
set users:9001 "{id: 9001, email: 'leto@dune.gov', ...}"
これはよくありません。なぜならユーザデータの整合性管理はまるで悪夢であり、二倍のメモリを消費するからです。
1つのキーから別のキーへとリンクを張れれば良いのですが、それはできません。将来に渡ってできるようになることもないでしょう。Redisの開発においてはコードとAPIを簡潔かつシンプルに保つことが重要視されています。キーリンクの実装にはあまり意味がありません、Redisはハッシュという解を既に用意しているからです。
ハッシュを使うと重複を避けることができます:
set users:9001 "{id: 9001, email: leto@dune.gov, ...}"
hset users:lookup:email leto@dune.gov 9001
ここでやっているのはフィールドを擬似的な二次インデックスにして、ユーザオブジェクトへの参照を持たせていることです。IDからユーザを検索するにはいつもどおりget
を使います:
get users:9001
メールアドレスからユーザを得るには、hget
のあとにget
を使います(以下はRubyの例です) :
id = redis.hget('users:lookup:email', 'leto@dune.gov')
user = redis.get("users:#{id}")
こういう処理を頻繁にすることになるでしょう。私にとってこれがハッシュの威力を最も感じる場面です。しかし実際に使ってみるまではよくわからないのです。
1つの値から別の値へと参照する例を幾つか見てきました。リストの例、ハッシュを使って検索を容易にする例などです。これらの意味するところは、値同士の参照やインデックスを手動で管理しなければならないということです。正直に言ってちょっとうんざりします。とりわけ参照を手で管理し、更新し、削除しなければならないと考えると。Redisにはこの問題を解決する魔法の杖はありません。
既に見たようにこのタイプの手動インデックスには集合がよく使われます:
sadd friends:leto ghanima paul chani jessica
この集合の各要素はユーザ情報を格納する文字列への参照になっています。chani
が名前を変えたり、アカウントを削除したりしたらどうなるでしょうか、逆向きの関係を保持しておくのが良いでしょう:
sadd friends_of:chani leto paul
メンテナンスにかかる手間は別として、あなたが私と似たような人であれば、この余計なインデックスに費やすCPUリソースとメモリに身が縮む思いがするでしょう。次の節では最初の章で簡単に触れたこういった余分な問い合わせのコストを減らす方法について検討します。
しかしよく考えれば、RDBにも同様のオーバーヘッドがあるのです。インデックスはメモリを消費し、走査したり検索したり対応するデータを見つけ出したりするのにつかわれます。こういったオーバーヘッドは上手く抽象化されています。また同時に処理を効率的にするために非常に最適化されています。
繰り返しますが、Redisにおいて参照を手動で管理しなければならないのは残念なことです。しかし先ほど述べたようなパフォーマンスやメモリに関する不安は検証しておくべきです。そして、それが問題ないことにすぐ気がつくことと思います。
サーバへ頻繁に問い合わせをするのはRedisでは普通のことだと既に述べました。頻繁におこなうことなので、どのような機能を利用できるか詳しく見ておくことにしましょう。
まず、ほとんどのコマンドは1つかそれ以上の引数か、複数の引数を取るサブコマンドを引数に取ります。さきほど見たmget
は複数のキーを引数に取り、複数の値を返します:
ids = redis.lrange('newusers', 0, 10)
redis.mget(*ids.map {|u| "users:#{u}"})
sadd
コマンドは1つ以上の要素を集合に追加します:
sadd friends:vladimir piter
sadd friends:paul jessica leto "leto II" chani
Redisはパイプライン化をサポートしています。普通はクライアントがRedisにリクエストを送った後、レスポンスが返ってきてから次のリクエストを送ります。パイプライン化を使うとレスポンスを待つこと無く複数のリクエストを送ることができます。これによってネットワークオーバーヘッドを減らし、多大なパフォーマンス向上を得られます。
Redisはコマンドをメモリにキューとして保存するので、コマンドをまとめてバッチにするのはいいアイデアです。バッチサイズをどのくらいにするかは、使うコマンドの種類によります。さらにいうなら、パラメータの大きさにもよります。しかし、例えば50文字のキーに対してコマンドを実行するなら、おそらく千か万の単位でバッチにできるでしょう。
パイプラインでコマンドを使う方法はライブラリによって異なります。Rubyでは以下のようにしてpipelined
メソッドにブロックを渡します:
redis.pipelined do
9001.times do
redis.incr('powerlevel')
end
end
ご推察の通り、パイプライン化はバッチインポートを劇的に高速化します!
Redisの全てのコマンドはアトミックです。複数の処理をおこなうものも含めて全てそうです。加えてRedisは複数のコマンドを使うのにトランザクションをサポートしています。
ご存じないかもしれませんがRedisはシングルスレッドで動きます。それゆえに全てのコマンドがアトミックであることが保証されます。一度に実行されるコマンドは常に1つだけです。スケールアウトについては後ほど触れます。いくつかのコマンドが複数の操作を行うと考えると、これはとても便利なことです:
incr
は set
の後にget
を行うのと同じです
getset
は新しい値を設定し、古い値を返します
setnx
はまずキーが存在するかどうか確認し、キーが存在しない場合のみ値を設定します
これらのコマンドは便利ですが、複数のコマンドをひとまとめにしてアトミックに実行するのは必ず必要になります。まずはじめにmulti
コマンドを実行し、続いて複数の操作をトランザクションとして実行し、最後にexec
コマンドを実行して実際の処理を行うか、discard
コマンドで操作を破棄します。Redisのトランザクションで保証されるのはどんなことでしょうか?
-
コマンドは順番通りに実行されます
-
コマンドはアトミックな操作として実行されます(コマンドの実行の最中に他のコマンドが割り込んでくることはありません)
-
トランザクション中のコマンドは、全てが実行されるか、全てが実行されないかのどちらかです
コマンドラインでこれらを確認できます、またそうすることを強くすすめます。加えてパイプライン化とトランザクションを組み合わせてはいけない理由は何もありません。
multi
hincrby groups:1percent balance -9000000000
hincrby groups:99percent balance 9000000000
exec
最後に、Redisは一つまたは複数のキーを監視し、キーが変更された場合に条件に応じてトランザクション操作を行うことができます。これはトランザクションの中で値を取得して、その値に基づいて処理を行う必要がある場合に使われます。先ほどのコード例では独自のincr
コマンドを実装することはできませんでした。なぜならexec
が呼ばれたタイミングで全てのコマンドが一緒に実行されるからです。つまり以下のようなことはできないわけです:
redis.multi()
current = redis.get('powerlevel')
redis.set('powerlevel', current + 1)
redis.exec()
Redisのトランザクションではこれは上手く行きません。しかしpowerlevel
にwatch
を追加すれば、次のようにすることができます:
redis.watch('powerlevel')
current = redis.get('powerlevel')
redis.multi()
redis.set('powerlevel', current + 1)
redis.exec()
もし他のクライアントがwatch
コマンドが実行された後にpowerlevel
を変更したとしたら、トランザクションは失敗します。そうでなければ値の変更が実行されます。このコードが成功するまでループの中で実行することができます。
次の章では特定のデータ構造と関係のないコマンドについて説明します。いくつかは管理用であったりデバッグ用であったりします。しかしとりわけ注目を要すると思うのはkeys
コマンドです。このコマンドは引数にパターンを取りそのパターンにマッチするKeyを全て返します。このコマンドは様々な用途に便利に使えそうです。しかし実運用コードでは使うべきではありません。なぜ?それは一致するKeyを探すために全てのキーを走査するからです。端的に言えば遅いということです。
実際keys
コマンドをどのように使ったり試したりするのでしょうか?たとえばバグトラッキングサービスを作っているとしましょう。各アカウントはid
を持っているので、各バグ情報を文字列としてbug:account_id:bug_id
のようなキーに保存することにしたとします。あるアカウントに関するバグ情報すべてを探すには次のようにしたい誘惑に駆られるでしょう(私がそうであったように!):
keys bug:1233:*
ハッシュを使うほうが良い解決策です。セカンダリインデックスとしてハッシュを使ったように、データの構成にもハッシュを使えます:
hset bugs:1233 1 "{id:1, account: 1233, subject: '...'}"
hset bugs:1233 2 "{id:2, account: 1233, subject: '...'}"
あるアカウントのバグIDを全て得るには単に hkeys bugs:1233
とします。1つのバグを削除するには hdel bugs:1233 2
とします。アカウントを削除するには del bugs:1233
とします。
この章と前の章を併せて、Redisを実際にどう活用するか多少なりとも理解してもらえたことと思います。実際にはもっとたくさんのパターンがありますが、基本的データ構造を理解する鍵は、どのようにそれらを使って最初に思いついた以上のことができるかを知ることです。
5つのデータ構造がRedisの基本である一方、データ構造に関係ないコマンドもあります。これまでに見てきた info
select
flushdb
multi
exec
discard
watch
keys
などがそうです。この章ではその他の重要なコマンドを紹介します。
Redisではキーに有効期限を設定することができます。Unixタイムスタンプの形式で指定するか、秒数を指定できます。これはキーに基づいたコマンドで、キーに紐付いたデータ構造には関係ありません。
expire pages:about 30
expireat pages:about 1356933600
最初のコマンドはキーとその値を30秒後に削除します。二番目は2012年12月31日の12時に削除します。
この機能はRedisをキャッシュの格納先として使うのに理想的です。期限切れまでどれだけかを知るには ttl
コマンドを使います。有効期限を無くしたい場合は persist
コマンドを使います:
ttl pages:about
persist pages:about
最後に、特別な文字列操作コマンドがあります。 setex
は1つのコマンドで、文字列をセットすると同時に有効期限を設定することができます:
setex pages:about 30 '<h1>about us</h1>....'
Redisのリストは blpop
と brpop
コマンドを持ち、これらのコマンドは最初(あるいは最後)の要素を返してリストから削除するか、アクセスできるまでブロックするかします。この2つのコマンドは簡単なキューを作るのに使えます。
さらにRedisではメッセージの発行(パブリッシュ)と、チャネルへの登録(サブスクリプション)をファーストクラスサポートしています。二つ目の redis-cli
ウィンドウを開いて自分で試してみることができます。一つ目のウィンドウでチャネルに登録し、そのチャネルを warnings
と呼ぶことにします。:
subscribe warnings
登録の情報が返ってきます。次に別のウィンドウで warnings
チャネルにメッセージを発行します:
publish warnings "it's over 9000!"
最初のウィンドウで warnings
チャネルに発行したメッセージを受け取っているはずです。
subscribe channel1 channel2
として複数のチャネルに登録することができます。あるいはパターンを使って (psubscribe warnings:*
) とすることもできます。 unsubscribe
と punsubscribe
コマンドを使うと登録を解除できます。
最後に、publish
コマンドが1を返したことに注意してください。これはメッセージを受け取ったクライアントの数を表しています。
monitor
コマンドはRedisの状態を表示します。アプリケーションがRedisとどんなやり取りをしているのかを見ることができるので、デバッグに便利です。redis-cliを2つのウィンドウで立ち上げて、一方でmonitor
コマンドを打ち込み、他方でget
やset
などのコマンドを実行してみてください。それらのコマンドがパラメータ付きで最初のウィンドウに表示されるはずです。
実運用環境でmonitorコマンドを使うのは慎重にすべきです。monitorはデバッグと開発用です。それ以外には特に言うべきことはありません。とても便利なツールです。
monitor
とあわせて、Redisにはプロファイリングに使えるslowlog
コマンドがあります。このコマンドは指定された マイクロ 秒以上かかっている全てのコマンドを記録します。次の章ではRedisの設定を簡単に説明しますが、ひとまず全てのコマンドを記録するのに次のように設定できます:
config set slowlog-log-slower-than 0
次に幾つかコマンドを実行しましょう。そして全てのログか直近のログを取得します:
slowlog get
slowlog get 10
記録されたログの数を数えるにはslowlog len
とします。
実行した各コマンドに対して4つのパラメータが表示されます:
-
自動採番されたID
-
コマンド実行時のUnixタイムスタンプ
-
コマンド実行にかかった時間、マイクロ秒単位
-
コマンドとそのパラメータ
slow logはメモリ上に保存されます。そのため低閾値であっても実運用環境での実行も問題ありません。デフォルトでは最新の1024個のコマンドをログに保持します。
Redisのもっとも強力なコマンドのうちの1つはsort
です。リストや集合、ソート済み集合にある値をソートすることができます。ソート済み集合の場合はメンバーではなくスコアでソートされます。ソートの一番単純な形は次のようなものです:
rpush users:leto:guesses 5 9 10 2 4 10 19 2
sort users:leto:guesses
値が小さなものから大きなものへと並べ替えられて返されます。さらに高度な例は次のようなものです:
sadd friends:ghanima leto paul chani jessica alia duncan
sort friends:ghanima limit 0 3 desc alpha
この例では並べ替えしたデータをlimit
でページ分けし、desc
で並び替えを降順にし、alpha
でアルファベット順に並べ替えしています。
sort
の真の力は参照オブジェクトに基づいて並べ替えができることです。先ほどリストや集合、ソート済み集合が他のRedisオブジェクトを参照するのに使われる例を紹介しました。sort
コマンドはそれらを逆参照して得た値で並べ替えをすることができます。例としてユーザが問題を監視できるバグトラッキングシステムがあったとしましょう。監視する問題を設定するには集合を使うでしょう:
sadd watch:leto 12339 1382 338 9338
これらのIDに基づいてソートするのも完全に理にかなっているのですが、問題の深刻度によってもソートしたいとします。そのためにはRedisにソート用のパターンを指定してやります。まずは結果をわかり易くするためにデータを幾つか追加しましょう:
set severity:12339 3
set severity:1382 2
set severity:338 5
set severity:9338 4
バグを深刻度の高いものから低いものへソートするには、次のようにします:
sort watch:leto by severity:* desc
Redisはby以降で指定された*
の部分をリスト/集合/ソート済み集合の値で置き換えます。置き換え後に得られたキーが実際の並べかえに使う値を得るのに参照されます。
Redisには何百万ものキーを保存できるとはいえ、先程の例はキーの管理という面でちょっと面倒かと思います。ありがたいことに sort
コマンドはハッシュを扱うことができます。キーをたくさん作る代わりにハッシュを利用できます:
hset bug:12339 severity 3
hset bug:12339 priority 1
hset bug:12339 details "{id: 12339, ....}"
hset bug:1382 severity 2
hset bug:1382 priority 2
hset bug:1382 details "{id: 1382, ....}"
hset bug:338 severity 5
hset bug:338 priority 3
hset bug:338 details "{id: 338, ....}"
hset bug:9338 severity 4
hset bug:9338 priority 2
hset bug:9338 details "{id: 9338, ....}"
より整理されただけでなく、severity
やpriority
でのソートもでき、さらにどのフィールドを取得するかsort
コマンドに指示することもできます:
sort watch:leto by bug:*->priority get bug:*->details
先ほどと同じように*
が置き換えられますが、同時に->
がハッシュの参照するフィールドを指定します。またget
パラメータをつかうことで、バグの詳細を取得しています。
巨大な集合に対してはsort
は遅くなることがあります。sort
結果は保存することができます:
sort watch:leto by bug:*->priority get bug:*->details store watch_by_priority:leto
store
を期限切れ(expiration)コマンドと一緒に使うのはよい組み合わせでしょう。
前の章で、keys
コマンドは便利だけど、実運用環境で鼻使うべきではないということを学びました。 Redis 2.8ではscan
コマンドが新たに実装され、keys
と同様の機能を提供する一方で、実運用環境での利用に適したものになっています。scan
とkeys
にはたくさんの違いがありますが、率直に言って多くの 違い は ばかばかしさ のように思われるでしょう。しかしこれは実運用での利用に耐えるだけのコマンドが支払うべき対価なのです。
その違いのまず第一のものは、一回のscan
コマンドの呼び出しは、必ずしもマッチするすべての結果を返さないということです。これがページ処理なら別に不思議でも何でもありませんが、scan
の返す結果の数は一定ではなく、その数を制御することはできません。count
コマンドで希望する結果の数(デフォルトは10)を指定することはできますが、その数より多くなったり少なくなったりするのは、全くあり得ることです。
limit
とoffset
をつかってページ処理をする代わりに、scan
ではcursor
を使います。scan
が最初に実行される時のカーソルは0
です。以下の例ではパターンマッチとcountを指定してscan
を実行しています。
scan 0 match bugs:* count 20
このコマンドの返答の一部として、scan
は次のカーソルを返します。あるいはscanは0
を返し、それ以上の結果がないことを知らせます。次のカーソルの値は結果の数やその他の有用な情報とは関係ないことに注意して下さい。
よくある処理の流れは以下のようなものです:
scan 0 match bugs:* count 2
> 1) "3"
> 2) 1) "bugs:125"
scan 3 match bugs:* count 2
> 1) "0"
> 2) 1) "bugs:124"
> 2) "bugs:123"
最初の呼び出しで、次のカーソル(3)と一つの結果が返ってきます。このカーソルを使って次の呼び出しをおこなうと、これ以上のデータがないことを示すカーソル(0)と残り二件の結果が返ってきます。これが 典型的な scanの使い方です。count
はただの希望なので、scan
が空の結果と0ではない次のcursor
を返すことはありえます。言い換えれば、空の結果は必ずしも残りのデータがないということではありません。0カーソルだけがscanが終了したことを意味します。
これには良い面もあります、scan
はRedisにとっては完全にステートレスだということです。つまりカーソルを閉じたり、結果をすべて読んだりする必要はないということです。Redisが次のカーソルを返したとしても、読み取りを途中で中断することができます。
あと2つ覚えておいてほしいことがあります。まず1つ目は、scan
は同じキーを複数回返すことがあるということです。キーの重複はあなた自身で処理する必要があります(集合にキーを入れておくとかして)。2つ目は、scan
はカーソルが走査中に存在した値のみを返すということです。もしscanの実行中に新しい値が追加されたり、今ある値が削除されたりしたら、それらの値は結果に含まれたり、含まれなかったりします。これはscan
のステートレス性に由来します。データベースの状態をスナップショットに取るのではなく、scanはRedisのメモリスペースを変更のあるなしにかかわらず走査するだけです。
scan
に加えて、hscan
sscan
zscan
というコマンドも追加されました。これらはハッシュ/集合/ソート済み集合の走査をするのに使います。なぜこれらのコマンドが必要かというと、keys
と同じように、ハッシュではhgetall
、集合ではsmembers
が他のすべてのコマンドをブロックするためです。非常に大きなハッシュや集合を走査する必要があるなら、これらのコマンドの利用を検討して下さい。ソート済み集合ではzrangebyscore
とzrangebyrank
を使ってページ処理が可能なので、zscan
コマンドはあまり使いみちがないように思われすが、巨大なソート済み集合をスキャンするならzscan
にも使い所はあります。
この章ではデータ構造に関係のないコマンドに焦点を当てました。他の全てと同じくこれらのコマンドを使う場面は様々です。expiration,publication/subscription,sortを使わないアプリケーションや機能を作ることも稀ではありません。しかしこういったコマンドがあるということを知っておくのはいいことです。また、ここではいくつかのコマンドに触れたに過ぎません。この本の内容を消化したら完全なリストに目を通してみるとよいでしょう。
Redis2.6には組み込みのLuaインタプリタが含まれています。開発者はこれを用いてより高度なクエリを書くことができます。この機能をストアド・プロシージャのようなものだと見るのはあながち間違いではありません。
この機能を使いこなすのに最大の難関になるのはLuaを学ぶ部分です。ありがたいことにLuaは一般的なプログラム言語とよくにており、ドキュメントが充実しており、活発なコミュニティに恵まれており、Redisのスクリプトを書く以上のことができます。このチャプターではLuaについて詳しい説明はしませんが、いくつかの例を見ることで簡単な紹介になればとおもいます。
Luaを使ったスクリプティングについて見る前に、この機能がどう便利なのかと疑問に思うことでしょう。多くの開発者はストアド・プロシージャが好きではありません。Redisの機能はストアド・プロシージャとは違うのでしょうか?簡単な答えはノーです。RedisのLuaスクリプティングはテストが難しいコードになったり、ビジネスロジックがデータアクセスと強く結びついたり、ロジックの重複を生んだりすることがあります。
しかし適切に使えば、コードを簡潔にし、パフォーマンスを上げることができます。これらの利点は複数のコマンドをグループ化し、シンプルなロジックをカスタムした関数にまとめるる事によってもたらされます。Luaのスクリプトは中断されることなく実行されるので、アトミックコマンドを自分で簡単に作ることができ(やっかいなwatch
コマンドをなくすこと)、その結果、コードは簡潔になります。中間コマンドの結果を返す必要がなくなるのでパフォーマンスが良くなります。最終的な問い合わせ結果はスクリプト内で計算されます。
次からのセクションで見る例でこれらの点についてより明確になるでしょう。
eval
コマンドは操作対象のキーと、オプション引数、そしてLuaスクリプトを文字列として受け取ります。簡単な例を見てましょう。(実行はRubyから行います。コマンドラインから複数行のRedisコマンドを実行するのは楽しくありません)
script = <<-eos
local friend_names = redis.call('smembers', KEYS[1])
local friends = {}
for i = 1, #friend_names do
local friend_key = 'user:' .. friend_names[i]
local gender = redis.call('hget', friend_key, 'gender')
if gender == ARGV[1] then
table.insert(friends, redis.call('hget', friend_key, 'details'))
end
end
return friends
eos
Redis.new.eval(script, ['friends:leto'], ['m'])
上記の例はLetoの男友達全員について詳細を取得します。スクリプト内でRedisコマンドを実行するにはredis.call("command", ARG1, ARG2, ...)
メソッドを使います。
Luaを使うのが初めてなら、一行ずつ念入りに読んで下さい。{}
が空のtable
(配列かディクショナリのように使えます)を作ることを知っておくと便利でしょう。#TABLE
はTABLE内の要素の数を得ます。..
は文字列の連結に使います。
eval
は実は4つの引数を取ります。2つ目の引数はキーの数ですが、Rubyドライバが自動で値を設定してくれます。なぜキーの数が必要なのかは、CLIで上記コマンドを実行してみるとよくわかります。
eval "....." "friends:leto" "m"
vs
eval "....." 1 "friends:leto" "m"
最初の間違った例では、どれがキーでどれがオプションの引数か、Redisは知るすべがありません。2つ目の例ではこのような曖昧さはなくなります。
これは次の疑問につながります:なぜすべてのキーを明示的に指定しなければならないのでしょうか?Redisのコマンドは実行時にどのキーが操作対象になるのかを知ります。これによって、将来的にRedis Clusterのような分散処理機能が実現可能になります。上記の例ではキーを動的に読み出すことに気がついたかもしれません。hget
はLetoの男友達全員について実行されます。これが予めキーをすべて指定するのは厳格なルールと言うよりむしろ提案である理由です。上記の例はシングルインスタンス化レプリケーションで実行できますが、リリース予定のRedisClusterでは実行できません。
eval
で実行されるスクリプトはRedisによってキャッシュされますが、毎回スクリプトを送信するのは場合によっては最適とは言えません。そのかわり、スクリプトをRedisに登録しておいて、その登録名で実行することができます。script load
コマンドを使うとこの機能がつかえます。このコマンドはスクリプトのSHA1を返します。
redis = Redis.new
script_key = redis.script(:load, "THE_SCRIPT")
スクリプトをロードしたら、evalsha
で実行できます。
redis.evalsha(script_key, ['friends:leto'], ['m'])
script kill
script flush
script exists
の3つもLuaスクリプトの管理に使えるコマンドです。それぞれ実行中のスクリプトを終了させる、キャッシュからすべてのスクリプトを削除する、スクリプトがすでにキャッシュにあるかを確認する、コマンドです。
RedisのLuaには便利なライブラリが付属しています。table.lib
とstring.lib
、そしてmath.lib
は大変便利です。私にとってはcjson.lib
がとくに便利なライブラリです。まず、複数の引数をスクリプトに渡す際には、JSONにして渡すとわかりやすくなることがあります:
redis.evalsha ".....", [KEY1], [JSON.fast_generate({gender: 'm', ghola: true})]
このJSONをLuaスクリプト内でデコードします:
local arguments = cjson.decode(ARGV[1])
もちろんJSONライブラリはRedisに保存された値をデコードするのにも使えます。上記の例は、次のように書くこともできます:
local friend_names = redis.call('smembers', KEYS[1])
local friends = {}
for i = 1, #friend_names do
local friend_raw = redis.call('get', 'user:' .. friend_names[i])
local friend_parsed = cjson.decode(friend_raw)
if friend_parsed.gender == ARGV[1] then
table.insert(friends, friend_raw)
end
end
return friends
性別をハッシュから読み出す代わりに、友人データから取り出すことができます。(この方法はもとの方法に比べてかなり遅くなります。個人的にはもとの方法が好みですが、こういうやり方もあるということをお見せしました)
Redisはシングルスレッドなので、Luaスクリプトの実行が他のRedisコマンドによって割り込みをうけることはありません。これにより明らかな利点の一つは、TTL付きのキーが処理の途中で消えたりしないということです。スクリプトの実行開始時に存在するキーは、あなたがスクリプト内で消さない限り、実行中のどの時点でも存在します。
次の章ではRedisの管理作業と設定についてより詳しく説明します。ひとまずこの章ではlua-time-limit
がスクリプトが強制終了されるまでの最大実行可能時間を設定することを知っておけば十分です。デフォルト値は長めの5秒です。短くすることを検討して下さい。
この章ではRedisのLuaスクリプティングの機能を紹介しました。他の機能と同様、この機能も使いすぎることがありますが、自分用のカスタムコマンドを作ために程々に使うなら、シンプルで高速なコードを書くことができるでしょう。LuaスクリプティングはRedisの他の機能と同じように、初めのうち少し使ってみると、そのうち毎日のようにこの機能を使うようになっているでしょう。
最後の章はRedisを運用するための管理作業についてです。この僅かな解説では到底Redis管理のための完全な手引きにはなりえませんが、Redisを使い始めるにあたって多くの人がぶつかるであろう基本的な疑問に可能な限り答えたいと思います。
Redisを初めて起動したとき、redis.conf
ファイルがないという警告が表示されました。このファイルはRedisの様々な設定をするのに使います。詳しいコメント付きの redis.conf
ファイルがRedisの各リリースに含まれています。サンプルにはデフォルトの設定が書いてあるので、設定の意味とデフォルト値を知るのに便利です。このファイルは https://github.com/antirez/redis/raw/2.4.6/redis.conf にあります。
ファイル内に詳細なコメントがあるので、ここでは設定については触れません。
redis.conf
ファイルで設定をするのに加えて、config set
コマンドで個々の値を設定できます。実は先程使ったslowlog-log-slower-than
を0にしたのはこれでした。
config get
というコマンドもあって、設定値を表示します。このコマンドはパターンマッチをサポートしており、例えばログ関連の設定を全部表示したいなら:
config get *log*
とすることができます。
Redisがパスワードを要求するよう設定できます。requirepass
を設定することでこの機能が有効になります。requirepass
がある値(これがパスワードになります)に設定されると、クライアントはauth password
コマンドを発行して認証を行う必要があります。
一旦クライアントが認証を済ませると、全てのキーをデータベースから削除するflashall
コマンドもふくんだ、あらゆるコマンドが実行可能になります。次に示すようにコマンド名を変更して難読化することでセキュリティを高めることができます:
rename-command CONFIG 5ec4db169f9d4dddacbfb0c26ea7e5ef
rename-command FLUSHALL 1041285018a942a4922cbf76623b741e
新しい名前を空文字列にするとコマンドは使えなくなります。
Redisを使い始めると、「いくつのキーを保存できるんだろう?」と考えることでしょう。あるいはハッシュにはどれだけのフィールドを格納できるのか、リストや集合はどれだけの要素を持てるのか、といったことも気になるかもしれません。現実的には各インスタンスにつきこれらの限界は何億という数値です。
Redisはレプリケーションをサポートしています。マスターのRedisインスタンスにデータを書き込むと、一つかそれ以上のスレーブインスタンスがマスターの更新に追従するという機能です。スレーブの設定は設定ファイルのslaveof
値か、slaveof
コマンドで行えます。この設定なしで実行されるインスタンスはマスターになりえます。
レプリケーションはデータを複数のサーバにコピーすることでデータを保護するのに役立ちます。データの読み出しをスレーブに任せることでパフォーマンスの改善にも使えます。これはわずかに古くなったデータを返す可能性がありますが、ほとんどのアプリケーションにとっては許容できる代償でしょう。
残念なことに、Redisのレプリケーションは自動フェイルオーバーに対応していません。マスターが落ちたら、スレーブを手動でマスターに昇格させなければなりません。Redisで高可用性を実現しようとするなら、広く使われている生存監視を使った高可用性ツールやスレーブを自動で切り替えるスクリプトを利用する手間が必要です。
Redisのバックアップはスナップショットファイルをどこでも好きなところにコピーするだけです。デフォルトではRedisはスナップショットをdump.rdb
という名前のファイルに保存します。いつの時点でもこのファイルをscp
ftp
cp
することできます。
マスターではスナップショットとアペンドオンリーファイル(aof)の両方を無効にして、スレーブにこれらの役割を任せてしまうのは珍しいことではありません。これによって全体のシステムのパフォーマンスを損なうこと無く、マスターの負荷が減り、スレーブで高頻度で保存を行えます。
レプリケーションはサイトの負荷増大に伴って使える最初の手段です。コマンドの中には実行に高いコストのかかるものがあり(例えばsort
など)、こういったコマンドの実行をスレーブに任せることで全体の応答性を高く保つことができます。
レプリケーションのさらに先、Redisで本当のスケールアウトをするには、キーを複数のインスタンス(これらは一つのサーバで実行され得るでしょう。思い出してください、Redisはシングルスレッドなのです)に分散配置するということになります。現在のところ、この機能は自分で何とかする必要があります(多くのRedisドライバは一貫性を保ったハッシュアルゴリズムを提供していますが)。データの水平分散はこの本で扱う内容をこえています。またしばらくの間はそんな心配も必要ないでしょう。しかしこの話題はどんな解決法を採るにしろ、念頭に置いておかなければならないことです。
ここで良いお知らせです。Redis Clusterが現在開発中で、この機能はデータの水平分散を可能にするだけでなく、リバランス機能や自動フェイルオーバー機能も含んでいます。
高可用性とスケールアウトは手間暇を惜しまなければ現状でも十分に実現可能です。将来的にはRedis Clusterがもっと話を簡単にしてくれるでしょう。
既にRedisを利用しているサイトやプロジェクトの数を考えると、Redisを実戦投入できるということには疑いの余地はありません。しかしセキュリティや可用性のツールについてはまだ未成熟です。期待の高まるRedis Clusterが管理業務の課題を解決してくれるはずです。
様々な点において、Redisはデータを扱う方法の単純化を示しています。他のやり方にあるような複雑さや抽象化は、そこにはありません。多くの場合において、これがRedisをまずい選択にします。その他の場合は、Redisはあつらえたようにぴったりだと感じられるでしょう。
結局は私がこの本の最初に述べたことに戻ってくるのです。Redisは簡単に学べます。新技術は数あれど、学ぶに値するかどうかを知ることすら難しいことがあります。Redisの単純さによってもたらされる真の利点を考えると、あなたとあなたのチームにとって、Redisを学ぶのは良い投資だと私は心から信じています。