[Redis] Redis 学習チュートリアル (10) Redis を使用してメッセージ キューを実装する

メッセージ キューが満たす必要がある要件:

  1. 一貫した順序: メッセージが送信される順序が、メッセージが消費される順序と一貫していることを確認します。一貫性がない場合、ビジネス エラーが発生する可能性があります。
  2. メッセージ確認メカニズム: 消費された (ACK が受信された) メッセージを再度消費することはできません
  3. メッセージの永続性: コンシューマーが予期せずシャットダウンし、再起動後に再度メッセージを消費する必要がある場合にメッセージを再度取得できるように、メッセージの損失を回避するために永続化する機能が必要です。

Redis には、メッセージ キューを実装するための 3 つの異なる方法が用意されています。

  1. リスト構造: リスト構造に基づいてメッセージ キューをシミュレートします。
  2. pubsub: ピアツーピア メッセージング モデル
  3. ストリーム: 比較的完全なメッセージ キュー モデル

1. リスト構造に基づく

list の基礎となる実装は「リンク リスト」であるため、先頭と末尾の操作要素の時間計算量は O(1) です。これは、メッセージ キュー モデルとの一貫性が非常に高いことを意味します。

ビジネス ニーズが十分に単純で、Redis をキューとして使用したい場合、最初に思い浮かぶのは、リスト データ型を使用することです。

よく使用されるコマンド:

  • lpush:発表する
  • rpop: メッセージをプルする
  • brpop: メッセージのプルをブロックします

プロデューサー:

ここに画像の説明を挿入します

消費者:

ここに画像の説明を挿入します

このモデルは、以下に示すように非常に単純です。

ここに画像の説明を挿入します

キューにメッセージがない場合、コンシューマは RPOP の実行時に NULL を返します。

ここに画像の説明を挿入します

コンシューマー ロジックを記述するとき、通常は「無限ループ」になります。このロジックは、処理のためにキューからメッセージを継続的にプルする必要があります。疑似コードは通常、次のように記述されます。

while true:
    msg = redis.rpop("queue")
    // 没有消息,继续循环
    if msg == null:
        continue
    // 处理消息
    handle(msg)

質問 1: この時点でキューが空の場合、コンシューマは依然としてメッセージを頻繁にプルするため、「CPU アイドリング」が発生し、CPU リソースが無駄になるだけでなく、Redis に負荷がかかります。

この問題を解決するにはどうすればよいでしょうか?

キューが空の場合は、しばらく「スリープ」して、メッセージを再度プルしてみることができます。コードは次のように変更できます。

while true:
    msg = redis.rpop("queue")
    // 没有消息,休眠2s
    if msg == null:
        sleep(2)
        continue
    // 处理消息        
    handle(msg)

これでCPUアイドリング問題が解決

質問 2: しかし、これは別の問題を引き起こします。コンシューマがスリープ状態で待機しているときに新しいメッセージが到着すると、コンシューマによる新しいメッセージの処理に「遅延」が発生します。

設定されたスリープ時間が 2 秒であると仮定すると、新しいメッセージには最大 2 秒の遅延が発生します。

この遅延を短縮するには、スリープ時間を短縮するしかありません。ただし、スリープ時間が短いほど、CPU アイドリングの問題が発生する可能性が高くなります。

Redis は、メッセージをプルするための「ブロック」コマンド BRPOP / BLPOP を提供しますここでの B はブロック (Block) を指します。

ここに画像の説明を挿入します
これで、次のようなメッセージをプルできるようになります。

while true:
    // 没消息阻塞等待,0表示不设置超时时间
    msg = redis.brpop("queue", 0)
    if msg == null:
        continue
    // 处理消息
    handle(msg)

BRPOP のブロッキング メソッドを使用してメッセージをプルする場合、「タイムアウト期間」の受け渡しもサポートされます。0 に設定すると、タイムアウトは設定されず、新しいメッセージが存在するまで戻りません。それ以外の場合は NULL指定されたタイムアウト期間の経過後に返されます。

注: タイムアウトの設定が長すぎて、接続が長期間アクティブでなかった場合、Redis Server によって無効な接続と判断される可能性があり、Redis Server はクライアントを強制的にオフラインにします。したがって、このソリューションを使用するには、クライアントに再接続メカニズムが必要です。

Jedis を使用して実装: https://blog.csdn.net/jam_yin/article/details/130967040

