Redis Pub/Sub 入門:仕組みと使い方を徹底解説

Redis Pub/Sub 入門:仕組みと使い方を徹底解説

はじめに

現代の多くのアプリケーション、特にリアルタイム性が求められるシステムや分散システムにおいて、異なるコンポーネント間での非同期なメッセージングは不可欠な要素となっています。ユーザーへのプッシュ通知、分散キャッシュの無効化、イベント駆動型マイクロサービス間の通信など、様々な場面でメッセージングシステムが利用されています。

メッセージングシステムにはいくつかのパターンがありますが、その中でも特にシンプルで効率的なのが「Publish/Subscribe (Pub/Sub)」モデルです。このモデルは、メッセージを送信する側(パブリッシャー)と、メッセージを受信する側(サブスクライバー)が直接通信するのではなく、中央のメッセージブローカー(この場合はRedis)を介して疎結合に通信します。

この記事では、インメモリデータストアとして広く利用されているRedisが提供するPub/Sub機能に焦点を当て、その仕組み、基本的な使い方から、より高度な利用方法、内部動作、注意点、そしてどのようなケースに適しているのかを約5000語というボリュームで徹底的に解説します。Pub/Subをこれから学びたい方、RedisのPub/Sub機能について深く理解したい方にとって、決定版となるような記事を目指します。

Redis Pub/Subとは何か?

Redis Pub/Subは、Redisが提供する機能の一つで、Publish/Subscribeメッセージングパラダイムを実装したものです。Redisサーバーがメッセージブローカーとなり、メッセージの送信者と受信者の仲介を行います。

  • パブリッシャー (Publisher): メッセージを作成し、特定の「チャンネル」に送信します。パブリッシャーは誰がそのメッセージを受信するのかを知る必要はありません。
  • サブスクライバー (Subscriber): 特定の「チャンネル」または「パターンのチャンネル」に登録し、そのチャンネルに送信されたメッセージを受信します。サブスクライバーは誰がメッセージを送信したのかを知る必要はありません。
  • チャンネル (Channel): メッセージが送信・受信される論理的な名前空間です。パブリッシャーは特定のチャンネルにメッセージを「発行 (Publish)」し、サブスクライバーは特定のチャンネルを「購読 (Subscribe)」します。

このモデルの最大の特徴は、パブリッシャーとサブスクライバーが互いの存在を知らない、つまり「疎結合」である点です。これにより、システムのコンポーネントを独立して開発・デプロイしやすくなり、柔軟性やスケーラビリティが向上します。

なぜPub/Subが必要なのか?(Publish/Subscribeモデルの利点)

直接的な点対点(Point-to-Point)通信と比較して、Pub/Subモデルには以下のような利点があります。

  1. 疎結合性: パブリッシャーはメッセージをブローカーに送るだけでよく、誰がそのメッセージを受け取るか(サブスクライバーの数や種類)を知る必要がありません。同様に、サブスクライバーも誰がメッセージを送ったかを知る必要がありません。これにより、システムの各部分が独立して変更・拡張可能になります。
  2. スケーラビリティ: メッセージブローカー(Redis)がメッセージの配信を仲介するため、一対多(One-to-Many)の通信が容易に実現できます。一つのメッセージを複数のサブスクライバーに同時に配信できます。パブリッシャーやサブスクライバーの数が増えても、ブローカーが負荷を吸収します。
  3. 柔軟性: 新しいサブスクライバーを追加する際に、既存のパブリッシャーや他のサブスクライバーに影響を与えることなく簡単にシステムに組み込むことができます。同様に、新しいパブリッシャーを追加する際も既存システムへの影響を最小限に抑えられます。
  4. 非同期性: メッセージの送信と受信が非同期に行われます。パブリッシャーはメッセージを送信したら即座に次の処理に進むことができ、サブスクライバーはメッセージが到着したときにそれを処理します。これにより、システムの応答性が向上し、ボトルネックを防ぐことができます。

この記事で学ぶこと

この記事を読み終えることで、あなたは以下の点を深く理解しているでしょう。

  • Redis Pub/Subの基本的な仕組みとPub/Subモデルの概念。
  • Redis CLIを使ったSUBSCRIBE, PUBLISH, UNSUBSCRIBEコマンドの基本的な使い方。
  • チャンネルパターンマッチングによる購読(PSUBSCRIBE, PUNSUBSCRIBE)の高度な使い方。
  • PUBSUBコマンドを使った購読状態の確認方法。
  • PythonやNode.jsなどの主要なプログラミング言語からRedis Pub/Subを利用する方法。
  • Redis Pub/Subの内部動作(Fan-out配信、バッファリングなど)と、メッセージの永続性や配信保証に関する重要な注意点。
  • Slow Subscriber Problemとその影響、そしてPub/Subの限界。
  • Redis Pub/Subが適しているユースケースと、そうでないケース(他のメッセージング技術との比較)。
  • Redis Streamsなど、関連するRedisの他の機能との違い。

さあ、Redis Pub/Subの世界に踏み込みましょう。

Pub/Subの基本的な概念

Pub/Subモデルは非常にシンプルです。パブリッシャー、サブスクライバー、チャンネルという3つの主要な要素から構成されます。

パブリッシャー (Publisher)

パブリッシャーはメッセージを生成し、Redisサーバーに送信する役割を担います。メッセージは常に特定のチャンネルに紐づけられて送信されます。パブリッシャーは、そのメッセージを誰が受信するか、あるいは受信するサブスクライバーが存在するかどうかすら知る必要がありません。ただ「このチャンネルにこのメッセージを送る」という操作を行うだけです。この操作はRedisのPUBLISHコマンドによって行われます。

サブスクライバー (Subscriber)

サブスクライバーは、特定のチャンネル、または特定のパターンに一致するチャンネルからのメッセージを受信したいと表明する役割を担います。サブスクライバーはRedisサーバーに対して「このチャンネルからのメッセージが来たら通知してほしい」とリクエストします。このリクエストはSUBSCRIBEコマンドまたはPSUBSCRIBEコマンドによって行われます。サブスクライバーは、メッセージがサーバーに到着すると、サーバーからそのメッセージがプッシュ配信されます。サブスクライバーは、メッセージが誰によって送信されたかを知る必要はありません。

チャンネル (Channel)

チャンネルは、メッセージが流れる「名前付きのパイプ」のようなものです。パブリッシャーは特定のチャンネルにメッセージを「発行」し、サブスクライバーは特定のチャンネルを「購読」します。チャンネル名は単なる文字列です。任意の文字列をチャンネル名として使用できます。例えば、news.sports, user.123.updates, chat.room.generalなどのように、ドット(.)などを使って階層的な命名規則を用いることが一般的です。

メッセージ (Message)

メッセージは、パブリッシャーからサブスクライバーへ伝えたい実際のデータです。Redis Pub/Subでは、メッセージは単なるバイト列として扱われます。通常、文字列やJSON形式のデータなどが使われます。Redisサーバーはメッセージの内容を検査したり、解釈したりすることはありません。単にパブリッシャーから受け取ったメッセージを、購読しているサブスクライバーすべてに配信するだけです。

Publish/Subscribeモデルの仕組み

