RedisのMGETで複数キーをまとめて取得するメリットと使い方


RedisのMGETで複数キーをまとめて取得するメリットと使い方

1. はじめに:なぜ複数キーをまとめて取得する必要があるのか

現代のウェブアプリケーションやサービスにおいて、データストアは不可欠な要素です。その中でも、超高速なキー・バリュー型インメモリデータストアとして広く利用されているのがRedisです。Redisはその単純なデータ構造と高速なアクセス性能により、キャッシュ、セッションストア、メッセージキューなど、様々な用途で活用されています。

Redisは基本的に、キーを指定して単一の値を操作するコマンドが豊富に用意されています。例えば、GETコマンドは指定したキーの値を取得し、SETコマンドは指定したキーに値を設定します。これらの単一操作は非常に高速ですが、アプリケーションが必要とする情報が複数のキーに分散して保存されている場合、問題が発生することがあります。

例えば、ユーザーのプロフィール情報をRedisに保存しているとしましょう。ユーザー名、メールアドレス、最終ログイン日時などがそれぞれ異なるキー(例: user:123:name, user:123:email, user:123:last_login)に保存されている場合、これらの情報をすべて取得するためには、それぞれのキーに対して個別にGETコマンドを発行する必要があります。もし10個の情報が必要であれば、10回のGETコマンドを実行することになります。

一見すると、それぞれのGETコマンドは非常に高速に実行されるため問題ないように思えます。しかし、クライアント(アプリケーションサーバーなど)からRedisサーバーへコマンドを送信し、結果を受け取るまでには、ネットワークを介した通信が必要です。この通信には、データの転送時間だけでなく、ネットワークの遅延(レイテンシ)が伴います。コマンドを10回発行するということは、10回のネットワーク往復(ラウンドトリップ)が発生することを意味します。

たとえRedisサーバーがコマンド自体を1ミリ秒未満で処理できたとしても、ネットワークのレイテンシが片道1ミリ秒あるとすれば、1回の往復で2ミリ秒かかります。10回の往復では合計20ミリ秒の遅延が発生することになります。これは、サーバー側で処理している時間よりも、ネットワーク待ちの時間の方がはるかに長くなってしまう典型的なケースです。このような問題は、特に複数のデータが必要な場合に、パフォーマンス上の大きなボトルネックとなります。

この「N+1問題」(N個のデータを取得するためにN回のクエリが必要になる問題)をデータストアとの通信において回避するために、多くのデータストアには複数のデータをまとめて操作するための機能が用意されています。Redisにおいても、複数のキーの値をまとめて取得するためのコマンドが存在します。それがMGET (Multi GET)コマンドです。

この記事では、RedisのMGETコマンドに焦点を当て、その基本的な使い方から、複数キーをまとめて取得することによって得られる最大のメリット、詳細な挙動、パフォーマンスに関する考慮事項、そして具体的な活用例までを徹底的に解説します。MGETコマンドを理解し、効果的に活用することで、Redisを利用するアプリケーションのパフォーマンスを劇的に改善できる可能性があります。

2. Redis MGETコマンドとは

MGETコマンドは、Redisに格納されている複数のキーに対応する値を、一度のコマンド実行でまとめて取得するためのコマンドです。

基本的な構文:

redis
MGET key1 [key2 ...]

MGETコマンドは、1つ以上のキー名を引数として取ります。Redisサーバーは、指定されたすべてのキーに対応する値を取得し、その結果をまとめてクライアントに返します。

実行例:

まず、いくつかのキーに値を設定しておきます。

redis
SET user:1:name "Alice"
OK
SET user:1:email "[email protected]"
OK
SET user:1:last_login "2023-10-27T10:00:00Z"
OK
SET product:123:name "Laptop"
OK
SET product:123:price "1200.00"
OK

これらのキーの値をまとめて取得したい場合、MGETコマンドは以下のように使用します。

redis
MGET user:1:name user:1:email user:1:last_login product:123:name

Redisサーバーは、これらのキーに対応する値をすべて取得し、以下の形式でクライアントに返します。

redis
1) "Alice"
2) "[email protected]"
3) "2023-10-27T10:00:00Z"
4) "Laptop"

戻り値は、引数で指定したキーの順序に対応した値のリスト(配列)になります。

もし存在しないキーを指定した場合、そのキーに対応する位置の戻り値はnil(またはクライアントライブラリによってはnullや特別な表現)となります。

例えば、存在しないキーuser:2:nameを含めてMGETを実行してみましょう。

redis
MGET user:1:name user:2:name user:1:email

戻り値は以下のようになります。

redis
1) "Alice"
2) (nil)
3) "[email protected]"

user:2:nameは存在しないため、対応する2番目の要素は(nil)となっています。これは、MGETコマンドが部分的な失敗(一部のキーが存在しないこと)を許容し、エラーで中断するのではなく、存在しないキーについてはnilを返すという設計になっているためです。