アドバンテージ:

  • JVM メモリ制限なしで Redis ストレージを利用
  • Redisの永続化メカニズムに基づいてデータのセキュリティが保証されます
  • メッセージの秩序性を満たすことができる

欠点:

  • 繰り返しの消費はサポートされていません: コンシューマがメッセージをプルした後、メッセージはリストから削除され、他のコンシューマが再度消費することはできません。つまり、複数のコンシューマが同じデータ バッチを消費することはサポートされていません。
  • メッセージ損失: コンシューマがメッセージをプルした後、異常なダウンタイムが発生すると、メッセージは失われます (メッセージがリストから POP された後、メッセージはリンク リストからすぐに削除されるためです。つまり、メッセージは失われます)リンクされたリストからすぐに削除されます。コンシューマーが正常に処理したかどうかに関係なく、このメッセージを再度使用することはできません)

2. Pub-Sub モデルに基づく

[Redis] Redis 学習チュートリアル (9) Pub の発行と Sub の購読

Redis には、パブリッシュおよびサブスクライブ操作を完了するための次のコマンドが用意されています。

  • SUBSCRIBE: 1 つ以上のチャンネルを購読します
  • UNSUBSCRIBE: 1 つ以上のチャンネルの登録を解除します
  • PSUBSCRIBE: 1 つ以上のスキーマをサブスクライブします
  • PUNSUBSCRIBE: 1 つ以上のパターンのサブスクライブを解除します

2.1 チャネル (Channel) を介した公開と購読

ここに画像の説明を挿入します
1. コンシューマのサブスクリプションキュー

SUBSCRIBE コマンドを使用して 2 つのコンシューマを開始し、同じキューに「サブスクライブ」します

ここに画像の説明を挿入します

現時点では、両方のコンシューマーがブロックされ、新しいメッセージの到着を待機します。

2. プロデューサーがニュースを公開

ここに画像の説明を挿入します

3. コンシューマはブロックを解除してメッセージを受信します

ここに画像の説明を挿入します

Pub/Sub ソリューションを使用すると、メッセージのプルのブロックをサポートするだけでなく、同じデータ バッチを消費する複数のコンシューマー グループのビジネス ニーズも満たします。

2.2 パターン マッチングを使用してパブリッシュとサブスクライブを実装する

ここに画像の説明を挿入します

1. コンシューマのサブスクリプションキュー

コンシューマは queue.* 関連のキュー メッセージをサブスクライブします

ここに画像の説明を挿入します

2. プロデューサーがニュースを公開

プロデューサはメッセージをそれぞれ queue.p1 と queue.p2 にパブリッシュします。

ここに画像の説明を挿入します

3. コンシューマはブロックを解除してメッセージを受信します

コンシューマはこれら 2 つのプロデューサからメッセージを受信します

ここに画像の説明を挿入します

Pub/Sub の最大の利点は、プロデューサーとコンシューマーの複数のグループによるメッセージの処理をサポートしていることですが、最大の問題はデータ損失です。

次のシナリオが発生した場合、データ損失が発生する可能性があります。

  • オフラインの消費者
  • Redis がダウンしています
  • メッセージが溜まっていく

Pub/Sub は実装が非常に簡単です。いかなるデータ型にも基づいておらず、データ ストレージも行いません。単にプロデューサーとコンシューマー向けの「データ転送チャネル」を確立し、ルールに準拠したデータを 1 つのチャネルから転送します。端からもう一方へ、もう一方の端へ

完全なパブリッシュおよびサブスクライブのメッセージ処理プロセスは次のとおりです。

  1. コンシューマが指定されたキューにサブスクライブすると、Redis はマッピング関係を記録します: キュー -> コンシューマ
  2. プロデューサはこのキューにメッセージをパブリッシュし、Redis はマッピング関係から対応するコンシューマを見つけて、そこにメッセージを転送します。

ここに画像の説明を挿入します

プロセス全体を通じて、データ ストレージはなく、すべてがリアルタイムで転送されます。

この設計ソリューションでは、前述の問題が発生します。たとえば、コンシューマが異常に電話を切った場合、オンラインに戻った後にのみ新しいメッセージを受信できます。オフライン期間中にプロデューサによってリリースされたメッセージは、見つからないため見つかりません。消費者に届かなければ廃棄されてしまいます。すべてのコンシューマがオフラインの場合、コンシューマが見つからないため、プロデューサによってリリースされたすべてのメッセージは「破棄」されます。