Pub/Subモデルの基本的な流れは以下のようになります。

  1. サブスクライバーがチャンネルを購読する: サブスクライバーはRedisサーバーに対して、特定のチャンネル(例: news.sports)からのメッセージを受信したいことを伝えます(SUBSCRIBE news.sports)。サーバーは内部的に、このサブスクライバーがどのチャンネルを購読しているかを記録します。
  2. パブリッシャーがチャンネルにメッセージを送信する: パブリッシャーはRedisサーバーに対して、特定のチャンネル(例: news.sports)にメッセージ(例: "Latest scores updated")を送信することを伝えます(PUBLISH news.sports "Latest scores updated")。
  3. Redisサーバーがメッセージを配信する: Redisサーバーは、news.sportsチャンネルを購読しているすべてのサブスクライバーを検索します。そして、見つかったすべてのサブスクライバーに対して、受信したメッセージをプッシュ配信します。
  4. サブスクライバーがメッセージを受信する: メッセージを受信したサブスクライバーは、そのメッセージを処理します。

この仕組みは、パブリッシャーとサブスクライバーが直接通信するのではなく、Redisという第三者を介してメッセージを交換することで実現されます。これにより、両者は互いの存在を知る必要がなくなり、疎結合が実現されます。

Pub/Subとキュー/メッセージブローカーの違い

Pub/Subはメッセージングのパターンの一つですが、同じメッセージングシステムでも「キュー」とは性質が異なります。

  • キュー (Queue): メッセージは通常、複数のコンシューマーのうちいずれか一つによって消費されます(Work Queueモデル)。メッセージは一度消費されるとキューから削除されるため、基本的に一つのメッセージが複数のコンシューマーに処理されることはありません。メッセージには順序性や永続性があることが一般的です。RabbitMQのQueue、KafkaのTopic/Partition(コンシューマーグループ使用時)、AWS SQSなどがこれに該当します。
  • Pub/Sub: メッセージは、そのチャンネルを購読しているすべてのサブスクライバーに配信されます(Fan-outモデル)。メッセージは基本的に一度配信されるとサーバー側には保持されません(Redis Pub/Subの場合)。Redis Pub/Sub、KafkaのTopic(単独コンシューマー使用時)、AWS SNSなどがこれに該当します。

Redis Pub/Subは、メッセージの永続性や複雑な配信保証(例えば、メッセージが確実に一度だけ処理されるExactly-onceなど)を提供しません。これは、Pub/Subがインメモリで動作し、シンプルさと高速性を追求しているためです。後述しますが、Redis Pub/Subは「Fire-and-Forget」型のメッセージングであり、接続が切れたサブスクライバーはメッセージを見逃します。

対照的に、RabbitMQやKafkaのような専用のメッセージブローカーは、より堅牢な永続性、配信保証、複雑なルーティング、コンシューマーグループなどの機能を提供します。Redis Pub/Subは、これらのフル機能のメッセージブローカーと比較すると機能は限定的ですが、Redisを既に利用しているシステムにおいては、シンプルで高速なメッセージ配信を実現する手段として非常に強力です。

Redis Pub/Subの特性

Redis Pub/Subの重要な特性をまとめます。

  • インメモリ: すべての操作がRedisサーバーのメモリ上で行われます。これにより非常に高速なメッセージ配信が可能ですが、サーバーがクラッシュしたり再起動したりすると、処理中のメッセージや購読情報は失われます(ディスクに永続化されるのは通常データのみで、Pub/Subの状態は永続化されません)。
  • Fire-and-Forget: パブリッシャーはメッセージを送信したら、それがサブスクライバーに正常に配信されたかどうかを確認できません。サーバーも、サブスクライバーがメッセージを受信したかどうかの確認応答(ACK)を行いません。
  • At-most-once配信: Redisサーバーはメッセージを最大で一度配信しようとします。サブスクライバーの接続が安定していればメッセージは配信されますが、メッセージ配信時にサブスクライバーがオフラインだったり、処理に時間がかかったりすると、そのメッセージは失われ、後から再配信されることはありません。
  • No Persistence: Redis Pub/Subで配信されたメッセージは、サーバー側には保持されません。メッセージはリアルタイムでサブスクライバーにプッシュされるだけで、過去のメッセージを後から取得することはできません。
  • Blocking Operation (Subscriber): サブスクライバーがSUBSCRIBEPSUBSCRIBEコマンドを発行すると、そのRedis接続はPub/Subモードに入り、他のコマンドを受け付けなくなります。メッセージが到着すると、接続を通じて非同期にプッシュされます。購読を解除するまでは、その接続でPub/Sub関連以外のコマンド(例: GET, SET)を実行することはできません。Pub/Subと通常のRedisコマンドを同じクライアント接続で行うことはできません。

これらの特性を理解することは、Redis Pub/Subを適切に利用するために非常に重要です。高い信頼性やメッセージの永続性が必要な場合は、Redis Streamsや他のメッセージキューシステムを検討する必要があります。

Redis Pub/Subの基本的な使い方 (CLI)

Redis Pub/Subの使い方は非常にシンプルです。主に以下の3つのコマンドを使用します。

  • SUBSCRIBE: チャンネルを購読する
  • PUBLISH: チャンネルにメッセージを送信する
  • UNSUBSCRIBE: チャンネルの購読を解除する

Redis CLIを使ってこれらのコマンドの挙動を見てみましょう。

まず、ターミナルを複数開き、それぞれでRedis CLIを起動します。Pub/Subでは複数のクライアントが同時に動作するため、複数のCLIセッションを用意すると動作が分かりやすいです。

SUBSCRIBE コマンド

SUBSCRIBEコマンドは、指定したチャンネルを購読するために使用します。

使い方:

bash
SUBSCRIBE channel [channel ...]

複数のチャンネルを一度に購読することも可能です。

例:

新しいターミナルでRedis CLIを起動し、mychannelというチャンネルを購読してみましょう。

bash
redis-cli
SUBSCRIBE mychannel

このコマンドを実行すると、CLIは待機状態に入ります。これは、SUBSCRIBEコマンドがブロッキング操作だからです。Redisサーバーからのメッセージを待ち受ける状態になります。

サーバーからの応答は、以下のような形式で表示されます。

Reading messages... (press Ctrl+C to quit)
1) "subscribe" # 応答タイプ(購読完了)
2) "mychannel" # 購読したチャンネル名
3) (integer) 1 # 現在購読中のチャンネル数

ここで表示されるメッセージは、Redisサーバーから非同期にプッシュされるものです。通常のコマンド応答とは形式が異なります。最初の要素はメッセージのタイプ(例: "subscribe", "message")、2番目の要素はチャンネル名またはパターン名、3番目の要素はメッセージの内容(または購読チャンネル数など、タイプによる)です。

複数のチャンネルへの登録:

一度に複数のチャンネルを購読することもできます。

bash
SUBSCRIBE channel1 channel2 channel3

例:

bash
redis-cli
SUBSCRIBE news.sports chat.general

応答はチャンネルごとに順番に表示されます。

Reading messages... (press Ctrl+C to quit)
1) "subscribe"
2) "news.sports"
3) (integer) 1
1) "subscribe"
2) "chat.general"
3) (integer) 2

このサブスクライバーは、news.sportschat.generalの両方からのメッセージを受信するようになります。

SUBSCRIBE実行時の注意点 (Blocking operation):

前述の通り、SUBSCRIBEコマンド(およびPSUBSCRIBE)を実行したRedis接続は、Pub/Subモードに入り、他の標準的なRedisコマンド(GET, SET, KEYSなど)を受け付けなくなります。この接続は、UNSUBSCRIBEまたはPUNSUBSCRIBEによって全てのチャンネル/パターンの購読を解除するか、接続が切断されるまでこの状態を維持します。

