Redis が速い理由 — メモリ、シングルスレッド、データ構造
API 診断の記事でキャッシュの保存先として Redis に触れました。ところで Redis はなぜ速いのでしょうか?「メモリだから」は半分の答えです。メモリベースでも Redis より遅いシステムは多くあります。この記事は Redis の速度を作る三本の柱(メモリ、シングルスレッド、データ構造)と、その設計が要求する代償まで整理します。原理が分かれば、「Redis が遅くなった」という状況の診断も一緒に手に入ります。
柱 ① メモリ — 経路にディスクがありません #
一般のデータベースは耐久性のために書き込みをディスクへ書き出し、読み取りもキャッシュになければディスクへ行きます。SSD の記事で見たとおり、この経路はマイクロ秒からミリ秒の単位です。Redis はすべてのデータをメモリに置くので、リクエスト処理の経路にディスクがそもそもありません。メモリアクセスはナノ秒からマイクロ秒の単位、2〜3 桁違います。
ディスクが完全に消えたわけではありません。永続化(スナップショット、AOF)は必要ですが、応答を待たせない経路の外の仕事へ押し出しました。スナップショットが fork と copy-on-write でサービスを止めずに回る構造は、fork の記事で扱いました。「速さの半分は、何を経路の外へ出すかの設計」というのが最初の柱の教訓です。
柱 ② シングルスレッド — ロックがない代わりに列が一つ #
コマンドを処理する Redis のコアは、スレッド一つのイベントループです。数万のクライアントのリクエストを一つの列に並べて順に処理しますが、それぞれがマイクロ秒で終わるので、毎秒数十万件をさばきます。直感に反するこの選択の根拠は、プロセスとスレッドの記事で見た共有メモリのコストです。
- ロックが要りません。 同時にデータに触れるスレッドがないので、ロックの競合も、デッドロックも、ロックを握って待つ時間もありません。マルチスレッドのシステムがコアを増やしながらロックで失う時間を、Redis はそもそも払いません。
- すべてのコマンドがアトミックです。 INCR のようなコマンドが競合状態の心配なしにそのままカウンターになる理由、分散ロックの実装に Redis が好んで使われる理由が、この直列性です。
- CPU はボトルネックではありませんでした。 Redis の仕事は大半がメモリの読み書きで、一つのコアでもネットワークが先に飽和します。実際、最近のバージョンがマルチスレッドを導入した場所も、コマンドの実行ではなくネットワーク I/O(読み書きバッファの処理)です。コアが余るならインスタンスを複数立てるのが Redis 式のマルチコア活用です。
代償も明確です。列が一つなので、遅いコマンド一つが全体を止めます。 100 万キーをなめる KEYS *、巨大なコレクションの全件参照、大型キーの削除が 100ms を使えば、その後ろのすべてのリクエストが 100ms を一緒に待ちます。「Redis が急に遅くなった」の常連の原因で、処方は KEYS の代わりに SCAN、大きい削除は UNLINK(バックグラウンドの削除)、スローログ(SLOWLOG)で犯人のコマンドを探すことです。
柱 ③ データ構造 — 変換のコストがありません #
リレーショナル DB に「最近の一覧」を保存するとテーブル・インデックスへ変換し、参照のときソート・パースでまた変換します。Redis はリスト、ハッシュ、セット、ソート済みセット(sorted set)のようなデータ構造をその形のままメモリに持っていて、用途に合った演算がすでに最適化された形で用意されています。
- ランキングボードはソート済みセットの「スコアでソートを維持 + 順位の参照」がそれだけで答えになります。SQL なら ORDER BY が毎回やる仕事を、データ構造が常に維持しているわけです。
- カウンターは INCR 一つ、最近の N 件の一覧はリストの LPUSH + LTRIM、重複の除去はセットが受け持ちます。
同じ「速い保存先」でも何が速いかはデータ構造の選択に掛かっていて、Redis をうまく使うとは事実上、データ構造をうまく選ぶという意味です。逆にすべてを JSON の文字列で入れて毎回丸ごと読んでパースするなら、三本目の柱を捨てて使っているのです。
それでも遅くなるケース — 診断リスト #
原理を裏返すと診断リストが出てきます。Redis が遅いなら、次を順に見ます。
- 遅いコマンド —
SLOWLOG GETで確認します。KEYS、大型コレクションの全件参照、大型キーの削除が常連です。 - メモリ超過 — maxmemory に達すると退避(eviction)の作業が乗り、スワップへ押し出されればメモリのシステムがディスクのシステムになります(メモリの記事のスワップの話がそのまま適用されます)。退避ポリシーとメモリの推移を見ます。
- 永続化のコスト — スナップショットの fork の瞬間の遅延、AOF の fsync 設定(
alwaysはディスクが経路の中に入ってきます)を確認します。 - ネットワークの往復 — コマンド一つはマイクロ秒でも、往復はミリ秒です。ループの中で GET を千回呼ぶコードは往復の掛け算の問題で、パイプラインや MGET でまとめるのが処方です。
まとめ #
- Redis の速度は、メモリ(経路にディスクなし)、シングルスレッド(ロックなし + コマンドのアトミック性)、データ構造(変換コストなし)の合作です。
- シングルスレッドの代償は「遅いコマンド一つが全体を止める」です。KEYS の代わりに SCAN、大きい削除は UNLINK、診断は SLOWLOG です。
- データ構造の選択がすなわち性能です。ランキングはソート済みセット、カウンターは INCR のように、用途に合う構造を選んではじめて三本目の柱が生きます。
- 遅くなったら、遅いコマンド → メモリ・退避 → 永続化の設定 → 往復の回数の順に見ます。
- アプリケーション側では、往復をまとめること(パイプライン、MGET)がサーバーのチューニングより先です。