それで、Pub/Sub を使用する場合は、プロデューサーがメッセージをパブリッシュする前に、まずコンシューマーがキューにサブスクライブする必要があることに注意してください。そうしないと、メッセージが失われます。Pub/Sub 関連の操作は RDB および AOF に書き込まれません。Redis がクラッシュして再起動すると、すべての Pub/Sub データが失われます。

「メッセージ バックログ」を処理するときに Pub/Sub でもデータが失われるのはなぜですか?

コンシューマーの速度がプロデューサーに追いつかないと、データのバックログが発生します。

リストがキューとして使用される場合、メッセージがバックログされると、リンク リストは非常に長くなります。最も直接的な影響は、コンシューマーがリンク リストからすべてのデータを取り出すまで、Redis メモリが増大し続けることです。

ただし、Pub/Sub の処理は異なり、メッセージがバックログされると、消費の失敗やメッセージの損失が発生する可能性があります。

Pub/Sub の実装の詳細から: 各コンシューマがキューにサブスクライブすると、Redis はサーバー上のコンシューマに「バッファ」を割り当てます。このバッファは実際にはメモリの一部です。プロデューサがメッセージをパブリッシュすると、Redis はまずコンシューマに対応するバッファにメッセージを書き込みます。その後、コンシューマは引き続きバッファからメッセージを読み取り、メッセージを処理します。

ここに画像の説明を挿入します
ただし、問題はこのバッファにあります。

このバッファーには実際には「上限」(構成可能) があるため、コンシューマーがメッセージをプルするのが遅いと、プロデューサーによってバッファーにパブリッシュされたメッセージのバックログが発生し、バッファー メモリが増加し続けます。バッファー構成の上限を超えると、Redis はコンシューマーを「強制的に」オフラインにします。この時点で、消費者はデータを消費できず、データが失われます。

このバッファのデフォルト構成は、Redis 構成ファイルから確認できます。client-output-buffer-limit pubsub 32mb 8mb 60

  • 32mb: バッファーが 32MB を超えると、Redis はコンシューマーを直接強制的にオフラインにします。
  • 8mb + 60: バッファーが 8MB を超えて 60 秒間続く場合、Redis はコンシューマーをオフラインにします。

Pub/Sub のこの機能はリスト キューとはまったく異なります。リストは実際には「プル」モデルに属しますが、Pub/Sub は実際には「プッシュ」モデルに属します。

  • リスト内のデータは常にメモリに蓄積でき、消費者はいつでもそれを「引き出す」ことができます。
  • Pub/Sub は、まず Redis サーバー上のコンシューマのバッファにメッセージを「プッシュ」し、コンシューマがメッセージを取得するのを待ちます。生成速度と消費速度が一致しない場合、バッファ内のメモリが拡張され始めますが、バッファの上限を制御するために、Redis にはコンシューマを強制的にオフラインにするメカニズムがあります。

アドバンテージ:

  1. パブリッシュ/サブスクライブをサポートし、メッセージを処理するプロデューサーとコンシューマーの複数のグループをサポートします。

欠点:

  1. 消費者がオフラインになるとデータが失われます
  2. データの永続化はサポートされていないため、Redis がダウンするとデータが失われます。
  3. メッセージが蓄積され、バッファ オーバーフローが発生し、コンシューマは強制的にオフラインになり、データが失われます。

3. ストリームベースのメッセージキュー

Redis の開発中に、Redis の作成者はオープンソース プロジェクト ディスクも開発しました。このプロジェクトの位置付けは、メモリベースの分散メッセージ キュー ミドルウェアです。しかし、さまざまな理由から、このプロジェクトは停滞しています。最後に、Redis 5.0 バージョンでは、作成者は disque 関数を Redis に移植し、その新しいデータ型を定義しました。

ストリームは本質的に Redis のキーであり、関連する命令は、メッセージ キュー関連の命令とコンシューマ グループ関連の命令の 2 つのカテゴリに分類できます。

メッセージキュー関連の手順:

コマンド名 コマンド機能
XADD メッセージをキューの最後に追加します
XREAD メッセージ (ブロッキング/非ブロッキング) を取得し、指定された ID より大きいメッセージを返します
XLEN Stream内のメッセージ長を取得する
XDEL メッセージを削除する
エックスレンジ メッセージのリストを取得(範囲指定可能)、削除されたメッセージを無視
XREVRANGE XRANGE と比較した場合の違いは、ID が大きいものから小さいものまで、逆引き検索にあります。
エクストリム ストリームの長さを制限します。長すぎるとインターセプトされます。