したがって、アプリケーションでPub/Subを利用する場合、Pub/Sub用の接続と、通常のRedisコマンド用の接続は別に用意する必要があります。一つのクライアントライブラリインスタンスや接続で両方の操作を同時に行うことはできません。

PUBLISH コマンド

PUBLISHコマンドは、指定したチャンネルにメッセージを送信するために使用します。

使い方:

bash
PUBLISH channel message

例:

別のターミナルでRedis CLIを起動し、メッセージを送信してみましょう。先ほどmychannelを購読したCLIセッションとは別のセッションを使います。

bash
redis-cli
PUBLISH mychannel "Hello, subscribers!"

このコマンドを実行すると、Redisサーバーはmychannelを購読しているすべてのサブスクライバーにメッセージを配信します。PUBLISHコマンドの戻り値は、そのメッセージが配信されたサブスクライバーの数です。

(integer) 1

上記の例では、mychannelを購読しているサブスクライバーが1人(先ほどSUBSCRIBEしたCLIセッション)いたことを示しています。もし誰も購読していなければ、戻り値は0になります。パブリッシャーは、この戻り値を見ても、誰にメッセージが配信されたのか、あるいは配信が成功したのかどうかを知ることはできません。単に「何人に配信しようとしたか」が分かるだけです。

購読している側のCLIセッションを見ると、以下のようなメッセージを受信しているはずです。

1) "message" # 応答タイプ(メッセージ受信)
2) "mychannel" # メッセージが届いたチャンネル名
3) "Hello, subscribers!" # メッセージの内容

別のチャンネルにもメッセージを送信してみましょう。

bash
PUBLISH anotherchannel "This message won't be received by the first subscriber."

最初のサブスクライバーはmychannelしか購読していないため、このメッセージは受信しません。PUBLISHの戻り値は、anotherchannelを購読しているサブスクライバーの数になります。もし誰も購読していなければ0です。

複数のサブスクライバーが同じチャンネルを購読している場合、PUBLISHの戻り値はそれらのサブスクライバーの合計数になります。

例えば、3つ目のCLIセッションで再度SUBSCRIBE mychannelを実行し、合計2つのセッションがmychannelを購読している状態で、パブリッシャーからPUBLISH mychannel "Another message"を実行すると、戻り値は(integer) 2となり、両方のサブスクライバーセッションがメッセージを受信します。

UNSUBSCRIBE コマンド

UNSUBSCRIBEコマンドは、購読しているチャンネルの購読を解除するために使用します。

使い方:

bash
UNSUBSCRIBE [channel ...]

SUBSCRIBEと同様に、複数のチャンネル名を指定して一度に解除できます。チャンネル名を何も指定しない場合、その接続で購読している全てのチャンネルの購読が解除されます。

例:

SUBSCRIBE mychannel chat.generalを実行したサブスクライバーセッションで、chat.generalのみ購読解除してみましょう。

bash
UNSUBSCRIBE chat.general

応答は以下のようになります。

1) "unsubscribe" # 応答タイプ(購読解除完了)
2) "chat.general" # 購読解除したチャンネル名
3) (integer) 1 # 現在購読中のチャンネル数(`mychannel`が残っているため1)

これで、このサブスクライバーはmychannelからのメッセージのみを受信するようになります。

次に、残りのmychannelも解除してみましょう。

bash
UNSUBSCRIBE mychannel

応答:

1) "unsubscribe"
2) "mychannel"
3) (integer) 0 # 現在購読中のチャンネル数(全て解除されたため0)

これで、このサブスクライバーはどのチャンネルも購読していない状態に戻ります。購読中のチャンネルが全て無くなると、その接続はPub/Subモードを終了し、再び通常のRedisコマンドを受け付けられるように見えます。しかし、多くのクライアントライブラリの実装では、Pub/Subモードに入った接続はPub/Sub専用として扱うのが一般的です。CLIの場合も、SUBSCRIBEでPub/Subモードに入ると、UNSUBSCRIBEで購読数が0になっても、通常のコマンド応答形式には戻らず、Reading messages...の状態は継続します(ただし、もうメッセージは届かない)。完全にモードを抜けるには、接続を閉じる(CLIならCtrl+C)必要があります。

全チャンネルからの解除:

引数を指定せずにUNSUBSCRIBEを実行すると、その接続で購読している全てのチャンネルから購読解除されます。

bash
UNSUBSCRIBE

応答:

1) "unsubscribe"
2) "news.sports"
3) (integer) 1
1) "unsubscribe"
2) "chat.general"
3) (integer) 0

これは、購読していたチャンネルがnews.sportschat.generalだった場合の例です。購読していたチャンネルごとに購読解除の応答が返され、最後に残りの購読数(0)が返されます。

Redis Pub/Subの高度な使い方 (CLI)

Pub/Subには、チャンネル名にパターンマッチングを使用して複数のチャンネルをまとめて購読する機能があります。これは、動的に生成されるチャンネル名や、特定のカテゴリに属する全てのチャンネルを購読したい場合に非常に便利です。

パターンマッチングによる購読 (PSUBSCRIBE)

PSUBSCRIBEコマンドは、ワイルドカードを含むパターン文字列を指定して、そのパターンにマッチする全てのチャンネルを購読するために使用します。

使い方:

bash
PSUBSCRIBE pattern [pattern ...]

複数のパターンを一度に購読することも可能です。

ワイルドカード (*) の使い方:

パターン文字列では、アスタリスク (*) がワイルドカードとして使用できます。*は、ゼロ個以上の任意の文字にマッチします。

例:

news.*というパターンを購読してみましょう。これは、news.で始まる全てのチャンネル(例: news.sports, news.politics, news.technologyなど)にマッチします。

新しいターミナルでRedis CLIを起動し、news.*パターンを購読します。

bash
redis-cli
PSUBSCRIBE news.*

SUBSCRIBEと同様に、CLIは待機状態に入ります。応答は以下のようになります。

Reading messages... (press Ctrl+C to quit)
1) "psubscribe" # 応答タイプ(パターン購読完了)
2) "news.*" # 購読したパターン文字列
3) (integer) 1 # 現在購読中のパターン数

ここで、別のターミナルからメッセージを送信してみましょう。

bash
redis-cli
PUBLISH news.sports "Arsenal won the match!"
PUBLISH news.politics "Election results are coming in."
PUBLISH news.random "This message will also be received."
PUBLISH updates.user.123 "User 123 profile updated."

パターンnews.*を購読しているサブスクライバーセッションには、以下のメッセージが届きます。

1) "pmessage" # 応答タイプ(パターンマッチングによるメッセージ受信)
2) "news.*" # マッチしたパターン文字列
3) "news.sports" # メッセージが送信された実際のチャンネル名
4) "Arsenal won the match!" # メッセージの内容
1) "pmessage"
2) "news.*"
3) "news.politics"
4) "Election results are coming in."
1) "pmessage"
2) "news.*"
3) "news.random"
4) "This message will also be received."

最後のメッセージupdates.user.123はパターンnews.*にマッチしないため、このサブスクライバーには届きません。

PSUBSCRIBEで受信するメッセージの形式は、SUBSCRIBEで受信する場合と少し異なります。タイプが"pmessage"になり、パターン文字列と実際のチャンネル名の両方が含まれます。

複数のパターンによる登録:

複数のパターンを一度に購読することもできます。

bash
PSUBSCRIBE news.* updates.*