また、MGETコマンドは文字列(String)型の値を持つキーに対してのみ有効です。もしリスト(List)型やハッシュ(Hash)型など、文字列型以外の値を持つキーをMGETで指定した場合、Redisはエラーを返します。

redis
LPUSH mylist "item1" "item2"
(integer) 2
MGET user:1:name mylist

この場合、Redisはエラーを返します。

redis
(error) WRONGTYPE Operation against a key holding the wrong kind of value

これは、MGETが期待するのは文字列型の値のみだからです。異なるデータ型の値をまとめて取得したい場合は、パイプライン処理を使ってそれぞれの型に応じたコマンド(例: HGETALLLRANGEなど)をまとめて実行する必要があります。ただし、文字列型として保存されているキーであれば、その値がJSON文字列やシリアライズされたデータなど、どのような形式であってもMGETで取得できます。

3. MGETコマンドの最大のメリット:ネットワークラウンドトリップの削減

MGETコマンドの最大のメリットは、まさに「はじめに」で触れたネットワークラウンドトリップの削減です。これはRedisのパフォーマンス最適化において非常に重要な要素です。

単一GETコマンドでの複数キー取得の問題点:

複数のキーの値を個別にGETコマンドで取得する場合、各コマンドごとに以下のような一連の処理が発生します。

  1. クライアントがRedisサーバーにコマンドを送信する。
  2. コマンドがネットワークを介してサーバーに到達する。
  3. Redisサーバーがコマンドを受け取り、処理キューに入れる。
  4. Redisサーバーがコマンドを実行する(キーのルックアップ、値の取得)。
  5. Redisサーバーが結果をクライアントに送信する。
  6. 結果がネットワークを介してクライアントに到達する。
  7. クライアントが結果を受け取る。

この「送信」から「結果受け取り」までの一往復がネットワークラウンドトリップです。この往復にかかる時間、特にネットワークのレイテンシは、Redisサーバーがコマンドを処理する時間と比較して、多くの場合無視できないほど大きくなります。ローカルネットワーク内であっても数ミリ秒、インターネットを介していれば数十ミリ秒、あるいはそれ以上かかることもあります。

例えば、1回のコマンド実行(サーバー側の処理)が0.1ミリ秒で、ネットワークの片道遅延が1ミリ秒と仮定します。
– 単一のGETコマンドの実行時間 ≈ 0.1ms (サーバー処理) + 1ms (送信) + 1ms (受信) = 2.1ms
– 10個のキーを個別のGETで取得する場合: 10 * 2.1ms = 21ms

MGETによるラウンドトリップ削減のメカニズム:

MGETコマンドを使用すると、複数のキーに対する取得要求を一度のコマンドとしてRedisサーバーに送信できます。サーバーはこれらの要求をまとめて処理し、結果を一度の応答としてクライアントに返します。

MGETコマンドによる10個のキー取得の場合、発生するラウンドトリップは1回です。

  1. クライアントがMGET key1 key2 … key10というコマンドをRedisサーバーに送信する。
  2. コマンドがネットワークを介してサーバーに到達する(1回の往復)。
  3. Redisサーバーがコマンドを受け取り、処理キューに入れる。
  4. RedisサーバーがMGETコマンドを実行する(10個のキーをまとめてルックアップし、値を取得)。サーバー側の処理時間は個別のGETを10回行うよりもわずかにオーバーヘッドがありますが、劇的に増えるわけではありません。
  5. Redisサーバーが10個の値を含む結果をクライアントに送信する(1回の往復)。
  6. 結果がネットワークを介してクライアントに到達する。
  7. クライアントが結果を受け取り、パースする。

この場合、かかる時間は以下のようになります。
– MGETコマンドの実行時間 ≈ 0.1ms (サーバー処理) + α (MGETの処理オーバーヘッド) + 1ms (送信) + 1ms (受信)
– 例えば、サーバー処理が合計0.5msになったとしても: 0.5ms + 1ms + 1ms = 2.5ms

10個のキーを取得するのに、個別のGETでは21msかかっていたものが、MGETではわずか2.5ms程度で済む可能性があります。これは劇的なパフォーマンス改善です。レイテンシが大きい環境(例えば、Redisが別のデータセンターにある場合など)ほど、この効果は顕著になります。

パフォーマンスへの影響(具体例):

ウェブページを表示するために、バックエンドサーバーがRedisから様々な情報を取得する必要があると想像してください。
– ユーザー情報 (ユーザー名、アイコンURL、ステータスなど): 3キー
– 表示中の記事情報 (タイトル、本文、作成日時、著者IDなど): 4キー
– 関連情報 (タグリスト、コメント数など): 2キー
– ユーザーの設定 (表示言語、テーマなど): 2キー

合計で11個のキーの情報を取得する必要があるとします。

個別のGETの場合:
11回のGETコマンド発行。
仮にサーバー処理0.1ms、ネットワーク片道1msとすると、1回のGETは2.1ms。
合計時間: 11 * 2.1ms = 23.1ms (最低でも)