消費者グループ関連の指示:

コマンド名 コマンド機能
Xグループの作成 消費者グループの作成
XREADGROUP コンシューマ グループのメッセージを読む
ザック ack メッセージ、メッセージは「処理済み」としてマークされます
XグループセットID コンシューマ グループによって配信された最後のメッセージの ID を設定します
XGROUP デルコンシューマー コンシューマ・グループの削除
支出中 保留中のメッセージの詳細を出力する
XCLAIM メッセージの所有権を譲渡します (処理されなかったメッセージまたは長期間処理できなかったメッセージは、処理のために他のコンシューマ グループに譲渡されます)
XINFO Stream\Consumer\Groupの詳細情報を出力する
XINFOグループ 消費者グループの詳細を印刷する
XINFOストリーム ストリームの詳細情報を印刷する

3.1 XREADコマンドによるメッセージの読み取り

コマンドは次のとおりです。

  • XADD:アナウンスをします。XADD key [NOMKSTREAM] [MAXLEN|MINID [= | ~] threshold [LIMIT count]] *|ID field value [field value ...]
    • [NOMKSTREAM]: キューが存在しない場合にキューを自動的に作成するかどうか。デフォルトは
    • [MAXLEN|MINID [= | ~] しきい値 [LIMIT count]]: メッセージ キュー内のメッセージの最大数を設定します
    • |ID: メッセージの一意の ID。Redis によって自動的に生成されたものを表します。形式: タイムスタンプの増加する数値
    • フィールド値 [フィールド値...]: キューに送信されるメッセージ エントリ。形式はキーと値です。

例: mystream という名前のキューを作成し、Redis の増分 ID を使用してメッセージ {"name": "zzc", "age": 26} をキューに送信します。

xadd mystream * name zzc age 26
  • XREAD:メッセージを読みます。XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key...] ID [ID ...]
    • [COUNT カウント]: 1 回あたりに読み取られるメッセージの最大数
    • [BLOCK ミリ秒]: メッセージがない場合、ブロックするかどうかとブロック期間
    • STREAMS key [key...]: どのキューからメッセージを読み取るか、key はキュー名です
    • ID [ID …]: 開始 ID。この ID より大きいメッセージのみが返されます。0: 最初のメッセージから開始; $: 最新のメッセージから開始

例: mystream という名前のキューから最新のメッセージを一度に 1 メッセージずつ読み取ります

XREAD COUNT 1 BLOCK 0 STREAMS mystream $

プロデューサー:

ここに画像の説明を挿入します

消費者:

ここに画像の説明を挿入します

3.2 コンシューマ・グループ・コマンドによるメッセージの読み取り

コンシューマグループ: 複数のコンシューマをグループに分けて同じキューを監視するもので、以下の特徴があります。

  • メッセージのオフロード: キュー内のメッセージは、繰り返し消費されるのではなく、グループ内の異なるコンシューマにオフロードされるため、メッセージの処理が高速化されます。
  • メッセージ ID: コンシューマ グループは、最後に処理されたメッセージを記録する ID を維持します。コンシューマーがクラッシュして再起動した場合でも、メッセージはマークから読み取られ、すべてのメッセージが確実に消費されます。
  • メッセージの確認: コンシューマがメッセージを読んだ後、メッセージは保留状態になり、保留リストに保存されます。処理が完了したら、メッセージは保留リストから削除される前に、ACK によって確認され、処理済みとしてマークされる必要があります。

コマンドは次のとおりです。

  • XGROUP CREATE:コンシューマ グループを作成します。XGROUP CREATE key groupName ID|$ [NOMKSTREAM]
    • キー: キュー名
    • groupName: コンシューマ グループ名
    • ID: 開始 ID 識別。0: 最初のメッセージ; $: 最新のメッセージから
    • NOMKSTREAM: キューが存在しない場合にキューを自動的に作成するかどうか。デフォルトは

コンシューマ グループの作成: コンシューマ グループ mystreamGroup をキュー mystream に作成し、最初のメッセージから読み取りを開始します。