応答:

Reading messages... (press Ctrl+C to quit)
1) "psubscribe"
2) "news.*"
3) (integer) 1
1) "psubscribe"
2) "updates.*"
3) (integer) 2

このサブスクライバーは、news.で始まるチャンネルとupdates.で始まるチャンネルの両方からのメッセージを受信するようになります。

パターンとチャンネルの同時購読:

一つの接続で、SUBSCRIBEによるチャンネル購読とPSUBSCRIBEによるパターン購読を同時に行うことも可能です。メッセージが到着すると、そのメッセージが通常のチャンネル購読にマッチした場合はタイプ"message"として、パターン購読にマッチした場合はタイプ"pmessage"として配信されます。もし一つのメッセージが通常のチャンネル購読とパターン購読の両方にマッチした場合、そのメッセージは両方の形式で二重に配信されます。

PUNSUBSCRIBE コマンド

PUNSUBSCRIBEコマンドは、購読しているパターンの購読を解除するために使用します。

使い方:

bash
PUNSUBSCRIBE [pattern ...]

複数のパターン名を指定して一度に解除できます。パターン名を何も指定しない場合、その接続で購読している全てのパターンの購読が解除されます。

例:

PSUBSCRIBE news.* updates.*を実行したサブスクライバーセッションで、updates.*のみ購読解除してみましょう。

bash
PUNSUBSCRIBE updates.*

応答:

1) "punsubscribe" # 応答タイプ(パターン購読解除完了)
2) "updates.*" # 購読解除したパターン文字列
3) (integer) 1 # 現在購読中のパターン数(`news.*`が残っているため1)

次に、残りのnews.*も解除してみましょう。

bash
PUNSUBSCRIBE news.*

応答:

1) "punsubscribe"
2) "news.*"
3) (integer) 0 # 現在購読中のパターン数(全て解除されたため0)

これで、このサブスクライバーはどのパターンも購読していない状態に戻ります。購読中のチャンネルとパターンが全て無くなると、その接続はPub/Subモードを終了しますが、CLIでは待機状態が継続します(メッセージは来ない)。

全パターンからの解除:

引数を指定せずにPUNSUBSCRIBEを実行すると、その接続で購読している全てのパターンから購読解除されます。

bash
PUNSUBSCRIBE

PUBSUB コマンド (Redis 2.8以降)

PUBSUBコマンドは、Redis Pub/Subシステムに関する情報を取得するために使用します。これはデバッグや監視に役立ちます。PUBSUBコマンド自体はPub/Subモードではない通常の接続から実行できます。

使い方:

bash
PUBSUB <subcommand> [argument [argument ...]]

主なサブコマンド:

  • PUBSUB CHANNELS [pattern]: 現在アクティブな(つまり、少なくとも1つのサブスクライバーが購読している)チャンネルのリストを返します。パターンを指定すると、そのパターンにマッチするチャンネルのみを返します。
    例:
    bash
    redis-cli
    PUBSUB CHANNELS # 全てのアクティブチャンネル
    PUBSUB CHANNELS news.* # news. で始まるアクティブチャンネル

    購読しているサブスクライバーが0になったチャンネルはリストに含まれません。また、PSUBSCRIBEによる購読だけでは、そのパターンにマッチするチャンネルが実際にPUBLISHされるまでPUBSUB CHANNELSには表示されないことに注意が必要です。
  • PUBSUB NUMSUB [channel ...]: 指定したチャンネルを購読しているサブスクライバーの数を返します。複数のチャンネル名を指定できます。
    例:
    bash
    redis-cli
    PUBSUB NUMSUB mychannel news.sports # mychannelとnews.sportsの購読者数

    戻り値は、チャンネル名とその購読者数のペアのリストになります。SUBSCRIBEによる購読数のみをカウントし、PSUBSCRIBEによる購読数はカウントしないことに注意が必要です。
  • PUBSUB NUMPAT: 現在サーバーによって購読されているパターンの総数を返します。これは、アクティブなチャンネルの購読ではなく、PSUBSCRIBEコマンドによって登録されたパターン自体の数をカウントします。
    例:
    bash
    redis-cli
    PUBSUB NUMPAT

    これはRedisサーバー全体でのパターン購読数を返します。特定のクライアントが購読しているパターン数ではありません。

これらのPUBSUBサブコマンドは、現在のPub/Subシステムの状況を把握するのに役立ちますが、完全な情報を提供するわけではないこと(例: NUMSUBがパターン購読者を含まないなど)を理解しておくことが重要です。

Redis Pub/Subをプログラムから利用する

実際のアプリケーションでは、ほとんどの場合プログラミング言語を使ってRedis Pub/Subを利用します。主要な言語のRedisクライアントライブラリは、Pub/Sub機能をサポートしています。基本的な考え方はCLIと同じですが、プログラミング言語の非同期処理の仕組みに合わせて実装する必要があります。

各言語ごとのライブラリ紹介

主要な言語で使用される一般的なRedisクライアントライブラリと、Pub/Sub関連機能のサポート状況は以下の通りです(全てのライブラリを網羅するわけではありません)。

  • Python: redis-py (非常に一般的で高機能)
  • Node.js: ioredis (高機能、Promiseベース/async/await対応), node-redis (公式クライアント)
  • Ruby: redis (標準的なクライアント)
  • PHP: phpredis (C拡張), predis (Composerライブラリ)
  • Java: Jedis (シンプル), Lettuce (Nettyベース、非同期高機能)
  • Go: go-redis (高機能、Goらしいインターフェース)

これらのライブラリはそれぞれ異なるAPIを持っていますが、Pub/Subに関しては大体同じパターンに従います。通常、Pub/Sub操作専用のクライアントインスタンス(または接続)を作成し、そこで購読を行い、メッセージを待ち受ける非同期処理を実装します。メッセージを受信すると、ライブラリは登録されたコールバック関数やイベントハンドラを呼び出します。

Python (redis-py) での実装例

redis-pyを使ったPub/Subの実装例を見てみましょう。Pub/Subモードに入った接続はブロッキングされるため、購読は通常、別のスレッドやプロセスで行います。

まず、redis-pyライブラリをインストールします。

bash
pip install redis

Subscribe側の実装例:

“`python
import redis
import threading
import time

Redisサーバーに接続

decode_responses=True にすると、受信メッセージがバイトではなく文字列になる

r = redis.StrictRedis(host=’localhost’, port=6379, db=0, decode_responses=True)

Pub/Subオブジェクトを作成(この接続がPub/Sub専用になる)

p = r.pubsub()

メッセージ受信時に呼び出されるコールバック関数

def handler(message):
“””
Pub/Subメッセージを受信したときに実行される関数
message ディクショナリの主なキー:
– ‘type’: ‘subscribe’, ‘unsubscribe’, ‘message’, ‘psubscribe’, ‘punsubscribe’, ‘pmessage’
– ‘channel’: メッセージが届いたチャンネル名 (subscribe/unsubscribe/message タイプの場合)
– ‘pattern’: マッチしたパターン文字列 (psubscribe/punsubscribe/pmessage タイプの場合)
– ‘data’: メッセージの内容 (message/pmessage タイプの場合) または購読数など (subscribe/unsubscribe/psubscribe/punsubscribe タイプの場合)
“””
print(f”Received: {message}”)
# 購読解除メッセージの場合は処理を終了する例
if message[‘type’] == ‘message’ and message[‘data’] == ‘QUIT’:
print(“Quit message received, unsubscribing…”)
p.unsubscribe() # この購読解除は、後述のlisten()メソッドからの戻り値で処理される
# p.punsubscribe() # パターン購読もしている場合はこちらも必要
return True # listen()にTrueを返すとループを抜ける
return False # listen()にFalseを返すとループを継続

購読を開始

通常のチャンネル購読

p.subscribe(**{‘mychannel’: handler, ‘chat.general’: handler})

パターン購読も追加する場合

p.psubscribe(*{‘news.‘: handler})

print(“Subscribing to channels…”)

別スレッドでメッセージ受信を待ち受ける

listen()メソッドはブロッキングされるため、通常は別スレッドで実行

thread = p.run_in_thread(sleep_time=0.01) # listen()を内部で実行し、指定されたsleep_timeでポーリング

print(“Waiting for messages…”)

メッセージ受信スレッドが動作している間、メインスレッドは他の処理を行うか、

ここで待機することも可能(例: ユーザー入力や他のイベントを待つ)

簡単のため、ここでは一定時間待機するだけ

try:
# スレッドが終了するまで待機 (ここでは無限ループだが、QUITメッセージ受信でlisten()が止まる)
thread.join()
except KeyboardInterrupt:
print(“Keyboard interrupt received, shutting down subscriber…”)
# Ctrl+Cなどで終了する場合、スレッドに停止を指示
p.unsubscribe() # スレッド内で処理される
# p.punsubscribe() # パターン購読もしている場合
# スレッドが終了するまで少し待つ
thread.stop()
thread.join(timeout=5) # 最大5秒待つ
print(“Subscriber shut down.”)

print(“Subscriber thread finished.”)

注: run_in_thread は内部で listen() を呼び出し、指定された sleep_time でポーリングします。

listen() 自体を直接使う場合は、別のスレッドを自分で管理する必要があります。

“`

Publish側の実装例:

Publish側はPub/Subモードに入らないため、通常のRedisコマンドと同じ接続を使用できます。

“`python
import redis
import time

Redisサーバーに接続

r = redis.StrictRedis(host=’localhost’, port=6379, db=0, decode_responses=True)

print(“Publishing messages…”)

mychannelにメッセージを送信

subscribers_count = r.publish(‘mychannel’, ‘Hello from publisher!’)
print(f”Published to mychannel. Received by {subscribers_count} subscribers.”)

time.sleep(1)

chat.generalにメッセージを送信

subscribers_count = r.publish(‘chat.general’, ‘Welcome to the chat!’)
print(f”Published to chat.general. Received by {subscribers_count} subscribers.”)

time.sleep(1)

news.sportsにメッセージを送信 (news.*パターン購読者が受信)

subscribers_count = r.publish(‘news.sports’, ‘Go team!’)
print(f”Published to news.sports. Received by {subscribers_count} subscribers.”)

time.sleep(1)

購読者終了用のQUITメッセージを送信

Subscribe側のハンドラで QUIT メッセージを受け取ったら購読解除するように実装している場合

print(“Sending QUIT message to mychannel…”)
subscribers_count = r.publish(‘mychannel’, ‘QUIT’)
print(f”Published QUIT to mychannel. Received by {subscribers_count} subscribers.”)

print(“Publisher finished.”)
“`

Pub/Subのサブスクライバーはメッセージ受信待ちでブロッキングされるため、redis-pyではrun_in_threadメソッドを提供しており、これを使うと簡単に別スレッドでメッセージ受信ループを実行できます。あるいは、listen()メソッドを自分で呼び出し、それを別のスレッドで実行することも可能です。listen()はメッセージが到着するたびにコールバック関数を呼び出し、コールバックがTrueを返すとループを終了します。

Node.js (ioredis) での実装例

ioredisを使ったNode.jsでのPub/Sub実装例を見てみましょう。Node.jsは非同期I/Oが得意なので、スレッドを使わずにイベントハンドラとして実装するのが一般的です。

まず、ioredisライブラリをインストールします。

bash
npm install ioredis

Subscribe側の実装例:

Node.jsでは、Pub/Sub用の接続と通常コマンド用の接続を分ける必要があります。

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

// Pub/Sub用のクライアント接続を作成
// subscribe(), psubscribe() などの購読コマンドはこのクライアントで行う
const subscriber = new Redis();

// 通常コマンド用のクライアント接続を作成
// get(), set(), publish() などのコマンドはこのクライアントで行う(今回は未使用だが例として)
// const publisher = new Redis();

// 購読が成功したときのイベントハンドラ
subscriber.on(‘subscribe’, (channel, count) => {
console.log(Subscribed to channel ${channel}. Currently subscribed channels: ${count});
});

// パターン購読が成功したときのイベントハンドラ
subscriber.on(‘psubscribe’, (pattern, count) => {
console.log(Subscribed to pattern ${pattern}. Currently subscribed patterns: ${count});
});

// メッセージを受信したときのイベントハンドラ (SUBSCRIBEによる購読)
subscriber.on(‘message’, (channel, message) => {
console.log(Received message from channel ${channel}: ${message});
// 特定のメッセージで購読解除する例
if (message === ‘QUIT’) {
console.log(Received QUIT message on channel ${channel}. Unsubscribing...);
subscriber.unsubscribe(channel); // 特定のチャンネルを購読解除
// subscriber.punsubscribe(‘news.*’); // パターン購読も解除する場合
// subscriber.quit(); // 全て購読解除したら接続を閉じるなど
}
});

// パターンマッチングによるメッセージを受信したときのイベントハンドラ (PSUBSCRIBEによる購読)
subscriber.on(‘pmessage’, (pattern, channel, message) => {
console.log(Received message from pattern ${pattern}, channel ${channel}: ${message});
});

// 購読解除が成功したときのイベントハンドラ
subscriber.on(‘unsubscribe’, (channel, count) => {
console.log(Unsubscribed from channel ${channel}. Remaining subscribed channels: ${count});
if (count === 0) {
console.log(“No more channels subscribed.”);
// 全てのチャンネル/パターンから購読解除されたら終了するなどの処理
// 例: process.exit();
}
});

// パターン購読解除が成功したときのイベントハンドラ
subscriber.on(‘punsubscribe’, (pattern, count) => {
console.log(Unsubscribed from pattern ${pattern}. Remaining subscribed patterns: ${count});
if (count === 0) {
// 全てのチャンネル/パターンから購読解除されたら終了するなどの処理
console.log(“No more patterns subscribed.”);
// 例: process.exit();
}
});

// エラーハンドリング
subscriber.on(‘error’, (err) => {
console.error(‘Redis Subscriber Error:’, err);
});

console.log(“Subscribing to channels and patterns…”);

// チャンネルを購読
subscriber.subscribe(‘mychannel’, ‘chat.general’);

// パターンを購読
subscriber.psubscribe(‘news.*’);

// この後、プログラムはメッセージを受信するまで待機します。
// Node.jsのイベントループのおかげで、ブロッキングせずに待機できます。