MGETの場合:
1回のMGETコマンド発行 (キーを11個指定)。
仮にサーバー処理0.1ms + MGETオーバーヘッド0.2ms、ネットワーク片道1msとすると、1回のMGETは 0.3ms + 1ms + 1ms = 2.3ms。
合計時間: 2.3ms (最低でも)

この例では、MGETを使うことで通信にかかる時間を約1/10に短縮できることになります。これは、ユーザーがページをリクエストしてから表示されるまでの時間を大幅に短縮し、アプリケーションのスループット(単位時間あたりに処理できるリクエスト数)を向上させることに直接貢献します。

ラウンドトリップの削減は、Redisのようなネットワーク越しに利用するデータストアのパフォーマンス最適化において、最も基本的かつ重要なテクニックの一つです。MGETは、文字列型のキーをまとめて取得する場合に、このテクニックを簡単に実現できる手段として提供されています。

4. MGETコマンドのその他のメリット

ラウンドトリップ削減が最大のメリットですが、MGETコマンドには他にもいくつかの利点があります。

  • コードの簡潔性: 複数のGETコマンドをループで回す代わりに、MGETコマンドを一度呼び出すだけで済みます。これにより、アプリケーションコードがよりシンプルになり、読みやすく、メンテナンスしやすくなります。多くのRedisクライアントライブラリは、キーのリスト(配列)を引数としてMGETを呼び出すAPIを提供しており、コードの記述をさらに容易にしています。

  • Redisサーバーの負荷軽減: MGETは、複数のコマンド処理要求を一度に受け取ります。これにより、Redisサーバーは一度のコンテキストスイッチで複数のキー操作を実行できます。個別のGETコマンドが連続して到着する場合と比較して、コマンドの受け付け、パース、実行、応答という一連の処理におけるオーバーヘッドが削減されます。特に多数のクライアントから頻繁に細かな要求が来るような状況では、MGETのようなまとめて処理できるコマンドを使うことで、サーバーのCPUリソースをより効率的に利用できます。

  • アトミック性(厳密には異なるが実用上の利点): MGETコマンド自体は、指定されたすべてのキーに対して「アトミック」な操作ではありません。つまり、MGETコマンドが実行されている最中に、指定したキーのどれかが別のクライアントによって変更される可能性はあります。しかし、MGETコマンドによる値の取得は、Redisサーバー内での処理としては非常に短い時間で完了します。すべてのキーの値は、ほぼ同時に、特定の時点でのスナップショットとして取得されます。個別のGETをループで回す場合、ループの開始時と終了時でキーの値が変更されている可能性がMGETよりも高くなります。実用上、MGETによって取得された値のセットは、単一の操作で取得されたかのように扱えることが多く、データの一貫性(特定の時点における整合性)が必要な場合にMGETは有用です。厳密なアトミック性が求められる場合は、Luaスクリプトやトランザクション(WATCH/MULTI/EXEC)を利用する必要がありますが、多くのキャッシュ取得のような用途ではMGETで十分です。

これらのメリットを考慮すると、複数の関連する文字列型データをRedisから取得する際には、MGETコマンドを利用することが強く推奨されます。

5. MGETコマンドの基本的な使い方

MGETコマンドの使い方は非常にシンプルです。コマンドラインインターフェース(redis-cli)から使用する場合と、プログラミング言語のクライアントライブラリから使用する場合で見てみましょう。

redis-cliでの使い方:

redis-cliを起動し、MGETコマンドの後に取得したいキー名をスペース区切りで列挙します。

bash
redis-cli
127.0.0.1:6379> SET key1 "value1"
OK
127.0.0.1:6379> SET key2 "value2"
OK
127.0.0.1:6379> SET non_existent_key "some_value" # このキーは後で削除
OK
127.0.0.1:6379> DEL non_existent_key # 削除して存在しない状態にする
(integer) 1
127.0.0.1:6379> MGET key1 key2 non_existent_key key3
1) "value1"
2) "value2"
3) (nil)
4) (nil)
127.0.0.1:6379>

ご覧の通り、存在しないキー(non_existent_key, key3)に対応する戻り値は(nil)となります。戻り値の順序は、引数で指定したキーの順序と完全に一致します。

クライアントライブラリでの使い方:

ほとんどのプログラミング言語用のRedisクライアントライブラリは、MGETコマンドに対応するメソッドを提供しています。メソッド名は通常mgetget_manyのような名前であり、引数としては取得したいキー名のリスト(あるいは可変長引数)を受け取ります。戻り値は、キーに対応する値のリスト(あるいは連想配列など、言語による)となります。

Python (using redis library):