XGROUP CREATE mystream mystreamGroup 0
  • XREADGROUP: コンシューマ グループからのメッセージを読み取ります。XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds] [NOACK] STREAMS key [key ...] ID [ID ...]
    • グループ: コンシューマ グループ名
    • Consumer: コンシューマ名。コンシューマ名が存在しない場合は、コンシューマが自動的に作成されます。
    • count: このクエリの最大数
    • ミリ秒: メッセージがない場合の最大待機時間
    • NOACK: 手動 ACK は必要ありません。メッセージを取得すると自動的に確認されます。
    • STREAMSキー:キュー名を指定します。
    • ID: メッセージの開始 ID を取得します。">": 次の未消費メッセージから開始します (通常の状況では推奨); その他: 指定された ID に従って保留リストから消費済みだが未確認のメッセージを取得します。例: 0 は保留リストの最初のメッセージから始まります。

コンシューマ c1 は、キュー mystream 内のコンシューマ グループ mystreamGroup からメッセージを読み取りますが、2000 ミリ秒以内に読み取って返すことができません。

XREADGROUP GROUP mystreamGroup c1 COUNT 1 BLOCK 2000 STREAMS mystream

その他のコマンド:

// 删除指定的消费者组
XGROUP DESTROY key groupName
// 给指定的消费者组添加消费者
XGROUP CREATECONSUMER key groupName consumername
// 删除消费者组中指定的消费者
XGROUP DELCONSUMER key groupName consumername

プロデューサー:

プロデューサーは 2 つのメッセージを送信します。

ここに画像の説明を挿入します

コンシューマ グループを作成します。

2 つのコンシューマ グループが同じデータ バッチを処理できるようにするには、2 つのコンシューマ グループを作成する必要があります。0-0: メッセージを最初からプルします

ここに画像の説明を挿入します

消費者:

コンシューマ グループを作成した後、各「コンシューマ グループ」に「コンシューマ」をアタッチして、それぞれが同じデータ バッチを処理できるようにします。

最初の消費者グループは次のものを消費し始めます。

ここに画像の説明を挿入します

2 番目の消費者グループは次のものを消費し始めます。

ここに画像の説明を挿入します

これら 2 つのコンシューマ グループが、処理するデータの同じバッチを取得できることがわかります。このようにして、複数の消費者グループによる「サブスクリプション」消費の目的が達成されます。

ここに画像の説明を挿入します

3.2.1 メッセージ処理中に例外が発生する ストリームは、メッセージが失われないようにし、再度利用できるようにします。

コンシューマがメッセージを消費したが、それが正常に処理されなかった場合 (たとえば、コンシューマ プロセスがクラッシュした場合)、グループ内の他のコンシューマがメッセージを再度消費できないため、メッセージが失われる可能性があります。

コンシューマーのグループがメッセージを処理した後、XACK コマンドを実行して Redis に通知する必要があります。この時点で、Redis はメッセージに「処理が完了しました」というマークを付けます。

  • XPENDING: メッセージの読み取り中、グループでの処理中にコンシューマがクラッシュすることによって引き起こされるメッセージ損失の問題を解決するために、Stream は、読み取られたが確認されていないメッセージを記録する保留リストを設計しました。XPENDING key group [start end count] [consumer]
    • キー: キュー名
    • グループ: コンシューマ グループ名
    • start: 開始値。-: 最小値
    • end: 終了値。+:最大値
    • カウント数量
  • XACK: 読み取られたが処理されていないメッセージの場合は、XACK to complete コマンドを使用して、メッセージの処理が完了したことを通知します。XACK コマンドは、消費された情報を確認し、情報が確認されて処理されると、情報の処理が完了したことを意味します。XACK key group ID [ID ...]

消費されたが処理されていない (ACK されていない) クエリ メッセージ:

ここに画像の説明を挿入します

ACK メッセージ:

ここに画像の説明を挿入します

コンシューマーが異常にクラッシュした場合、XACK は確実に送信されず、Redis はこのメッセージを保持したままになります。

このコンシューマのグループがオンラインに戻った後、Redis は正常に処理されなかったデータをこのコンシューマに再送信します。これにより、消費者に異常が発生してもデータが失われることはありません。

3.2.2 コードの実装

①:redis依存関係を導入する

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

②:構成