// 必要に応じて、一定時間後に購読解除するなどのロジックを追加
// setTimeout(() => {
// console.log(“Auto unsubscribing from mychannel…”);
// subscriber.unsubscribe(‘mychannel’);
// }, 10000); // 10秒後に購読解除
“`

Publish側の実装例:

Publish側は通常のRedisコマンドを使用するため、別のクライアントインスタンスを使用するのが安全です。

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

// Publisher用のクライアント接続を作成
const publisher = new Redis();

async function publishMessages() {
console.log(“Publishing messages…”);

// mychannelにメッセージを送信
let subscribersCount = await publisher.publish('mychannel', 'Hello from publisher (Node.js)!');
console.log(`Published to mychannel. Received by ${subscribersCount} subscribers.`);

await new Promise(resolve => setTimeout(resolve, 1000));

// chat.generalにメッセージを送信
subscribersCount = await publisher.publish('chat.general', 'Node.js welcomes you!');
console.log(`Published to chat.general. Received by ${subscribersCount} subscribers.`);

await new Promise(resolve => setTimeout(resolve, 1000));

// news.sportsにメッセージを送信 (news.*パターン購読者が受信)
subscribersCount = await publisher.publish('news.sports', 'Node.js team scored!');
console.log(`Published to news.sports. Received by ${subscribersCount} subscribers.`);

await new Promise(resolve => setTimeout(resolve, 1000));

// 購読者終了用のQUITメッセージを送信
console.log("Sending QUIT message to mychannel...");
subscribersCount = await publisher.publish('mychannel', 'QUIT');
console.log(`Published QUIT to mychannel. Received by ${subscribersCount} subscribers.`);

console.log("Publisher finished.");
// publisher.quit(); // 終了時に接続を閉じる

}

publishMessages().catch(err => {
console.error(‘Publisher Error:’, err);
// publisher.quit();
});
“`

Node.jsでは非同期処理が基本となるため、ioredisのようなPromiseやasync/awaitに対応したライブラリを使うと直感的に実装できます。Pub/Subのイベントは、on()メソッドでイベントハンドラを登録することで処理します。

その他の言語でのポイント

他の言語(Java, Go, Ruby, PHPなど)でも基本的な考え方は同じです。

  • Pub/Sub専用の接続: Pub/Subの購読は接続をブロッキングするため、購読用の接続と通常コマンド用の接続は分ける必要があります。
  • 非同期処理: メッセージの受信は非同期で行われます。
    • Javaであれば、ExecutorServiceを使ったスレッドプールで購読ループを実行したり、Nettyベースの非同期ライブラリ(Lettuceなど)を利用します。
    • Goであれば、ゴルーチンを使ってPub/SubクライアントのListenメソッドを実行します。
    • RubyやPHPでも、メッセージ受信を待ち受けるループをバックグラウンドプロセスやスレッド(もしサポートされていれば)で実行し、受信時にコールバックを呼び出す形式が一般的です。
  • エラーハンドリングと再接続: Pub/Sub接続が切断されると、購読情報とメッセージは失われます。安定したシステムのためには、接続が切断された場合のリトライロジック(一定時間待ってから再接続し、再度購読を開始するなど)を実装することが重要です。

クライアントライブラリのドキュメントを参照し、各言語・ライブラリの推奨されるPub/Sub実装パターンを確認してください。

Redis Pub/Subの内部動作と注意点

Redis Pub/Subはシンプルで高速ですが、その仕組みゆえの制限や注意点があります。これらを理解せず利用すると、予期しない問題に遭遇する可能性があります。

メッセージの配信方法(Fan-out)

Redis Pub/Subでは、パブリッシャーからチャンネルに送信されたメッセージは、そのチャンネルを購読している全てのアクティブなサブスクライバーに対してコピーされ、それぞれに配信されます。これは「Fan-out(扇形配信)」と呼ばれます。

Redisサーバーは、購読している各クライアント接続に対して、メッセージをソケット経由でプッシュします。サーバー側では、各サブスクライバーのバッファにメッセージを一時的に保持し、順次クライアントに送信しようとします。

メッセージの永続性について(永続化されない -> Fire-and-forget)

Redis Pub/Subは、基本的にメッセージを永続化しません。パブリッシャーがメッセージを送信し、Redisサーバーがそれを受け取ると、その時点で購読しているサブスクライバーにのみ配信されます。メッセージはサーバーのメモリ上に一時的に保持されますが、配信が完了するか、サブスクライバーの接続が切断されると破棄されます。

これは、Pub/Subがリアルタイムなイベント通知などに特化しており、高速な配信を優先しているためです。メッセージがディスクに書き込まれたり、永続的なキューに格納されたりすることはありません。

この特性から、「Fire-and-forget」型のメッセージングと呼ばれます。パブリッシャーはメッセージを発行したら、サーバーがそれを引き受けた時点で役割を終えます。メッセージが実際にサブスクライバーに届いたかどうか、あるいは何人のサブスクライバーが受信したかは、パブリッシャーからは基本的に保証も確認もできません(PUBLISHコマンドの戻り値で「配信を試みたサブスクライバーの数」は分かりますが、成功の保証ではありません)。

メッセージの配信保証について(At-most-once配信)

Redis Pub/Subは、At-most-once配信を保証します。これは「メッセージは最大で一度配信される」という意味です。

  • メッセージロスが発生するケース:
    • メッセージがチャンネルに発行された時点で、そのチャンネルを購読しているサブスクライバーが一人もいない場合、メッセージはそのまますぐに失われます。
    • サブスクライバーがRedisサーバーに接続していない間に発行されたメッセージは、後から接続しても受け取ることはできません。
    • サブスクライバーの接続が不安定で、メッセージの受信中に切断された場合、そのメッセージは失われる可能性があります。
    • 後述する「Slow Subscriber Problem」により、サブスクライバーの処理が追いつかず、サーバー側の出力バッファが満杯になった場合、サーバーはサブスクライバーの接続を強制的に切断します。このとき、バッファに残っていたメッセージは失われます。

Redis Pub/Subは、メッセージが「少なくとも一度配信される (At-least-once)」や「正確に一度配信される (Exactly-once)」といった、より強力な配信保証を提供しません。これらの保証が必要な場合は、メッセージの再送や重複排除の仕組みを持つ他のメッセージキューシステム(例: RabbitMQ, Kafka, Redis Streams)を検討する必要があります。

遅いサブスクライバーへの対応(Slow Subscriber Problem)

Redis Pub/Subの最も重要な注意点の一つが、Slow Subscriber Problemです。

Redisサーバーは、メッセージを購読している各クライアント接続の出力バッファに書き込みます。サブスクライバーがメッセージを迅速に読み取って処理していれば問題ありませんが、何らかの理由でサブスクライバーのメッセージ処理が遅延した場合、サーバー側の出力バッファに未送信のメッセージが溜まっていきます。

Redisサーバーは、各クライアント接続に対して出力バッファの上限を設定しています。これは設定ファイル(redis.conf)のclient-output-buffer-limitパラメータで調整可能です。Pub/Subクライアントのバッファ上限は、通常client-output-buffer-limit pubsub <hard limit> <soft limit> <soft seconds>のように設定されます。

  • <hard limit>: バッファサイズがこの上限を超えた場合、サーバーは即座にクライアント接続を強制的に切断します。
  • <soft limit><soft seconds>: バッファサイズが<soft limit>を超え、その状態が<soft seconds>秒間続いた場合、サーバーはクライアント接続を強制的に切断します。これは、一時的なスパイクではなく、持続的な遅延を検知するためです。

サブスクライバーの処理が遅くバッファ上限に達した場合、Redisサーバーはサブスクライバーを強制的に切断します。これにより、サーバー全体のメモリ使用量が制御され、他の高速なクライアントへの影響を防ぐことができます。しかし、切断されたサブスクライバーは、切断中に発行されたメッセージを全て見逃します