“`python
import redis

Redisサーバーに接続

r = redis.Redis(host=’localhost’, port=6379, db=0)

事前にキーを設定

r.set(‘user:1:name’, ‘Alice’)
r.set(‘user:1:email’, ‘[email protected]’)
r.set(‘user:2:name’, ‘Bob’)

MGETを使って複数キーの値を取得

keys_to_get = [‘user:1:name’, ‘user:1:email’, ‘user:3:name’, ‘user:2:name’]
values = r.mget(keys_to_get)

print(f”Keys: {keys_to_get}”)
print(f”Values: {values}”)
“`

出力例:

Keys: ['user:1:name', 'user:1:email', 'user:3:name', 'user:2:name']
Values: [b'Alice', b'[email protected]', None, b'Bob']

Pythonのredisライブラリでは、キー名も値もデフォルトでバイト列として扱われるため、値はb'...'の形式で表示されます。存在しないキーuser:3:nameに対応する値はNoneとなっています。戻り値のリストの順序は、入力のキーリストの順序に対応しています。

Node.js (using ioredis library):

“`javascript
const Redis = require(‘ioredis’);
const redis = new Redis();

// 事前にキーを設定
async function setKeys() {
await redis.set(‘product:1:name’, ‘Widget’);
await redis.set(‘product:1:price’, ‘19.99’);
await redis.set(‘product:2:name’, ‘Gadget’);
}

// MGETを使って複数キーの値を取得
async function getValues() {
await setKeys(); // キーを設定 (初回実行時のみ)

const keysToGet = [‘product:1:name’, ‘product:1:price’, ‘product:3:name’, ‘product:2:name’];
const values = await redis.mget(…keysToGet); // 可変長引数として渡す

console.log(Keys: ${keysToGet});
console.log(Values: ${values});

redis.quit(); // 接続を閉じる
}

getValues();
“`

出力例:

Keys: [ 'product:1:name', 'product:1:price', 'product:3:name', 'product:2:name' ]
Values: [ 'Widget', '19.99', null, 'Gadget' ]

Node.jsのioredisでは、デフォルトで値は文字列として扱われます。存在しないキーproduct:3:nameに対応する値はnullとなっています。

Ruby (using redis-rb library):

“`ruby
require ‘redis’

Redisサーバーに接続

redis = Redis.new

事前にキーを設定

redis.set(‘session:abc:user_id’, ‘123’)
redis.set(‘session:abc:last_activity’, Time.now.to_s)

MGETを使って複数キーの値を取得

keys_to_get = [‘session:abc:user_id’, ‘session:def:user_id’, ‘session:abc:last_activity’]
values = redis.mget(*keys_to_get) # 可変長引数として渡す

puts “Keys: #{keys_to_get}”
puts “Values: #{values}”

redis.quit # 接続を閉じる
“`

出力例:

Keys: ["session:abc:user_id", "session:def:user_id", "session:abc:last_activity"]
Values: ["123", nil, "2023-10-27 12:34:56 +0900"] # 時刻は実行時のもの

Rubyのredis-rbでは、存在しないキーに対応する値はnilとなります。

多くのクライアントライブラリでは、MGETに渡すキーのリストと返ってくる値のリストのインデックスが一致するため、取得した値を元のキーと関連付けるのは比較的簡単です。例えばPythonの例であれば、values[0]keys_to_get[0] (user:1:name) の値、values[1]keys_to_get[1] (user:1:email) の値、というようになります。

6. MGETコマンドの詳細な挙動

MGETコマンドの挙動について、さらに詳しく見ていきましょう。

戻り値の順序と対応付け:

MGETコマンドの戻り値は、常に引数として指定されたキーと同じ順序で並んだ値のリスト(配列)です。これは非常に重要な特性であり、クライアント側でどの値がどのキーに対応するかを簡単に判断できます。例えば、MGET keyA keyB keyCというコマンドに対して、Redisは[valueA, valueB, valueC]というリストを返します。たとえkeyBが存在しなくても、戻り値は[valueA, nil, valueC]のようになり、nilが2番目の要素であることから、それが2番目の引数であるkeyBに対応することがわかります。

存在しないキーが含まれる場合の戻り値:

前述の通り、MGETで指定したキーの中に存在しないものが含まれていても、コマンド全体がエラーになるわけではありません。存在しないキーに対応する位置には、Redisプロトコル上ではNull Bulk Stringという特別な形式が返されます。多くのクライアントライブラリはこれを、各言語の「null」や「None」といった値として扱います。これにより、アプリケーションはMGETの戻り値をチェックするだけで、どのキーが存在したか、どのキーが存在しなかったか、そしてそれぞれの値を取得できます。

“`redis

例: key1, key3 は存在し、 key2, key4 は存在しない

MGET key1 key2 key3 key4
1) “value1” # key1 の値
2) (nil) # key2 は存在しない
3) “value3” # key3 の値
4) (nil) # key4 は存在しない
“`

この挙動は、アプリケーションが一部のデータが欠落している可能性を考慮して設計されている場合に非常に便利です。例えば、ユーザープロフィールでオプションの情報(例: プロフィール画像URL)をMGETに含めた場合、その情報が設定されていなくても(キーが存在しなくても)、他の必須情報を取得できます。