spring:
  redis:
    host: localhost
    port: 6379
    password:
    timeout: 2000s
    # 配置文件中添加 lettuce.pool 相关配置,则会使用到lettuce连接池
    lettuce:
      pool:
        max-active: 8  # 连接池最大连接数(使用负值表示没有限制) 默认为8
        max-wait: -1ms # 接池最大阻塞等待时间(使用负值表示没有限制) 默认为-1ms
        max-idle: 8    # 连接池中的最大空闲连接 默认为8
        min-idle: 0    # 连接池中的最小空闲连接 默认为 0
  main:
    allow-circular-references: true

redis:
  mq:
    streams:
      # key名称
      - name: redis:mq:streams:key1
        groups:
          # 消费者组名称
          - name: group1
            # 消费者名称
            consumers: group1-con1, group1-con2
      - name: redis:mq:streams:key2
        groups:
          - name: group2
            consumers: group2-con1, group2-con2
      - name: redis:mq:streams:key3
        groups:
          - name: group3
            consumers: group3-con1, group3-con2

キュー、コンシューマ グループ、およびコンシューマは、構成ファイルを通じて構成されます。

③:Redis設定クラス

@Slf4j
@Configuration
public class RedisConfig {
    
    

    @Resource
    private RedisMqProperties redisMqProperties;

    @Resource
    private RedisStreamUtil redisStreamUtil;

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    
    
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        // json 序列化配置
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 所有的 key 采用 string 的序列化
        template.setKeySerializer(stringRedisSerializer);
        // 所有的 value 采用 jackson 的序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 key 采用 string 的序列化
        template.setHashKeySerializer(stringRedisSerializer);
        // hash 的 value 采用 jackson 的序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory redisConnectionFactory, RedisMessageListener listener, MessageListenerAdapter adapter) {
    
    
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        // 设置连接工厂
        container.setConnectionFactory(redisConnectionFactory);
        // 所有的订阅消息,都需要在这里进行注册绑定,new PatternTopic("topic")表示发布的主题信息。可以添加多个 messageListener,配置不同的通道
        container.addMessageListener(listener, new PatternTopic("topic1"));
        container.addMessageListener(adapter, new PatternTopic("topic2"));
        // 设置序列化对象:① 发布的时候需要设置序列化;订阅方也需要设置序列化;② 设置序列化对象必须放在[加入消息监听器]这一步后面,否则会导致接收器接收不到消息
        Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        seria.setObjectMapper(objectMapper);
        container.setTopicSerializer(seria);
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(PrintMessageReceiver printMessageReceiver) {
    
    
        MessageListenerAdapter receiveMessage = new MessageListenerAdapter(printMessageReceiver, "receiveMessage");
        Jackson2JsonRedisSerializer seria = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        seria.setObjectMapper(objectMapper);
        receiveMessage.setSerializer(seria);
        return receiveMessage;
    }

    @Bean
    public List<Subscription> subscription(RedisConnectionFactory factory){
    
    
        List<Subscription> resultList = new ArrayList<>();
        AtomicInteger index = new AtomicInteger(1);
        int processors = Runtime.getRuntime().availableProcessors();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(processors, processors, 0, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(), r -> {
    
    
            Thread thread = new Thread(r);
            thread.setName("async-stream-consumer-" + index.getAndIncrement());
            thread.setDaemon(true);
            return thread;
        });
        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
                StreamMessageListenerContainer
                        .StreamMessageListenerContainerOptions
                        .builder()
                        // 一次最多获取多少条消息
                        .batchSize(5)
                        .executor(executor)
                        .pollTimeout(Duration.ofSeconds(1))
                        .errorHandler(throwable -> log.error("[MQ handler exception]" + throwable.getMessage()))
                        .build();
        for (RedisMqStream redisMqStream : redisMqProperties.getStreams()) {
    
    
            String streamName = redisMqStream.getName();
            RedisMqGroup redisMqGroup = redisMqStream.getGroups().get(0);

            initStream(streamName,redisMqGroup.getName());
            var listenerContainer = StreamMessageListenerContainer.create(factory,options);
            // 手动ask消息
            Subscription subscription = listenerContainer.receive(Consumer.from(redisMqGroup.getName(), redisMqGroup.getConsumers()[0]),
                    StreamOffset.create(streamName, ReadOffset.lastConsumed()), new ReportReadMqListener());
            // 自动ask消息
           /* Subscription subscription = listenerContainer.receiveAutoAck(Consumer.from(redisMqGroup.getName(), redisMqGroup.getConsumers()[0]),
                    StreamOffset.create(streamName, ReadOffset.lastConsumed()), new ReportReadMqListener());*/
            resultList.add(subscription);
            listenerContainer.start();
        }
        ReportReadMqListener.redisStreamUtil = redisStreamUtil;
        return resultList;
    }