Slow Subscriber Problemの解決策の検討:

  1. サブスクライバーの処理を高速化する: メッセージ処理がCPUバウンドなら並列化、I/Oバウンドなら非同期I/Oを利用するなど、サブスクライバー側で処理性能を向上させます。
  2. バッファ上限を調整する: client-output-buffer-limit pubsubの設定値を増やすことで、より多くのメッセージをバッファリングできるようになります。ただし、これはサーバーのメモリ使用量を増やすため、無制限に増やすべきではありません。
  3. 重要なメッセージにはPub/Subを使わない: メッセージロスが許容できない場合は、信頼性の高い他のメッセージングシステム(Redis Streams、RabbitMQ、Kafkaなど)を検討します。
  4. Alternative (Redis Streams): Redis 5.0から導入されたStreamsは、メッセージの永続性、コンシューマーグループ、順序保証などを提供しており、Pub/Subの多くの制限を克服できます。メッセージロスが許容できないユースケースでは、Pub/SubではなくStreamsを検討することが強く推奨されます。

スケーラビリティ

Redis Pub/Subのスケーラビリティは、いくつかの要因に依存します。

  • チャンネル数: Redisサーバーは多数のチャンネルを同時に管理できます。チャンネル数自体が直接的なボトルネックになることは少ないです。
  • メッセージレート: Redisサーバーが処理できるメッセージの送信レートには限界があります。これはCPUやネットワーク帯域に依存します。非常に高いメッセージレートの場合、サーバーがボトルネックになる可能性があります。
  • サブスクライバー数: Pub/Subでは、一つのメッセージが全ての購読サブスクライバーにコピーされて配信されます。したがって、チャンネルあたりのサブスクライバー数が非常に多い場合、サーバーはメッセージのコピーと送信に多くのリソース(CPU、ネットワーク帯域、メモリバッファ)を消費します。これが大きなボトルネックになる可能性があります。特に、多くのサブスクライバーが多数のチャンネルやパターンを購読している構成では、サーバー負荷が増大します。
  • ネットワーク帯域: メッセージ量が多い場合やサブスクライバー数が多い場合、Redisサーバーとクライアント間のネットワーク帯域がボトルネックになる可能性があります。

Redis ClusterはPub/Subをサポートしていますが、Pub/Subメッセージはクラスター内の全てのノードにブロードキャストされるため、通常のキー操作のような線形的なスケーラビリティは期待できません。大規模なPub/Sub負荷を分散するには、メッセージング専用のRedisインスタンスを立てるか、メッセージングシステム自体をスケールアウト可能なもの(Kafkaなど)に移行することを検討する必要があります。

マスター・スレーブ構成とPub/Sub

Redisのマスター・スレーブ構成を使用している場合、Pub/Subに関しては以下の点に注意が必要です。

  • パブリッシュ操作: PUBLISHコマンドは書き込み操作ですが、レプリケーションキューには積まれません。マスターがメッセージを受け取ると、マスターはその時点で接続している全てのサブスクライバー(マスターに接続しているクライアントと、スレーブへのレプリケーションリンク自身)にメッセージを配信します。
  • スレーブからのサブスクライブ: クライアントはマスターだけでなく、スレーブにも接続してSUBSCRIBEPSUBSCRIBEを行うことができます。
  • メッセージの伝播: マスターにパブリッシュされたメッセージは、レプリケーションプロトコルの一部としてスレーブに伝達されます。スレーブは、マスターから受信したメッセージを、そのスレーブ自身に接続している全てのサブスクライバーに配信します。
  • 注意点: スレーブはマスターからのメッセージをリアルタイムで受け取りますが、ネットワーク遅延などにより、スレーブの受信がマスターよりわずかに遅れる可能性があります。したがって、マスターに接続したサブスクライバーとスレーブに接続したサブスクライバーが全く同じタイミングでメッセージを受信するとは限りません。また、マスターがフェイルオーバーして新しいマスターになった場合、Pub/Subの状態(購読情報)は永続化されないため引き継がれません。新しいマスターでは購読が失われた状態から始まります。

安定性や可用性の高いPub/Subシステムを構築する場合、これらの挙動を理解しておくことが重要です。

Pub/Subのユースケース

Redis Pub/Subはシンプルで高速なため、特定のユースケースにおいて非常に効果的です。

  1. リアルタイム通知(チャット、アクティビティフィードなど): ユーザーがオンラインでアプリケーションを使用している間に発生したイベントを即座に通知する場合に適しています。チャットアプリケーションでのメッセージ配信、新しいコメントや「いいね!」のアクティビティフィード更新、株式市場データのプッシュなどが含まれます。メッセージロスが多少発生してもユーザー体験に大きな影響がない、またはリアルタイム性が最優先される場合に有効です。
  2. イベントドリブンアーキテクチャの一部: マイクロサービスや分散システムにおいて、あるサービスで発生したイベントを複数の他のサービスにブロードキャストするシンプルな方法として利用できます。例えば、「ユーザー登録完了」イベントを認証サービス、メール送信サービス、アナリティクスサービスなどに通知する場合などです。ただし、各サービスがイベントを確実に処理する必要がある場合は、Redis Streamsや他の永続性を持つメッセージキューを検討すべきです。
  3. キャッシュ無効化のトリガー: 分散キャッシュを使用している複数のアプリケーションサーバーがある場合、いずれかのサーバーがキャッシュを更新または削除した際に、他のサーバーの該当キャッシュを無効化するトリガーとしてPub/Subを利用できます。「このキーのキャッシュが無効になった」というメッセージをチャンネルに発行し、各サーバーがそれを購読してキャッシュをクリアします。
  4. 分散システムにおけるシンプルなブロードキャスト: 複数インスタンスで実行されているアプリケーション全体に、設定変更や再起動指示などのコマンドをブロードキャストする場合など。システム管理用途の軽量なメッセージ配信に適しています。

Redis Pub/Subが適しているケース:

  • リアルタイム性が重要だが、メッセージロスが多少許容される場合。
  • メッセージの順序保証や永続性が厳密に必要ない場合。
  • インメモリの高速性を活かしたい場合。
  • Redisを既に利用しており、追加のミドルウェアを導入したくない場合。
  • シンプルなFire-and-forget型のブロードキャスト通信で十分な場合。

Redis Pub/Subが適していないケース:

  • メッセージロスが一切許容できない場合(例: 課金処理、在庫管理)。
  • メッセージの順序保証が必要な場合。
  • 過去のメッセージを後から参照する必要がある場合。
  • 複雑なルーティングやメッセージ変換が必要な場合。
  • コンシューマーグループによる負荷分散が必要な場合(メッセージを複数のワーカーで分担して処理したい場合)。
  • 非常に高いメッセージレートや非常に多数のサブスクライバーによる高負荷が予想される場合で、サーバーリソースが逼迫する可能性がある場合。

これらの要件がある場合は、Redis StreamsやKafka、RabbitMQ、ActiveMQ、AWS SQS/SNS、Google Cloud Pub/Subなどの他のメッセージング技術を検討する必要があります。

Pub/Subの代替/関連技術

Redis Pub/Subの限界を理解する上で、他の関連技術や代替技術を知ることは重要です。

Redis Streams

Redis 5.0で導入されたStreamsは、Pub/Subとキューの両方の特性を併せ持つ、より高機能なメッセージング機能です。

Redis Streamsの特徴(Pub/Subとの比較で重要):

  • 永続性: メッセージはディスクに永続化されます(AOFやRDBの設定による)。Redisが再起動してもメッセージは失われません。
  • 順序保証: ストリーム内のメッセージは追加された順序が保証され、コンシューマーはその順序でメッセージを読み取ることができます。
  • コンシューマーグループ: 複数のコンシューマーをグループ化し、ストリーム内のメッセージをグループ内で分担して処理させることができます。これにより負荷分散やフォールトトレランスを実現できます。
  • メッセージID: 各メッセージには一意のIDが付与され、コンシューマーは特定のIDから読み取りを開始したり、既読管理を行ったりできます。
  • Acknowledgement (ACK): コンシューマーはメッセージを正常に処理したことをACKでサーバーに通知できます。ACKされていないメッセージは、他のコンシューマーに再配信されたり、後から手動で処理されたりすることができます。
  • 履歴アクセス: コンシューマーは、特定のメッセージID以降のメッセージを読み取ったり、ストリームの先頭からメッセージを読み直したりすることができます。

Pub/Sub vs Streams:

特徴 Redis Pub/Sub Redis Streams
メッセージ永続性 なし(インメモリのみ) あり(設定による永続化)
配信保証 At-most-once(最大1回) At-least-once(少なくとも1回)
メッセージ順序 チャンネル内での順序は保証されるが、複数のチャンネル/パターン購読では保証されない ストリーム内での追加順序が保証される
コンシューマー 全てのサブスクライバーに配信(Fan-out) 単独コンシューマー or コンシューマーグループ
メッセージID なし あり(一意のID)
既読管理/ACK なし コンシューマーグループで可能
履歴アクセス 不可能(リアルタイム配信のみ) 可能(特定のIDから読み取りなど)
API PUBLISH, SUBSCRIBE, PSUBSCRIBEなど XADD, XREAD, XGROUP, XREADGROUPなど
複雑さ シンプル Pub/Subより高機能だが複雑
適したユースケース リアルタイム性重視、ロス許容、シンプルブロードキャスト 高信頼性、順序保証、タスクキュー、イベントソーシング

メッセージの永続性、信頼性、コンシューマーグループによる負荷分散が必要な場合は、Pub/SubではなくStreamsを検討すべきです。StreamsはPub/Subよりも強力な保証を提供しますが、その分APIは少し複雑になります。

他のメッセージキューシステム (RabbitMQ, Kafka, SQS, etc.)

RabbitMQ, Apache Kafka, AWS SQS/SNS, Google Cloud Pub/Subなど、Redis Pub/Sub以外にも多くのメッセージングシステムが存在します。それぞれ異なる設計思想と得意な領域を持っています。

  • RabbitMQ: 高度なルーティング機能(Exchangeの種類)、永続性、配信保証(ACK)、タスクキューなど、伝統的なエンタープライズメッセージングの機能が豊富です。複雑なルーティングが必要な場合や、信頼性の高いタスクキューとして適しています。
  • Apache Kafka: 分散型ストリーミングプラットフォームであり、非常に高いスループット、スケーラビリティ、永続性(ログとして保持)、コンシューマーグループによる負荷分散が特徴です。大量のメッセージをリアルタイムに近い速度で処理し、複数のアプリケーションで共有・再利用する場合(イベントソーシング、ログ収集、データパイプラインなど)に特に強力です。
  • AWS SQS (Simple Queue Service): マネージドな分散メッセージキューサービスです。シンプルで信頼性の高いメッセージキューを提供します。メッセージロスを防ぎ、At-least-once配信を保証します。タスクキューとして利用するのに適しています。
  • AWS SNS (Simple Notification Service): マネージドなPub/Subサービスです。様々なエンドポイント(SQSキュー、Lambda関数、HTTPエンドポイント、メール、SMSなど)にメッセージを Fan-out 配信できます。Redis Pub/Subよりも多様な配信先に対応し、AWSエコシステムとの連携が容易です。
  • Google Cloud Pub/Sub: マネージドなスケーラブルなメッセージングサービスです。永続性があり、At-least-once配信を保証します。Google Cloud Platformを利用している場合に有力な選択肢となります。

これらのシステムは、Redis Pub/Subと比較して、より高い信頼性、永続性、スケーラビリティ、豊富な機能を提供する一方で、通常はRedis Pub/Subほどの低遅延ではない場合が多く、Redisサーバー単体に比べて運用が複雑になることがあります。

どのメッセージング技術を選択するかは、必要な信頼性、永続性、スケーラビリティ、メッセージレート、機能、運用コストなどを総合的に考慮して決定する必要があります。Redis Pub/Subは、Redisを既に利用しており、リアルタイム性やシンプルさを優先し、メッセージロスが許容できるユースケースにおいて、非常に優れた選択肢となります。

まとめ

この記事では、Redis Pub/Sub機能について、その基本的な仕組みから応用、内部動作、注意点、そして他の技術との比較まで、幅広く徹底的に解説しました。

Redis Pub/Subの主要なポイント:

  • Publish/Subscribeモデルに基づき、パブリッシャーとサブスクライバーを疎結合で接続します。
  • PUBLISHコマンドでメッセージを送信し、SUBSCRIBE/PSUBSCRIBEコマンドでチャンネル/パターンを購読します。
  • 購読した接続はPub/Subモードに入り、他のコマンドは実行できません。Pub/Sub用と通常コマンド用は別の接続が必要です。
  • メッセージはインメモリで処理され、非常に高速ですが、永続性はありません(Fire-and-forget)。
  • 配信保証はAt-most-onceであり、接続が切れたり、サブスクライバーの処理が遅れたりするとメッセージは失われます(Slow Subscriber Problem)。
  • パターン購読(PSUBSCRIBE)は柔軟なメッセージルーティングを可能にします。
  • PUBSUBコマンドでPub/Subの状態をある程度確認できます。
  • リアルタイム通知、イベントブロードキャスト、キャッシュ無効化トリガーなど、メッセージロスが許容できるリアルタイムなユースケースに適しています。
  • メッセージの永続性や信頼性、コンシューマーグループが必要な場合は、Redis Streamsや他の専用メッセージキューシステムを検討すべきです。

Redis Pub/Subは、そのシンプルさとRedisの高速性によって、特定のシナリオで非常に強力なツールとなり得ます。しかし、メッセージロスや配信保証に関する特性を十分に理解し、アプリケーションの要件に合致するかどうかを慎重に判断することが重要です。

この記事を通じて、Redis Pub/Subの仕組みと使い方、そしてその限界について深く理解していただけたなら幸いです。

次のステップ

  • 実際にPub/Sub機能を使って簡単なアプリケーション(例: チャットアプリケーションのバックエンド通知部分)を開発してみる。
  • redis-pyioredisなど、使用している言語のRedisクライアントライブラリのPub/Sub関連ドキュメントを詳しく読む。
  • Slow Subscriber Problemを意図的に再現させ、バッファ上限の設定や挙動を確認する。
  • Redis Streamsについて学び、Pub/Subが適さないユースケースでStreamsがどのように役立つかを理解する。
  • RabbitMQやKafkaなど、他のメッセージングシステムについて学び、それぞれの特徴やRedis Pub/Subとの違いを比較検討する。

RedisはPub/Sub以外にも非常に多くの強力な機能を持っています。Pub/Subを入り口として、Redisの他の機能や、より広い分散システムのメッセージングパターンについても学びを深めていくことをお勧めします。

コメントする

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

上部へスクロール