異なるデータ型のキーをMGETで指定した場合:

MGETコマンドは、文字列(String)型の値を取得するためのコマンドです。もし指定したキーの中に、文字列型以外のデータ型(例: List, Set, Hash, Sorted Setなど)の値を持つキーが含まれている場合、MGETコマンドはエラーを返します。コマンド全体が実行されず、エラーメッセージがクライアントに返されます。

“`redis
SET my_string_key “hello”
RPUSH my_list_key “item1” “item2”

MGET my_string_key my_list_key
(error) WRONGTYPE Operation against a key holding the wrong kind of value
“`

これは、MGETが内部的に文字列型に対応する取得ロジックを使用しており、他のデータ型に対してはそのロジックが適用できないためです。異なる型の値をまとめて取得したい場合は、パイプライン処理を使用し、それぞれのキーに対して適切なコマンド(GET, LLEN, HGETALLなど)を発行する必要があります。

大量のキーを指定した場合の注意点:

MGETコマンドには指定できるキーの数に理論上の上限はありませんが、実際にはいくつかの制限や注意点があります。

  1. コマンドバッファサイズ: Redisサーバーは、クライアントから受信したコマンドをバッファリングします。非常に長いMGETコマンド(非常に多数のキーを指定した場合)は、このバッファを使い果たしたり、処理に時間がかかったりする可能性があります。デフォルトのコマンドバッファサイズは十分に大きいことが多いですが、極端なケースでは問題になることもあります。
  2. ネットワーク帯域: MGETで取得するキーの数が多いほど、またはそれぞれの値が大きいほど、Redisサーバーからクライアントへの応答のサイズが大きくなります。これにより、ネットワーク帯域を圧迫し、転送に時間がかかる可能性があります。特に、非常に大きなバリューを持つキーを多数MGETで取得しようとすると、ネットワークのボトルネックになることがあります。
  3. Redisサーバーのブロック: Redisは基本的にシングルスレッドでコマンドを処理します(バージョン4.0以降はバックグラウンドスレッドも利用されますが、基本的なデータ操作はメインスレッドで行われます)。非常に多数のキーに対するMGETコマンドは、サーバーのメインスレッドを長時間ブロックする可能性があります。これにより、他のクライアントからのコマンド処理が遅延し、Redis全体の応答性が低下する可能性があります。Redisのドキュメントでは、一つのMGETコマンドで処理するキーの数を、せいぜい数百から数千程度に抑えることが推奨されています。大量のキーが必要な場合は、MGETを複数回に分割するか、後述するパイプライン処理を検討する必要があります。
  4. メモリ使用量: MGETコマンドを実行する際、Redisサーバーは指定されたすべてのキーに対応する値をメモリ上に一時的に保持し、まとめてクライアントに送信します。取得する値の合計サイズが大きい場合、サーバーのメモリ使用量が増加し、他の操作に影響を与える可能性があります。

これらの点から、MGETを使う際には指定するキーの数や値の大きさに注意が必要です。一般的には、関連性の高い、まとめて必要になるであろう少数のキー(数十〜数百程度)に対してMGETを使用するのが効果的です。

7. パフォーマンスに関する考慮事項

MGETコマンドのパフォーマンスを最大限に引き出すためには、いくつかの点を考慮する必要があります。

MGETで指定するキーの数(バッチサイズ):

MGETで取得するキーの数は、パフォーマンスに大きく影響します。
キーが少なすぎる場合: MGETを使うメリット(ラウンドトリップ削減)が小さくなります。数個のキーであれば、個別のGETとMGETで大きな差はないかもしれません。
キーが多すぎる場合: 前述の通り、Redisサーバーのブロック、ネットワーク帯域の圧迫、クライアントでの応答パース時間の増加など、パフォーマンス上の問題が発生する可能性があります。

最適なMGETのバッチサイズ(一度に指定するキーの数)は、アプリケーションの利用パターン、ネットワーク環境(レイテンシ、帯域)、Redisサーバーのリソース(CPU、メモリ)、Redisのバージョンなどによって異なります。一般的には、数十から数百、場合によっては数千個程度が実用的な範囲と言われます。本番環境で実際にテストを行い、最適なバッチサイズを見つけることが重要です。

大きなバリューを持つキーの影響:

MGETで取得するキーの中に、非常に大きなバリューを持つものが含まれている場合、その影響は大きくなります。大きな値をネットワーク越しに転送する時間が増加し、またサーバー側でその大きな値をメモリに読み込んでクライアントに送信する際のコストも増加します。MGETは複数のキーの値をすべて取得してまとめて返しますが、もし大きな値が多数含まれていると、ネットワーク転送がボトルネックとなり、ラウンドトリップを削減した効果が相殺されてしまう可能性もあります。もし、取得する値のサイズが大きく変動する場合や、一部に巨大な値が含まれる可能性がある場合は、MGETの使用を再検討するか、キーの設計を見直す必要があるかもしれません。