    private void initStream(String key, String group) {
    
    
        boolean hasKey = redisStreamUtil.hasKey(key);
        if(!hasKey){
    
    
            Map<String,Object> map = new HashMap<>(1);
            map.put("field","value");
            //创建主题
            String result = redisStreamUtil.addMap(key, map);
            //创建消费组
            redisStreamUtil.createGroup(key, group);
            //将初始化的值删除掉
            redisStreamUtil.del(key, result);
            log.info("stream:{}-group:{} initialize success",key, group);
        }
    }

}

④:コンシューマグループの構成に対応したJavaクラス

RedisMqProperties: すべてのキュー

@Data
@Configuration
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "redis.mq")
public class RedisMqProperties {
    
    

    // 所有队列
    public List<RedisMqStream> streams;
    
}

RedisMqStream:キューカプセル化クラス

@Data
public class RedisMqStream {
    
    

    // 队列
    public String name;

    // 消费者组
    public List<RedisMqGroup> groups;

}

RedisMqGroup: 消費者団体

@Data
public class RedisMqGroup {
    
    

    // 消费者组名
    private String name;

    // 消费者
    private String[] consumers;
    
}

⑤:RedisStreamUtil:Streamを操作するためのツールクラス

@Component
public class RedisStreamUtil {
    
    

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 创建消费组
    public String createGroup(String key, String group){
    
    
        return redisTemplate.opsForStream().createGroup(key, group);
    }

    // 获取消费者信息
    public StreamInfo.XInfoConsumers queryConsumers(String key, String group){
    
    
        return redisTemplate.opsForStream().consumers(key, group);
    }

    public StreamInfo.XInfoGroups queryGroups(String key) {
    
    
        return redisTemplate.opsForStream().groups(key);
    }

    // 添加Map消息
    public String addMap(String key, Map<String, Object> value){
    
    
        return redisTemplate.opsForStream().add(key, value).getValue();
    }

   // 读取消息
    public List<MapRecord<String, Object, Object>> read(String key){
    
    
        return redisTemplate.opsForStream().read(StreamOffset.fromStart(key));
    }

    // 确认消费
    public Long ack(String key, String group, String... recordIds){
    
    
        return redisTemplate.opsForStream().acknowledge(key, group, recordIds);
    }

    // 删除消息。当一个节点的所有消息都被删除,那么该节点会自动销毁
    public Long del(String key, String... recordIds){
    
    
        return redisTemplate.opsForStream().delete(key, recordIds);
    }

    // 判断是否存在key
    public boolean hasKey(String key){
    
    
        Boolean aBoolean = redisTemplate.hasKey(key);
        return aBoolean != null && aBoolean;
    }
}

⑥:消費者

@Slf4j
@Component
public class ReportReadMqListener implements StreamListener<String, MapRecord<String, String, String>> {
    
    

    public static RedisStreamUtil redisStreamUtil;

    @Override
    public void onMessage(MapRecord<String, String, String> message) {
    
    
        // stream的key值
        String streamKey = message.getStream();
        //消息ID
        RecordId recordId = message.getId();
        //消息内容
        Map<String, String> msg = message.getValue();
        //TODO 处理逻辑

        log.info("【streamKey】= " + streamKey + ",【recordId】= " + recordId + ",【msg】=" + msg);
        //逻辑处理完成后,ack消息,删除消息,group为消费组名称
        StreamInfo.XInfoGroups xInfoGroups = redisStreamUtil.queryGroups(streamKey);
        xInfoGroups.forEach(xInfoGroup -> redisStreamUtil.ack(streamKey, xInfoGroup.groupName(), recordId.getValue()));
        redisStreamUtil.del(streamKey, recordId.getValue());
    }
}

⑦:ニュースの発行

@GetMapping("/testStream")
public String testStream() {
    
    
    HashMap<String, Object> message = new HashMap<>(2);
    message.put("body", "消息主题" );
    message.put("sendTime", "消息发送时间");
    String streamKey = "redis:mq:streams:key2";
    redisStreamUtil.addMap(streamKey, message);
    return "testStream";
}

4. まとめ

ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/sco5282/article/details/132904956