パイプライン処理との比較:

MGETは、複数のGET操作を一度のコマンドとして実行できるため、一種のパイプライン処理と見なすことができます。Redisのパイプライン(Pipeline)機能は、MGETよりも汎用的なメカニズムです。パイプラインを使うと、複数のRedisコマンド(GET、SET、DEL、LPUSH、HGETALLなど、MGET以外の様々なコマンドを含む)をまとめてクライアントからサーバーに送信し、すべてのコマンドの応答をまとめて受け取ることができます。

“`python

Pythonにおけるパイプライン処理の例

pipe = r.pipeline()
pipe.get(‘key1’)
pipe.set(‘key4’, ‘value4’)
pipe.delete(‘key3’)
pipe.mget([‘keyA’, ‘keyB’]) # パイプライン内にMGETを含めることも可能
results = pipe.execute()
print(results) # [value of key1, True, 1 (or 0), [valueA, valueB]] のようなリスト
“`

パイプライン処理も、MGETと同様にネットワークラウンドトリップを削減することを目的としています。複数の異なる種類のコマンドをまとめて実行したい場合や、MGETだけでは対応できないケース(例えば、複数のハッシュキーをそれぞれHGETALLで取得したい場合など)では、パイプラインがより適しています。

MGETはパイプライン処理の特定のケース(複数のGETコマンドをまとめる場合)を単純化した専用コマンドと考えることができます。GET操作のみをまとめて行いたい場合はMGETがシンプルで分かりやすいですが、より複雑な複数のコマンドをまとめて実行したい場合はパイプラインを使用するのが一般的です。パフォーマンスの観点では、どちらもラウンドトリップ削減という点では同様の効果をもたらしますが、パイプラインの方がより多くのコマンドをまとめて送信できる可能性があります(ただし、コマンドの合計サイズや処理時間が増える点はMGETと同様に注意が必要です)。

8. MGET以外の複数キー操作コマンド

MGETコマンドは、文字列型の値を複数まとめて取得するためのものですが、Redisには他にも複数のキーをまとめて操作するためのコマンドがいくつか存在します。これらも同様にネットワークラウンドトリップの削減というメリットがあります。

  • MSET (Multi SET): 複数のキーに値をまとめて設定するコマンドです。
    構文: MSET key1 value1 [key2 value2 ...]
    例: MSET user:1:name "Alice" user:1:email "[email protected]"
    MSETは、複数のSET操作を一度に実行するため、複数のキーを初期設定したり更新したりする場合にラウンドトリップを削減できます。

  • DEL (Delete): 1つ以上のキーをまとめて削除するコマンドです。
    構文: DEL key1 [key2 ...]
    例: DEL session:abc:user_id session:abc:last_activity
    複数の関連するキーをまとめて削除したい場合に便利です。

  • EXISTS (Exists): 1つ以上のキーが存在するかどうかを確認するコマンドです。Redis 3.0.3以降で複数キーを指定できるようになりました。
    構文: EXISTS key1 [key2 ...]
    例: EXISTS user:1:name user:2:name
    戻り値は、指定したキーの中で実際に存在したキーの総数です。これにより、複数のキーの存在チェックを一度に行えます。

  • UNLINK (Unlink): 非同期でキーを削除するコマンドです。DELと同様に複数キーを指定できます。大きなキーを削除する際に、サーバーのブロックを防ぐために使用されます。
    構文: UNLINK key1 [key2 ...]
    例: UNLINK cache:large_data:1 cache:large_data:2

これらのコマンドも、関連する複数のキーに対してまとめて操作を行うことで、個別のコマンドを繰り返し実行する場合に比べてネットワーク効率とサーバー効率を向上させることができます。MGETと同様の原理でパフォーマンスが改善されます。

9. MGETコマンドの具体的な使用例

MGETコマンドは様々な場面で役立ちます。代表的な使用例をいくつか紹介します。

  • ユーザープロフィール情報の取得:
    ユーザーのプロフィール情報は、名前、メールアドレス、ID、最終ログイン日時、設定など、多くの属性を持つことがよくあります。これらの属性をそれぞれ別のキーに保存している場合(例: user:{user_id}:name, user:{user_id}:email, user:{user_id}:last_loginなど)、ユーザーのプロフィールページを表示する際にこれらの情報をまとめて取得する必要があります。このような場合にMGETは非常に有効です。

    “`python
    user_id = 123
    keys = [
    f’user:{user_id}:name’,
    f’user:{user_id}:email’,
    f’user:{user_id}:last_login’,
    f’user:{user_id}:profile_image_url’, # オプション情報
    f’user:{user_id}:status’
    ]
    profile_data = r.mget(keys)

    profile_data はキーの順序に対応した値のリスト

    name, email, last_login, profile_image_url, status = profile_data

    nil/None チェックを行いながらデータを処理

    user_info = {
    ‘name’: name.decode(‘utf-8’) if name else None,
    ‘email’: email.decode(‘utf-8’) if email else None,
    ‘last_login’: last_login.decode(‘utf-8’) if last_login else None,
    ‘profile_image_url’: profile_image_url.decode(‘utf-8’) if profile_image_url else None,
    ‘status’: status.decode(‘utf-8’) if status else None
    }
    print(user_info)
    “`
    この例では、5つの属性を一度のMGET呼び出しで取得しています。もし個別のGETを使用した場合、5回のラウンドトリップが必要になるところです。

  • 商品詳細情報の取得:
    ECサイトで商品の詳細ページを表示する際、商品名、価格、在庫数、商品説明、画像URLリストなど、複数の情報が必要です。これらをキーに保存している場合(例: product:{product_id}:name, product:{product_id}:price, product:{product_id}:stockなど)、MGETでまとめて取得できます。

    ``javascript
    const productId = 456;
    const keys = [
    product:${productId}:name,product:${productId}:price,product:${productId}:stock,product:${productId}:description_short,product:${productId}:main_image_url`
    ];
    const values = await redis.mget(…keys);

    const productDetails = {
    name: values[0],
    price: values[1],
    stock: values[2],
    description_short: values[3],
    main_image_url: values[4]
    };
    console.log(productDetails);
    “`

  • キャッシュデータの取得:
    データベースから取得したデータをRedisにキャッシュする場合、関連する複数のデータをまとめてキャッシュすることがよくあります。例えば、ある記事に関連する情報(記事本文、著者名、カテゴリ名など)をそれぞれ別のキーにキャッシュした場合、記事ページ表示時にこれらのキーをMGETでまとめて取得することで、キャッシュヒット時の表示速度を向上させることができます。

    “`ruby
    article_id = 789
    keys = [
    “cache:article:#{article_id}:body”,
    “cache:article:#{article_id}:author_name”,
    “cache:article:#{article_id}:category_name”,
    “cache:article:#{article_id}:published_at”
    ]
    cached_data = redis.mget(*keys)

    article_body, author_name, category_name, published_at = cached_data

    if article_body # 少なくとも本文がキャッシュにあればヒットとみなす
    puts “Cache hit!”
    puts “Article Body: #{article_body}”
    puts “Author: #{author_name}” if author_name
    # … データの利用 …
    else
    puts “Cache miss. Fetching from DB…”
    # データベースから取得し、RedisにSET/MSETでキャッシュする
    end
    “`

  • セッション情報の取得:
    ユーザーのセッション情報をRedisに保存する場合、ログインユーザーID、カート内容、設定フラグなど、複数の属性をまとめて保存することがあります。これらを個別のキーで管理している場合(例: session:{session_id}:user_id, session:{session_id}:cart, session:{session_id}:flags)、リクエスト処理の開始時にMGETでまとめて取得することで効率化できます。

これらの例からもわかるように、MGETは、アプリケーション内で論理的に関連する複数のデータがRedis上で複数の文字列キーとして管理されている場合に、それらのデータを効率的に取得するための非常に強力なツールです。キーの命名規則を工夫することで、関連するキーを容易にリストアップし、MGETでまとめて取得できるようになります。

10. MGETコマンドを使う上での注意点

MGETコマンドを効果的に、そして問題なく使用するためには、いくつかの注意点を理解しておく必要があります。

  • キー名の設計の重要性: MGETを効果的に使うためには、関連するデータがどのようにキーとして保存されているかが重要です。前述の例のように、user:{user_id}:name, user:{user_id}:emailのように、共通のプレフィックスやパターンを持つようにキーを設計すると、プログラムから関連するキー名を簡単に生成し、MGETに渡すことができます。キー設計がバラバラだと、まとめて取得したいキー名をプログラムで生成するのが難しくなります。

  • 指定するキーがすべて文字列型であること: MGETは文字列型(String)専用のコマンドです。異なるデータ型のキーを混在させて指定するとエラーになります。MGETを使用する前に、指定するキーがすべて文字列型であることを確認する必要があります。もし異なる型を含む複数のキーの値をまとめて取得したい場合は、パイプライン処理を使用して、各キーに対して適切なコマンドを発行してください。

  • 大量のキーを指定する際のリスク: セクション6でも詳しく述べたように、MGETで大量のキーを指定すると、Redisサーバーのブロック、ネットワーク帯域の圧迫、クライアント側のメモリ使用量増加などのリスクがあります。数万、数十万といった数のキーを一度のMGETで取得することは避けるべきです。アプリケーションの要件とRedisサーバーのリソース、ネットワーク環境を考慮して、適切なバッチサイズを検討してください。大量のキーが必要な場合は、MGETを複数回に分割するか、他の手段(例えば、データをより大きな単一のキーにJSONやMsgPackなどでまとめて格納し、GETで取得後にクライアント側でパースするなど)を検討する必要があります。

  • Redis Cluster環境での注意点: Redis Clusterはデータを複数のノード(シャード)に分散して格納します。各キーは特定のハッシュスロットにマッピングされ、そのスロットを担当するノードに保存されます。MGETコマンドは、指定されたすべてのキーが同じハッシュスロットに属している場合のみ、単一のコマンドとしてそのスロットを担当するノードで実行できます。もしMGETコマンドに異なるスロットに属するキーが含まれている場合、多くのRedisクライアントライブラリは、このMGETコマンドを自動的に分解し、各スロットを担当するノードに対して個別のMGETコマンド(またはパイプライン)を発行します。この場合、ラウンドトリップ数は削減されますが、「単一のコマンド」として扱われるわけではないため、クライアント側の処理やネットワーク通信が複雑になります。また、一部の古いクライアントライブラリや、特定のケースでは、異なるスロットのキーを含むMGETコマンドはエラーとなることもあります。したがって、Redis Cluster環境でMGETを使用する場合は、指定するキーが可能な限り同じスロットに属するように設計するか、クライアントライブラリのCluster対応状況を確認することが重要です。キーが同じスロットに属するようにするには、Redis Cluster key hash tagsという機能を利用できます。キー名の一部を{}で囲むことで、その囲まれた部分に基づいてハッシュスロットが計算されます。例えば、user:{123}:nameuser:{123}:emailは、どちらも{123}に基づいて同じスロットにマッピングされます。

  • トランザクションとの組み合わせ: MGETコマンドは、Redisのトランザクション(MULTI/EXEC)の中に含めることができます。これにより、MGETによる取得操作を他のコマンドと組み合わせてアトミックに実行できます(厳密にはEXECが実行されるまでのコマンドキューイングがアトミックです)。ただし、トランザクション内でのMGETは、そのトランザクションが完了するまで応答を返しません。また、トランザクション内でMGETの結果を使って後続のコマンドの引数を決定するといった複雑なロジックは、Redisのトランザクションモデル(キューイング方式)には合わないため、代わりにLuaスクリプトの使用を検討する必要があります。

これらの注意点を踏まえることで、MGETコマンドのメリットを享受しつつ、潜在的な問題を回避することができます。特にRedis Cluster環境でのスロットに関する問題は、大規模なシステムでRedisを運用する際に考慮すべき重要な点です。

11. まとめ

この記事では、RedisのMGETコマンドについて詳細に解説しました。MGETは、複数のキーに対応する値を一度のネットワークラウンドトリップでまとめて取得するためのコマンドです。

MGETの最大のメリットは、ネットワークラウンドトリップ数を劇的に削減できる点にあります。これにより、特にネットワーク遅延が大きい環境や、アプリケーションが頻繁にRedisから複数の関連データを取得する必要がある場合に、アプリケーションの応答性能とスループットを大幅に向上させることが可能です。個別のGETコマンドを繰り返し実行する「N+1問題」を回避する、Redisにおけるパフォーマンス最適化の基本的なテクニックの一つです。

MGETコマンドはシンプルで使いやすく、多くのRedisクライアントライブラリから容易に利用できます。引数で指定したキーの順序に対応した値のリストを返し、存在しないキーに対してはnilを返すという挙動は、アプリケーション側での結果処理を容易にします。

ただし、MGETは文字列型専用であり、異なるデータ型のキーを混在させることはできません。また、一度に大量のキーを指定すると、Redisサーバーのリソース(CPU、メモリ)やネットワーク帯域を圧迫し、かえってパフォーマンスが悪化したり、サーバーがブロックされたりするリスクがある点に注意が必要です。適切なバッチサイズを検討し、本番環境でのテストを通じて最適な運用方法を見つけることが重要です。

Redis Cluster環境でMGETを使用する場合は、異なるスロットに属するキーをMGETで指定するとクライアント側で分割処理が行われるか、場合によってはエラーとなる可能性があるため、キーの設計やクライアントライブラリの動作を理解しておく必要があります。

MGETは、MSET(複数キー設定)、DEL(複数キー削除)、EXISTS(複数キー存在確認)、UNLINK(非同期複数キー削除)といった他の複数キー操作コマンドと同様に、複数の操作をまとめて行うことで効率化を図るためのコマンドファミリーの一部です。これらのコマンドを適切に使い分けることで、Redisとの通信効率を最大限に高めることができます。

アプリケーション設計において、複数の関連するデータをRedisに保存する際には、それらをまとめて取得する可能性があるかを検討し、MGETが使いやすいようにキー名を設計することが推奨されます。MGETコマンドを効果的に活用することで、Redisをバックエンドとしたアプリケーションのパフォーマンスを向上させ、よりスケーラブルで応答性の高いシステムを構築できるでしょう。

Redisを利用する際には、単一コマンドの高速性だけでなく、MGETやパイプライン処理のような複数コマンドをまとめて実行する機能の重要性を理解し、積極的に活用していくことが、パフォーマンス最適化の鍵となります。


コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール