1. メッセージキュー
1.0 コースの紹介
1.1.MQ の関連概念
1.1.1.MQとは
MQ (メッセージ キュー: メッセージ キュー) は、文字通りキュー、FIFO 先入れ先出しですが、キューに格納される内容はメッセージであり、上流と下流にメッセージを送信するために使用されるクロスプロセス通信メカニズムでもあります。。インターネット アーキテクチャでは、MQ は非常に一般的なアップストリームおよびダウンストリームのメッセージ通信サービスです。MQ を使用した後は、アップストリーム メッセージの送信は MQ のみに依存する必要があり、他のサービスに依存する必要はありません。“逻辑解耦+物理解耦”
- 上流と下流のメッセージ送信: たとえば、QQ アカウントで、クラスメート A がクラスメート B にメッセージを送信し、その後、クラスメート A が上流、クラスメート B が下流になります。このメッセージ送信プロセスは、上流と下流にメッセージを送信します。
1.1.2. MQ を使用する理由
1. トラフィックのピーク削減
例えば、注文システムが最大10,000件の注文に対応できれば、通常期であれば十分な処理能力があり、通常期であれば注文後1秒で結果を返すことができます。ただし、繁忙期に 20,000 件の注文が発生すると、OS では対応できず、10,000 件を超える注文数を制限し、ユーザーが注文できないようにすることしかできません。メッセージキューをバッファとして使用することで、この制限を解除し、1 秒以内に行われた注文を処理時間内に分散させることができます。このとき、一部のユーザーは注文後 10 秒以上経過しても正常な注文操作を受信できない場合があります。でも、注文できないよりはいいし、一度の体験の方が良いです。
概要: 過剰な注文データはキューに入れられます。
- 利点: システムがダウンしない
- デメリット:速度が遅い
2. アプリケーションの分離:
電子商取引アプリケーションを例に挙げると、アプリケーションには注文システム、在庫システム、物流システム、支払いシステムが含まれます。ユーザーが注文を作成した後、在庫システム、物流システム、決済システムが連携して呼び出された場合、いずれかのサブシステムに障害が発生すると、注文動作が異常になります。メッセージ キュー ベースのアプローチに変換すると、システム間呼び出しの問題が大幅に軽減され、たとえば、物流システムの障害による修復には数分かかります。この数分間の間に、物流システムによって処理されるメモリがメッセージ キューにキャッシュされ、ユーザーの注文操作は正常に完了できます。物流システムが復旧した場合でも、注文情報の処理を継続できるため、中間注文者は物流システムの障害を感じることがなくなり、システムの可用性が向上します。
概要:
- ご使用の前に: 注文システムはサブシステムを直接呼び出しますので、サブシステムが異常になると注文システム全体が停止します。
- 使用後: 注文システムは、実行が完了した後でのみメッセージをキューに送信し、その後のタスクは、3 つの主要なサブシステムが完了するまで、キューによって順番に支払いシステム、在庫システム、物流システムに分散されます。実行プロセス中にいずれかのサブシステムで例外が発生すると、キューは配信が完了するまで続行するように監視します。
3. 非同期処理:
サービス間の一部の呼び出しは非同期です。たとえば、A が B を呼び出し、B の実行には長い時間がかかりますが、A は B がいつ完了するかを知る必要があります。以前は、一般的に 2 つの方法がありました。しばらくしてから B を呼び出します クエリ API クエリ。または、A がコールバック API を提供し、B が実行を完了した後、その API を呼び出して A にサービスを通知します。これら 2 つの方法はあまりエレガントではありません。メッセージ バスを使用すると、この問題は簡単に解決できます。A が B サービスを呼び出した後は、B が完了したというメッセージを監視するだけで済みます。B が処理を完了すると、B は MQ にメッセージを送信します。 、MQ はこのメッセージをサービス A に転送します。この方法では、サービス A はループ内で B のクエリ API を呼び出す必要がなく、コールバック API を提供する必要もありません。同様に、サービス B はこれらの操作を実行する必要はありません。サービス A は、非同期処理が成功したことを示すメッセージをタイムリーに取得することもできます。
シナリオ: A が B にメッセージを送信し、B の実行には長い時間がかかりますが、A は B がいつ実行を完了できるかを知る必要があります。
- 同期処理: 一定期間後に、A が B のクエリ API クエリを呼び出します。または、A がコールバック API を提供し、B が実行を完了した後、その API を呼び出して A にサービスを通知します。このプロセス A は永遠に待つ必要があり、他のことができません。
- 非同期処理: A がサービス B を呼び出した後は、B が処理を完了したというメッセージをリッスンするだけで済みます。B が処理を完了すると、メッセージが MQ に送信され、MQ はそのメッセージをサービス A に転送します。A はこのプロセス中ずっと待機する必要はなく、他のことを行うことができます。
1.1.3. MQ の分類
1. ActiveMQ ( 最先出现的MQ,比较老
)
利点: 10,000 レベルの単一マシンのスループット、ms レベルの適時性、高可用性、マスター/スレーブ アーキテクチャに基づく高可用性、低いメッセージ信頼性によるデータ損失の可能性が低い
欠点: 現在、公式コミュニティが ActiveMQ 5.x を保守していることはますます少なくなり、高スループットのシナリオではあまり使用されなくなりました。
Shang シリコンバレー公式ウェブサイトのビデオ: http://www.gulixueyuan.com/course/322
2. ビッグデータの切り札 Kafka
ビッグデータ分野のメッセージ送信といえば、Kafka は避けては通れません100 万レベルの TPSスループットで有名となり、急速に普及が進んでいるビッグデータ用メッセージミドルウェアです。これはビッグデータ分野の最愛の人となり、データの収集、送信、保存のプロセスにおいて決定的な役割を果たしています。LinkedIn、Uber、Twitter、Netflixなどの大手企業で採用されています。
利点: 優れたパフォーマンス、単一マシンの書き込み TPS は約 100 万エントリ/秒、最大の利点は高いスループットです。適時性と MS レベルの可用性は非常に高いです。Kafka は分散されています。1 つのデータのコピーが複数あります。数台のマシンがダウンしても、データの損失や可用性の低下はありません。コンシューマはプル メソッドを使用してメッセージを取得し、制御を通じて、すべてのメッセージが 1 回だけ消費されることを保証できます。優れたサードパーティの Kafka Web 管理インターフェイス Kafka-Manager があります。ログの分野では比較的成熟しており、多くの企業で使用されています。複数のオープンソース プロジェクト; 関数サポート: 関数は比較的単純で、主に単純な MQ 関数をサポートし、ビッグ データの分野でのリアルタイム コンピューティングとログ収集に大規模に使用されます。
短所: Kafka が単一マシン上に 64 を超えるキュー/パーティションを持つ場合、負荷は明らかに急増します。キューの数が増えると、負荷が高くなり、メッセージ送信の応答時間が長くなります。短いポーリングが使用される場合、実際の時間のパフォーマンスはポーリング間隔によって異なります。再試行は消費エラーに対してはサポートされていません。メッセージの順序はサポートされていますが、エージェントがダウンすると、メッセージの順序が狂い、コミュニティの更新が遅くなります。
3. RocketMQ
RocketMQ は Alibaba のオープンソース製品で、Java 言語で実装されており、設計時に Kafka を参照し、独自の改良を加えています。Alibaba では、注文、トランザクション、リチャージ、ストリーム コンピューティング、メッセージ プッシュ、ログ ストリーミング処理、binglog 配布、その他のシナリオで広く使用されています。
利点:単一マシンのスループット 100,000 レベル、非常に高い可用性、分散アーキテクチャ、メッセージ損失ゼロ、MQ 機能は比較的完全、分散、優れたスケーラビリティ、10 億レベルのメッセージ蓄積をサポートし、蓄積による影響を受けず、パフォーマンスの低下を引き起こします。ソースコードはjavaなので、自分でソースコードを読んで自社のMQをカスタマイズすることができます。
短所:サポートされているクライアント言語は多くありません(現在 Java と C++ ですが、C++ は未熟です)、コミュニティ活動は平均的で、JMS などのインターフェイスは MQ コアに実装されていません。一部のシステムの移行には多くのコード変更が必要です。
4. RabbitMQ は
2007 年にリリースされ、AMQP (Advanced Message Queuing Protocol) に基づいた再利用可能なエンタープライズ メッセージング システムであり、現在最も主流のメッセージ ミドルウェアの 1 つです。
利点: アーラン言語の高い同時実行特性により、パフォーマンスが向上し、スループットが 10,000 レベルに達し、MQ 関数は比較的完全で、堅牢で、安定しており、使いやすく、クロスプラットフォームで、複数の言語。 Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP など、AJAX をサポートし、完全なドキュメントが用意されており、オープンソースによって提供される管理インターフェイスは非常に優れており、使いやすいです。 、コミュニティが非常に活発で、更新頻度が非常に高い
公式サイト:https://www.rabbitmq.com/news.html
デメリット:製品版は有料で学習コストが高い
1.1.4.MQの選択
1. Kafka Kafka は、Pull モードに基づいてメッセージ消費を処理し、高いスループットを追求することが最大の特徴であり、当初の目的はログの収集と送信を使用することであり、大量のデータが発生するインターネット サービスの
データ収集ビジネスに適しています。データ。大企業には利用を推奨しますが、ログ収集機能があれば間違いなくkafkaが第一候補です。Shang Silicon Valley 公式ウェブサイト kafka ビデオリンク http://www.gulixueyuan.com/course/330/tasks
2. RocketMQ は金融インターネット分野
のために生まれました。高い信頼性要件が必要なシナリオ、特に電子商取引での注文控除やビジネスのピークカットでは、大量のトランザクションが流入すると、バックエンドが時間内に処理できない可能性があります。 . . 安定性の点では RoketMQ の方が信頼できる可能性があります。これらのビジネス シナリオは Alibaba Double 11 で何度もテストされています。ビジネスに上記の同時実行シナリオがある場合は、RocketMQ を選択することをお勧めします。3. RabbitMQ は、アーラン言語自体の同時実行性の利点、優れたパフォーマンスとマイクロ秒の適時性、および比較的高いコミュニティ活動を組み合わせています。管理インターフェイスは非常に使いやすく、データ量がそれほど大きくない場合、中小企業向けです。比較的機能が充実したRabbitMQを優先します。
1.2.ウサギMQ
1.2.1. RabbitMQの概念
RabbitMQ はメッセージング ミドルウェアです。メッセージを受け入れて転送します。これは、宅配便サイトと考えることができます。荷物を送りたいときは、宅配便ステーションに荷物を置き、宅配便業者が最終的に荷物を受取人に届けます。このロジックによれば、RabbitMQ は宅配便ステーションです。ステーション、宅配業者が速達の配達をお手伝いします。RabbitMQ と速達ステーションの主な違いは、RabbitMQ は速達メールを処理せず、メッセージ データを受信、保存、転送することです。
1.2.2. 4 つの中心的な概念
- MQ は交換とキューの 2 つの部分で構成されます
- スイッチ----》キュー: 1 対多
- キュー----》消費者: 1 対 1 (1 つの速達に 2 人の受取人を指定できないため、1 つのキューで複数の消費者に対応することはできません)
プロデューサー
データを生成し、メッセージを送信するプログラムがプロデューサーです。
スイッチ
スイッチはRabbitMQ の非常に重要なコンポーネントであり、一方ではプロデューサーからメッセージを受信し、他方ではメッセージをキューにプッシュします。スイッチは、受信したメッセージを処理する方法、これらのメッセージを特定のキューまたは複数のキューにプッシュするか、メッセージを破棄するかを正確に知っている必要があります。これはスイッチのタイプによって異なります。
列
キューはRabbitMQ によって内部的に使用されるデータ構造であり、メッセージは RabbitMQ とアプリケーションを通過しますが、メッセージはキューにのみ保存できます。キューはホストのメモリとディスクの制限によってのみ制限され、本質的には大きなメッセージ バッファになります。多くのプロデューサーはキューにメッセージを送信でき、多くのコンシューマーはキューからデータの受信を試みることができます。これがキューの使い方です
消費者
消費は受け取ることと同様の意味を持ちます。コンシューマはほとんどの場合、メッセージの受信を待機しているプログラムです。プロデューサ、コンシューマ、およびメッセージ ミドルウェアは同じマシン上にないことが多いことに注意してください。同じアプリケーションがプロデューサーとコンシューマーの両方になることができます。
1.2.3. RabbitMQ コア部分 (6 つの主要モード)
- シンプルモード
- 動作モード
- パブリッシュ/サブスクライブパターン
- ルーティングモード
- テーマモード
- 解除確認モード
1.2.4. 各用語の概要
- ブローカー: メッセージを受信および配布するアプリケーション。RabbitMQ サーバーはメッセージ ブローカーです。
- 仮想ホスト: マルチテナンシーとセキュリティ上の理由から設計されており、AMQP の基本コンポーネントは、ネットワーク内の名前空間の概念と同様の仮想グループに分割されます。複数の異なるユーザーが同じ RabbitMQ サーバーが提供するサービスを利用する場合、複数の vhost を分割し、各ユーザーが自分の vhost に Exchange/queue などを作成することができます。
- 接続: パブリッシャー/コンシューマーとブローカー間の TCP 接続
- チャネル(チャネル:メッセージを送信するためのチャネル):RabbitMQ にアクセスするたびに Connection を確立すると、TCP Connection 確立のオーバーヘッドが大きくなり、メッセージ量が多い場合には効率が悪くなります。チャネルは、接続内で確立される論理接続です。アプリケーションがマルチスレッドをサポートしている場合、通常、各スレッドは通信用に個別のチャネルを作成します。AMQP メソッドには、クライアントとメッセージ ブローカーがチャネルを識別できるようにチャネル ID が含まれているため、チャネルは次のようになります。完全に孤立した。チャネルは軽量の接続として、TCP 接続を確立する際のオペレーティング システムのオーバーヘッドを大幅に削減します。
- Exchange : メッセージはブローカーの最初のストップに到達し、分散ルールに従ってクエリ テーブルのルーティング キーと一致し、メッセージをキューに分散します。一般的に使用されるタイプは、ダイレクト (ポイントツーポイント)、トピック (パブリッシュ/サブスクライブ)、およびファンアウト (マルチキャスト) です。
- Queue : メッセージは最終的にここに送信され、コンシューマーが受け取るのを待ちます。
- バインディング: エクスチェンジとキュー間の仮想接続。バインディングにはルーティング キーを含めることができます。バインディング情報はエクスチェンジのクエリ テーブルに保存され、メッセージ配信の基礎として使用されます。
1.2.5.インストール
1) 公式サイトアドレス
https://www.rabbitmq.com/download.html
注: 企業は通常、仕事に Linux システムを使用します。
2) ファイルのアップロード
例証します:
-
方法 1: 新しい仮想マシンを作成し、詳細を表示します...
-
方法 2: 仮想マシンのクローンを作成します。詳細を表示...
ホスト名node1
と IP アドレスを次のように変更します。192.168.10.120
-
ディレクトリにアップロードします
/usr/local/software
(ソフトウェアがない場合は自分で作成する必要があります)
- ローカル ディレクトリに移動し、新しいソフトウェア ディレクトリを作成します
- 次に、xftp ツールを使用して、ファイルを対応するディレクトリにアップロードします。
- ローカル ディレクトリに移動し、新しいソフトウェア ディレクトリを作成します
3) インストールファイル(以下の順序でインストールします)
# 安装erlang环境
rpm -ivh erlang-21.3-1.el7.x86_64.rpm
# 安装依赖包
yum install socat -y
# 安装rabbitmq
rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm
ステップ:
- ソフトウェア ディレクトリに入ると、ファイルがアップロードされていることがわかります。接尾辞 el7 は、Linux7 バージョンがサポートされていることを示します。
- 順番にインストールする
4) よく使うコマンド(以下の順番で実行)
起動時に開始する RabbitMQ サービスを追加します。
chkconfig rabbitmq-server on
サービスを開始します。
/sbin/service rabbitmq-server start
サービスのステータスを確認します。
/sbin/service rabbitmq-server status
サービスを停止します (実行を選択):
# 停止服务
/sbin/service rabbitmq-server stop
Web 管理プラグインを有効にし、start コマンドを入力してサービスを再起動します。
注: 後で使用するため、RabbitMQ バックグラウンド管理インターフェイス (Web 管理プラグイン) をインストールできます。これはブラウザーからアクセスできますが、サービスをオフにする必要があります。
# 先开启 web 管理插件
rabbitmq-plugins enable rabbitmq_management
# 之后再重启服务
/sbin/service rabbitmq-server start
Windows デスクトップに移動し、ブラウザを開きます。デフォルトのアカウント パスワード (ゲスト) を使用して、アドレスhttp://192.168.10.120:15672/
(ホスト IP + ポート番号) にアクセスします。権限に問題があるため、正常にアクセスできません。ファイアウォールが機能していない可能性があります。閉まっている。
解決策: ファイアウォールをオフにするか、ポート番号を有効にします。
- まずファイアウォールのステータスを確認し、ファイアウォールがオンになっていることを確認します。
systemctl status firewalld
- ファイアウォールをオフにし、ファイアウォールをオフにした後にサービスを自動的に開始します。
systemctl stop firewalld
systemctl disable firewalld.service
問題: Windows ページを再度開き、アクセス用のアドレスを入力し、初期化されたアカウントとパスワードを入力するとguest
、ゲストにログイン権限がないというメッセージが表示されます。
解決策: アカウントを作成し、スーパー管理者権限を付与します。
5) 新しいユーザーを追加する
アカウントを作成します: ユーザー名、パスワード
rabbitmqctl add_user admin 123456
ユーザーロールの設定(管理者:管理者)
rabbitmqctl set_user_tags admin administrator
ユーザー権限の設定
set_permissions [-p <vhostpath>] <user> <conf> <write> <read>
ユーザー user_admin には、仮想ホスト /vhost1 内のすべてのリソースに対する構成、書き込み、および読み取りの権限があります。
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
現在のユーザーとロールを表示する
rabbitmqctl list_users
6) 管理者ユーザーを使用して再度ログインします
ユーザー名、パスワード: admin、123456
7) リセットコマンド
アプリケーションを閉じるコマンドは、
rabbitmqctl stop_app
クリアコマンドは
rabbitmqctl reset
再起動コマンドは
rabbitmqctl start_app
2.Hello World(シンプルモード)
チュートリアルのこの部分では、Java で 2 つのプログラムを作成します。単一のメッセージを送信するプロデューサーと、メッセージを受信して出力するコンシューマーです。Java API の詳細については後ほど説明します。
以下の図では、「P」はプロデューサー、「C」はコンシューマーです。中央のボックスはキューです。コンシューマに代わって RabbitMQ によって保持されるメッセージのバッファです。
注: ミドルウェア RabbitMQ が Linux システムにインストールされたので、メッセージが送信できるかどうかをテストするために Java プログラム (プロデューサーとコンシューマー) を作成するだけで済みます。
2.1. 依存性
Maven プロジェクト (ここではモジュールに置き換えられています) を作成し、依存関係を導入します。
<!--指定 jdk 编译版本-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!--rabbitmq 依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的一个依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
2.2.メッセージプロデューサー
package com.atguigu.rabbitmq.one;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 生产者:发消息
*/
public class Producer {
//队列名称
private final static String QUEUE_NAME = "hello";
//发消息
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂ip 连接RabbitMQ的队列
factory.setHost("192.168.10.120");
//用户名
factory.setUsername("admin");
//密码
factory.setPassword("123456");
//创建连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
//入门级测试,这里直接连接的是队列,没有连接交换机,用的是默认的交换机。
/**
* 生成一个队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//发消息
String message = "hello world"; //初次使用
/**
*发送一个消息
*1.发送到那个交换机 本次是入门级程序,没有考虑交换机,可以写空串
*2.路由的 key 是哪个 本次是队列的名称
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
}
}
メイン メソッド テストを実行します。実行が成功したことを示すプロンプト メッセージが表示されます。
バックグラウンド管理インターフェイスを表示します。
2.3.メッセージコンシューマ
package com.atguigu.rabbitmq.one;
import com.rabbitmq.client.*;
/**
* 消费者:接收消息的
*/
public class Consumer {
//队列名称
private final static String QUEUE_NAME = "hello";
//接收消息
public static void main(String[] args) throws Exception {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.10.120");//设置ip
factory.setUsername("admin");//设置用户名
factory.setPassword("123456");//设置密码
Connection connection = factory.newConnection();//创建连接
Channel channel = connection.createChannel();//通过连接创建信道
System.out.println("等待接收消息 ");
//推送的消息如何进行消费的接口回调 使用lambda表达式代替匿名内部类的写法
DeliverCallback deliverCallback = (consumerTag, message) -> {
//直接输出message参数是对象的地址值,通过方法获取message对象的消息体并转化为字符串输出。
String mes = new String(message.getBody());
System.out.println(mes);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消息消费被中断");
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
3.ワークキュー(ワークモード)
ワーク キュー (タスク キューとも呼ばれる) の主な考え方は、リソースを大量に消費するタスクをすぐに実行し、その完了を待たなくて済むようにすることです。代わりに、後で実行されるタスクをスケジュールします。タスクをメッセージとしてカプセル化し、キューに送信します。バックグラウンドで実行されているワーカー プロセスがタスクをポップアウトし、最終的にジョブを実行します。複数のワーカー スレッドがある場合、これらのワーカー スレッドはこれらのタスクを一緒に処理します。
- 要約:
- プロデューサが大量のメッセージをキューに送信する場合、1 つのワーカー スレッド (コンシューマ) だけでメッセージを 1 つずつ受信して処理するには遅すぎるため、複数のワーカー スレッドを使用してメッセージを同時に処理します。
- 従うべき原則:
- プロデューサによって送信されたメッセージは 1 回しか処理できないため、作業モードはメッセージのポーリング分布に従うことを特徴としています。つまり、ワーカー スレッドがメッセージを順番に処理します。
- 作業スレッド間には競合関係があり、作業スレッド 1 が同じメッセージを取得すると、他の作業スレッドはそれを取得できなくなります。
3.1. ローテーショントレーニング配信メッセージ
この場合、2 つのワーカー スレッドと 1 つのメッセージ送信スレッドを起動します。これら 2 つのワーカー スレッドがどのように動作するかを見てみましょう。
3.1.1. 抽出ツールクラス
package com.atguigu.rabbitmq.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* 此类为连接工厂创建信道的工具类
*/
public class RabbitMqUtils {
//得到一个连接的 channel
public static Channel getChannel() throws Exception{
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.10.120");
factory.setUsername("admin");
factory.setPassword("123456");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
return channel;
}
}
3.1.2. 2 つのワーカースレッド (コンシューマー) を開始する
- ワーカースレッドコード:
package com.atguigu.rabbitmq.two;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.CancelCallback;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 这是一个工作线程(相当于之前消费者)
*/
public class Worker01 {
//队列的名称
private static final String QUEUE_NAME="hello";
//接收消息
public static void main(String[] args) throws Exception {
//通过工具类获取信道
Channel channel = RabbitMqUtils.getChannel();
//消息的接收
DeliverCallback deliverCallback=(consumerTag,message)->{
String receivedMessage = new String(message.getBody());
System.out.println("接收到消息:"+receivedMessage);
};
//消息接收被取消时 执行下面的内容
CancelCallback cancelCallback=(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
};
System.out.println("C2等待接收消息...");
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(QUEUE_NAME,true,deliverCallback,cancelCallback);
}
}
- 注: 2 つのワーカー スレッドのコードはまったく同じであるため、コピーする必要はありません。アイデアツールで複数のスレッドを実行できるように設定できます。
- ステップ:
-
最初にコンシューマーを実行してから変更を加えると、現在実行されているプログラムを見つけることができます。
-
ワーカー スレッド 1 の実行ウィンドウ
-
複数の実行を実行するように実行を構成します。
-
次に、プロンプト メッセージをワーク キュー 2 のプロンプト メッセージに変更します。
-
「実行」をクリックすると、ワーカースレッド 2 のコードが実行されます。
-
3.1.3. 送信スレッド(プロデューサー)の開始
package com.atguigu.rabbitmq.two;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 生产者 发送大量的消息
*/
public class Task01 {
//队列的名称
private static final String QUEUE_NAME="hello";
//发送大量的消息
public static void main(String[] args) throws Exception {
//通过工具类创建信道
try(Channel channel= RabbitMqUtils.getChannel();) {
/**
* 生成一个队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//从控制台当中接受信息进行发送,之前是直接定义一个参数写死了
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){
//判断是否还有输入的数据,有才进行循环获取
String message = scanner.next();
/**
*发送一个消息
*1.发送到那个交换机 本次是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 本次是队列的名称
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("发送消息完成:"+message);
}
}
}
}
3.1.4.結果表示
プログラムの実行により、プロデューサが合計 4 つのメッセージを送信し、コンシューマ 1 とコンシューマ 2 が 2 つずつメッセージを受信し、順番に 1 つずつメッセージを受信したことがわかりました。
3.2.メッセージ応答
3.2.1. コンセプト
コンシューマがタスクを完了するまでに時間がかかる場合があります。コンシューマの 1 人が長いタスクに取り組んでいて、部分的にしか完了せず、突然ハングした場合はどうなるでしょうか。RabbitMQ はメッセージをコンシューマに配信するとすぐに、そのメッセージに削除のマークを付けます。この場合、消費者が突然電話を切ると、処理中のメッセージが失われます。コンシューマが受信できないため、その後のメッセージがコンシューマに送信されます。
送信プロセス中にメッセージが失われないようにするために、rabbitmq はメッセージ応答メカニズムを導入します。メッセージ応答は次のとおりです:コンシューマがメッセージを受信してメッセージを処理した後、コンシューマはそれを処理したことを Rabbitmq に伝え、rabbitmq は次のことを行うことができますメッセージを削除します。
メッセージ応答の分類:
- 自動応答
- 手動回答
3.2.2. 自動応答 (非推奨)
メッセージは、送信直後に正常に配信されたと見なされます。このモードでは、コンシューマ側で接続が発生するか、メッセージが送信される前にチャネルが閉じられるため、高スループットとデータ送信セキュリティの間のトレードオフが必要です。メッセージが受信されると、メッセージは失われます。もちろん、その一方で、このモデルのコンシューマ側は、配信されるメッセージの数を制限せずに、過負荷のメッセージを配信できます。もちろん、これにより、コンシューマ側が過剰に受信する可能性があります。これらのメッセージのバックログが最終的にメモリを使い果たし、最終的にこれらのコンシューマ スレッドがオペレーティング システムによって強制終了されるため、このモードは、コンシューマがこれらのメッセージを効率的に処理できる場合にのみ使用に適しています。一定の割合。
要約:
- 自動応答には欠点があり、使用する前に良好な環境を確保する必要があります。
- 自動応答はメッセージを受信すると、すぐにキューにメッセージが完了したことを伝えますが、実際には完了していません。後続のコードに問題があると、メッセージも失われます。
3.2.3. 手動による回答
1) 手動メッセージ応答方法
- A.Channel.basicAck (肯定的な確認用)
- RabbitMQ はすでにメッセージを認識しており、正常に処理されているため、メッセージを破棄できます。
- B.Channel.basicNack (陰性確認用)
- C.Channel.basicReject (陰性確認用)
- Channel.basicNackと比較してバッチ処理パラメータが1つ少ない(複数)
- メッセージを処理したくない場合は、メッセージを拒否して破棄してください。
2) 複数の説明
手動応答の利点は、バッチで応答でき、ネットワークの混雑を軽減できることです。
複数の真と偽は異なる意味を表します
- true は、チャネル上の未応答メッセージに対するバッチ応答を表します。
- たとえば、チャネル上にタグ 5、6、7、8 のメッセージがあり、現在のタグは 8 です。すると、この時点では
- これらの未応答メッセージ 5 から 8 は、メッセージ応答を受信したものとして認識されます。
- 上記と比較すると false (推奨)
- tag=8 のメッセージ 5、6、7 のみが応答されますが、これら 3 つのメッセージは依然として確認応答されません。
- 8 の処理後に 5 6 7 の処理を行うと、メッセージの損失が発生する可能性があるため、バッチ応答は一般的に推奨されません。
3) メッセージは自動的に再キューイングされます
何らかの理由でコンシューマが接続を失い (チャネルが閉じられている、接続が閉じられている、または TCP 接続が失われた)、メッセージに ACK が送信されなかった場合、RabbitMQ はメッセージが完全に処理されていないことを認識し、メッセージを再度キューに入れます。この時点で別の消費者がそれを処理できる場合、すぐに別の消費者に再配布されます。こうすることで、コンシューマーが時折死亡したとしても、メッセージが失われることはありません。
- 要約:
- メッセージがある時点で失われた場合、メッセージの整合性をどのように確認すればよいでしょうか? ?
- 回答: メッセージは自動的に再度キューに入れられます。
- キューはどのメッセージが切断されたかを検出すると、メッセージが失われないように、すぐにメッセージを保存し、次のコンシューマによる処理のためにキューに戻します。
4) メッセージ手動応答コード
注意: プロデューサとは何の関係もありません。プロデューサはメッセージをキューに送信することだけを担当し、ワーカー スレッドは応答する責任があります。問題はワーカー スレッドで発生するため、ワーカー スレッドのコードが完了するまで応答しないでください。ワーカー スレッドが実行されます。コードの実行が完了するまで待機し、手動で応答する必要があります。そのため、コードの変更はワーカー スレッドで行う必要があります。
デフォルトのメッセージは自動応答を使用するため、消費プロセス中にメッセージが失われないことを保証したい場合は、自動応答を手動応答に変更する必要があります。上記のコードに基づいて、コンシューマは次のコードを追加します。下が赤。
テストする新しいプロデューサー コードとコンシューマー コードを作成します。
- メッセージプロデューサー
package com.atguigu.rabbitmq.three;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 消息在手动应答时是不丢失、一旦丢失会自动放回队列中重新消费
*/
public class Task02 {
//队列名称
private static final String TASK_QUEUE_NAME = "ack_queue";
public static void main(String[] argv) throws Exception {
//通过工具类获取信道
Channel channel = RabbitMqUtils.getChannel();
//声明队列
channel.queueDeclare(TASK_QUEUE_NAME, false, false, false, null);
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
/**
*发送一个消息
*1.发送到那个交换机 本次是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 本次是队列的名称
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。中文汉字有可能是乱码,所以
* 如果发送的是中文,一般需要指定编码格式。
*/
channel.basicPublish("", TASK_QUEUE_NAME, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
消費者01
package com.atguigu.rabbitmq.three;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.atguigu.rabbitmq.utils.SleepUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消息在手动应答时是不丢失、一旦丢失会自动放回队列中重新消费
*/
public class Work03 {
//队列名称
private static final String ACK_QUEUE_NAME="ack_queue";
//接收消息
public static void main(String[] args) throws Exception {
//通过工具类获取信道
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C1 等待接收消息处理时间较短");
//接收消息
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String message= new String(delivery.getBody(),"utf-8");
//接收到消息后,沉睡1s
SleepUtils.sleep(1);
System.out.println("接收到消息:"+message);
/**
* 确认手动应答:
* 1.消息标记 tag:每一个消息都有一个唯一的标识,表示应答的是哪一个消息。
* 2.是否批量应答未应答消息 false:不批量应答信道中的消息 true:批量
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
//采用手动应答
boolean autoAck=false;
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(ACK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
消費者02
package com.atguigu.rabbitmq.three;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.atguigu.rabbitmq.utils.SleepUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消息在手动应答时是不丢失、一旦丢失会自动放回队列中重新消费
*/
public class Work04 {
//队列名称
private static final String ACK_QUEUE_NAME="ack_queue";
//接收消息
public static void main(String[] args) throws Exception {
//通过工具类获取信道
Channel channel = RabbitMqUtils.getChannel();
System.out.println("C2 等待接收消息处理时间较长");
//接收消息
DeliverCallback deliverCallback=(consumerTag, delivery)->{
String message= new String(delivery.getBody(),"utf-8");
//接收到消息后,沉睡30s
SleepUtils.sleep(30);
System.out.println("接收到消息:"+message);
/**
* 确认手动应答:
* 1.消息标记 tag:每一个消息都有一个唯一的标识,表示应答的是哪一个消息。
* 2.是否批量应答未应答消息 false:不批量应答信道中的消息 true:批量
*/
channel.basicAck(delivery.getEnvelope().getDeliveryTag(),false);
};
//采用手动应答
boolean autoAck=false;
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(ACK_QUEUE_NAME,autoAck,deliverCallback,(consumerTag)->{
System.out.println(consumerTag+"消费者取消消费接口回调逻辑");
});
}
}
睡眠ツール
public class SleepUtils {
public static void sleep(int second){
try {
Thread.sleep(1000*second);
} catch (InterruptedException _ignored) {
Thread.currentThread().interrupt();
}
}
}
5) 手動対応効果の実証
通常の状況では、メッセージ送信者は 2 つのメッセージ C1 と C2 を送信し、それぞれのメッセージを受信して処理します。
送信者がメッセージ dd を送信した後、C2 コンシューマーはメッセージの送信後に停止します。論理的に言えば、C2 はメッセージを処理する必要がありますが、処理時間が長いためまだ処理されていません。つまり、C2 は ack を実行していません。この時点で、C2 は停止しています。この時点で、メッセージが C1 によって受信されたことがわかります。これは、メッセージ dd が再度キューに入れられ、メッセージを処理できる C1 に割り当てられたことを示しています。
3.3.RabbitMQ の永続化
3.3.1. コンセプト
タスクを失わない状況に対処する方法と、RabbitMQ サービスが停止したときにメッセージ プロデューサーによって送信されたメッセージが失われないようにする方法を説明しました。デフォルトでは、RabbitMQ が何らかの理由で終了またはクラッシュすると、指示されない限りキューとメッセージを無視します。メッセージが失われないようにするには、次の 2 つのことが必要です。キューとメッセージの両方を永続的としてマークする必要があります。
3.3.2. キューの永続性を実現する方法
以前に作成したキューはすべて非永続的です。rabbitmq を再起動すると、キューは削除されます。キューを永続的にしたい場合は、キューを宣言するときに、durable パラメーターを永続的に設定する必要があります。
プロデューサで変更を加えます。
ただし、以前に宣言されたキューが永続的でない場合は、最初に元のキューを削除するか、永続キューを再作成する必要があることに注意してください。そうしないとエラーが発生します。
元のキューを削除します。
プロデューサー コンシューマーを再度実行します。 以下は、コンソールの永続キューと非永続キューの UI 表示領域です。
このとき、rabbitmqを再起動してもキューはまだ存在します。
3.3.3. メッセージの永続化
メッセージを永続的にするには、メッセージ プロデューサーのコードを変更し、MessageProperties.PERSISTENT_TEXT_PLAIN
この属性を追加する必要があります。
プロデューサで変更を加えます。
メッセージを永続的としてマークしても、メッセージが失われないことが完全に保証されるわけではありません。RabbitMQ にメッセージをディスクに保存するように指示しますが、メッセージがディスクに保存されようとしているのにまだキャッシュされていない場合、メッセージがまだキャッシュされるギャップがまだあります。この時点では、実際には何もディスクに書き込まれません。耐久性の保証は強力ではありませんが、単純なタスク キューには十分です。より強力な永続化戦略が必要な場合は、以下のコースウェアのリリース確認の章を参照してください。
3.3.4. 不公平な分配
最初に、RabbitMQ はデフォルトでローテーション分散を使用してメッセージを分散することを学びましたが、この戦略は特定のシナリオではあまり適切ではありません。たとえば、タスクを処理する 2 つのコンシューマがあり、そのうちの 1 つであるコンシューマ 1 がタスクを処理していますコンシューマ 2 の速度は非常に速いですが、別のコンシューマ 2 の処理速度は非常に遅いです。現時点では、まだローテーション トレーニング分散を使用しています。処理速度が速いコンシューマは、大部分の時間アイドル状態になります。 , 一方、処理速度アイドル状態になりますの遅いコンシューマは
この状況を回避するには、パラメータ channel.basicQos(1); を設定します。
プロデューサーとコンシューマーの変更:
テスト: 時間が短いものはより多くのことを行い、時間が長いものはより少ないことがわかります。(より多くの作業ができる人)
利点:以前のポーリングメカニズムと比較して、不公平な配布により、アイドル状態のコンシューマがより多くのメッセージを処理できるようになり、コンシューマの能力を最大限に活用し、効率が向上します。
これは、私がこのタスクの処理を完了していない場合、またはまだ返信していない場合は、まだ私にタスクを割り当てないでください。現時点では 1 つのタスクしか処理できません。その後、rabbitmq はそのタスクを次のタスクに割り当てます。それほど忙しくないアイドル状態のコンシューマ。もちろん、すべてのコンシューマが手元のタスクを完了しておらず、キューが新しいタスクを追加し続ける場合、キューがいっぱいになる可能性があります。現時点では、新しいワーカーを追加するか、他のストレージを変更することしかできません。ミッション戦略。
3.3.5. プリフェッチ値
メッセージ自体は非同期で送信されるため、チャネル上には常に複数のメッセージが存在する必要があり、コンシューマーからの手動による確認も本質的に非同期です。ここには未確認メッセージ バッファがあるため、開発者がこのバッファのサイズを制限して、バッファ内に無制限の未確認メッセージが存在する問題を回避できることを願っています。これは、basic.qos メソッドを使用して「プリフェッチ カウント」値を設定することで実現できます。この値は、チャネル上で許可される未確認メッセージの最大数を定義します。この数が設定された数に達すると、少なくとも 1 つの未処理メッセージが確認応答されない限り、RabbitMQ はチャネル上でそれ以上のメッセージの配信を停止します。たとえば、チャネル上に未確認のメッセージ 5、6、7、8 があり、チャネルのプリフェッチがあったとします。 count は 4 に設定されます。現時点では、少なくとも 1 つの未応答メッセージが確認応答されない限り、RabbitMQ はこのチャネルでそれ以上のメッセージを配信しません。たとえば、tag=6 のメッセージが ACK されたばかりの場合、RabbitMQ はこの状況を感知して別のメッセージを送信します。メッセージ確認応答と QoS プリフェッチ値は、ユーザーのスループットに大きな影響を与えます。一般に、プリフェッチを増やすと、コンシューマへのメッセージ配信速度が向上します。自動応答の送信メッセージ レートは最適ですが、この場合、配信されたもののまだ処理されていないメッセージの数も増加するため、消費者の RAM (ランダム アクセス メモリ) の消費量が増加するため、自動確認では無制限に使用する必要があるため、注意して使用する必要があります。前処理のモードまたは手動確認モードでは、コンシューマが確認なしで大量のメッセージを消費すると、コンシューマの接続ノードのメモリ消費量が増加するため、適切なプリフェッチ値を見つけるには試行錯誤のプロセスが必要です。この値も異なります。通常、100 ~ 300 の範囲の値は、消費者に大きなリスクを与えることなく最高のスループットを提供します。プリフェッチ値 1 が最も保守的です。もちろん、これにより、特にコンシューマ接続の遅延が深刻な場合、特にコンシューマ接続の待ち時間が長い環境では、スループットが非常に低くなります。ほとんどのアプリケーションでは、わずかに高い値が最適です。
概要:
-
RabbitMQ のプリフェッチ値は、コンシューマーがキューからメッセージを取得するときに一度に取得するメッセージの数を指します。適切なプリフェッチ値を設定することにより、メッセージの分散とコンシューマの負荷分散を最適化できます。
-
RabbitMQ では、プリフェッチ値はコンシューマーがキューから取得するメッセージの数を指します。コンシューマーがメッセージを処理するとき、一度に 1 つのメッセージだけではなく、複数のメッセージを一度に取得できます。適切なプリフェッチ値を設定することにより、メッセージ処理の効率が向上し、ネットワーク遅延とコンシューマ間の通信オーバーヘッドを削減できます。
-
コードのテストを変更する: コンシューマで変更を加える
-
効果: 7 つのメッセージを送信します。コンシューマ 1 は 2 つのメッセージを受信し、コンシューマ 2 は 5 つのメッセージを受信すると予想されますが、実際には、コンシューマ 1 は 3 つのメッセージを受信し、コンシューマ 2 は 4 つのメッセージを受信します。
-
理由:
- まず、コンシューマ間には競合関係があるため、メッセージの順序は一定ではありません。たとえば、最初のメッセージはコンシューマ 1 に送信され、コンシューマ 2 にも送信される可能性があります。
- プリフェッチ値は、実際にはチャネル (キューとコンシューマー間のパイプ) 内のメッセージの最大蓄積量を設定しますが、コンシューマーが設定した数のメッセージを受け入れることを意味するものではありません。
- Consumer 1 はメッセージの処理に 1 秒しかかからず、Consumer 2 の処理は 30 秒に設定されているため、3 番目のメッセージが送信された時点で Consumer 1 の最初のメッセージがすでに処理されている可能性があり、そのため、 future. 消費者 1 にメッセージを送信します。
- 設定されたプリフェッチ値の 7 を超えると、8 番目のデータは、それをより速く消費するコンシューマーによって処理されます。
4.リリース確認
注: プロデューサによって送信されたメッセージが失われないようにするには、次の 3 つの手順が必要です。
- この設定では、キューが永続的である必要があります (これにより、RabbitMq サーバーがダウンしてもキューが失われないことが保証されます)。
- この設定では、キュー内のメッセージを永続化する必要があります (キュー内のメッセージは失われないことが保証されます)。
- 解放確認 (最初の 2 つの項目が設定されていても、メッセージがキューに転送されてディスクに保存される前にマシンがダウンします。この時点でもメッセージは失われるため、設定が必要です)リリース確認)
リリース確認の概要:
- プロデューサがメッセージをキューに送信し、ディスクに保存した後、MQ はプロンプト メッセージをプロデューサに返し、メッセージがディスクに保存されたことをプロデューサに通知します。この時点でのみ、メッセージがディスクに保存されたことが保証されます。失われることはありません。
4.1. リリース確認の原則
プロデューサはチャネルを確認モードに設定します。チャネルが確認モードに入ると、チャネル上でパブリッシュされたすべてのメッセージに一意の ID (1 から始まる) が割り当てられます。メッセージが一致するすべてのキューに配信されると、ブローカー A の確認が行われます。 (メッセージの一意の ID を含む) がプロデューサに送信され、これによりプロデューサは、メッセージが宛先キューに正しく到着したことを知ることができます。メッセージとキューが永続的であれば、確認メッセージによってメッセージがディスクに書き込まれます。その後、確認メッセージによってメッセージがディスクに書き込まれます。 , ブローカーによってプロデューサーに送り返される確認メッセージのdelivery-tag フィールドには、確認メッセージのシーケンス番号が含まれます。さらに、ブローカーは、これまでのすべてのメッセージを示すように、basic.ack の multiple フィールドを設定することもできます。シーケンス番号を受信しましたので対処いたします。
確認モードの最大の利点は、非同期であることです。メッセージがパブリッシュされると、プロデューサー アプリケーションは、チャネルから確認が返されるのを待ちながら次のメッセージの送信を続けることができます。メッセージが最終的に確認されると、プロデューサー アプリケーションはコールバック メソッド 確認メッセージを処理するために、RabbitMQ が内部エラーによりメッセージを失った場合、Nack メッセージが送信されます。プロデューサ アプリケーションは、コールバック メソッドで NACK メッセージを処理することもできます。
4.2. リリース確認の戦略
4.2.1. リリース確認を有効にする方法
リリース確認はデフォルトでは有効になっていません。有効にするには、confirmSelect メソッドを呼び出す必要があります。リリース確認を使用する場合は、チャネル上でこのメソッドを呼び出す必要があります。
- プロデューサのチャネル作成後にこのメソッドを呼び出します。
- テスト:
//开启发布确认
channel.confirmSelect();
4.2.2.シングルのリリースが確認されました
これは単純な確認メソッドです。リリースを同期的に確認する方法です。つまり、メッセージがリリースされた後、リリースされることが確認された場合にのみ、後続のメッセージをリリースし続けることができます。waitForconfirmsOrDie(long) メソッドは、メッセージは確認された後にのみ解放されます。時間が経過した場合にのみ返されます。指定された時間範囲内にメッセージが確認されない場合は、例外がスローされます。
この確認方法の最大の欠点は、公開されたメッセージが確認されない場合、後続のすべてのメッセージの公開がブロックされるため、公開速度が非常に遅いことです。この方法では、1 件あたり数百件以下の公開メッセージのスループットしか提供できません。 2番目です。もちろん、アプリケーションによってはこれで十分な場合もあります。
-
1 つを確認するために 1 つ送信します (同期確認)。2 番目のデータは、前のデータが公開されて確認されるまで送信されませんが、これは非常に時間がかかります。
-
テスト: テストする新しいプロデューサー コードを作成します。
package com.atguigu.rabbitmq.four;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.UUID;
/**
* 发布确认模式
* 使用的时间比较哪种确认方式是最好的
* 1、单个确认
* 2、批量确认
* 3、异步批量确认
*/
public class ConfirmMessage {
//批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
//1、单个确认
ConfirmMessage.publishMessageIndividually();//发布1000个单独确认消息,耗时989ms
//2、批量确认
//3、异步批量确认
}
//1、单个确认
public static void publishMessageIndividually() throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
//发送消息
channel.basicPublish("", queueName, null, message.getBytes());
//单个消息就马上发布确认,如果是true代表发送成功,使用if判断编写提示信息。
//服务端返回 false 或超时时间内未返回,生产者可以消息重发
boolean flag = channel.waitForConfirms();
if (flag) {
System.out.println("消息发送成功");
}
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + "ms");
}
}
- 効果: 1000 個の個別の確認メッセージを発行し、989 ミリ秒かかります。
4.2.3. 一括確認リリース
上記の方法は非常に時間がかかります。単一の確認メッセージを待つ場合と比較して、メッセージのバッチを発行してそれらをまとめて確認すると、スループットが大幅に向上します。もちろん、この方法の欠点は次のとおりです。障害が発生して発行が中断された場合どのメッセージに問題があるかは不明です。重要な情報を記録するためにバッチ全体をメモリに保存してから、メッセージを再発行する必要があります。もちろん、このソリューションは依然として同期的であり、メッセージのリリースをブロックします。
- テスト:
package com.atguigu.rabbitmq.four;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.UUID;
/**
* 发布确认模式
* 使用的时间比较哪种确认方式是最好的
* 1、单个确认
* 2、批量确认
* 3、异步批量确认
*/
public class ConfirmMessage {
//批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
//1、单个确认
//ConfirmMessage.publishMessageIndividually();//发布1000个单独确认消息,耗时989ms
//2、批量确认
ConfirmMessage.publishMessageBatch(); //发布1000个批量确认消息,耗时78ms
//3、异步批量确认
}
//2、批量确认
public static void publishMessageBatch() throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//批量确认消息大小
int batchSize = 100;
//批量发送消息 批量发布确认
for (int i = 0; i < MESSAGE_COUNT; i++) {
String message = i + "";
//发送消息
channel.basicPublish("", queueName, null, message.getBytes());
//判断达到100条消息的时候,批量确认一次
if(i%batchSize==0){
//发布确认
channel.waitForConfirms();
}
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + "ms");
}
}
- 効果:
4.2.4. 非同期確認リリース
非同期確認のプログラミング ロジックは前の 2 つよりも複雑ですが、信頼性と効率の両方の点で最もコスト効率が高くなります。コールバック関数を使用して信頼性の高いメッセージ配信を実現します。このミドルウェアはまた、関数コールバックを使用して、配信は成功しました。非同期確認の実装方法を詳しく説明します。
- テスト:
package com.atguigu.rabbitmq.four;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import java.util.UUID;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* 发布确认模式
* 使用的时间比较哪种确认方式是最好的
* 1、单个确认
* 2、批量确认
* 3、异步批量确认
*/
public class ConfirmMessage {
//批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
//1、单个确认
//ConfirmMessage.publishMessageIndividually();//发布1000个单独确认消息,耗时989ms
//2、批量确认
//ConfirmMessage.publishMessageBatch(); //发布1000个批量确认消息,耗时78ms
//3、异步批量确认
ConfirmMessage.publishMessageAsync(); //发布1000个异步发布确认消息,耗时52ms
}
//3、异步批量确认
public static void publishMessageAsync() throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
//开始时间
long begin = System.currentTimeMillis();
//消息确认成功 回调函数
ConfirmCallback ackCallback = (deliveryTag,multiple)->{
System.out.println("确认的消息:"+deliveryTag);
};
//消息确认失败 回调函数
/**
* 1.消息的标记
* 2.是否为批量确认
*/
ConfirmCallback nackCallback = (deliveryTag,multiple)->{
System.out.println("未确认的消息:"+deliveryTag);
};
/**
* 准备消息的监听器 监听那些消息成功了 那些消息失败了
* 位置:监听器要写在发送消息之前,如果写在发消息之后,有可能在发消息的
* 过程中通知你的时候就接收不到消息了。因为你必须发完1000条消息
* 才会触发监听器,所以要把监听器放到前面,在你没有发的时候就准备出来,
* 在发的过程中就有可能随时监听到哪些消息成功哪些消息失败。
* 参数1:监听哪些消息成功了
* 参数2:监听哪些消息失败了
* 参数类型:函数式接口,使用lambda代替匿名内部类的写法
*
*/
channel.addConfirmListener(ackCallback,nackCallback);//异步通知
for(int i =0;i<MESSAGE_COUNT;i++){
String message ="消息"+i;
//发送消息
channel.basicPublish("", queueName, null, message.getBytes());
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个异步发布确认消息,耗时" + (end - begin) + "ms");
}
}
4.2.5. 非同期の未確認メッセージの処理方法
最善の解決策は、未確認のメッセージを、公開スレッドからアクセスできるメモリベースのキューに入れることです。たとえば、ConcurrentLinkedQueue( 并发链路队列
) を使用して、確認コールバックと公開スレッドの間でメッセージを転送します。
- 質問 1: 前回の非同期リリース確認では、リスナーを使用して、どのメッセージが正常に確認され、どのメッセージが失敗したかを非同期的に監視しました。正常に確認されたメッセージを処理する必要はありませんが、確認に失敗したメッセージにはどのように対処すればよいでしょうか? ? ?
- 未確認メッセージを再送信したり、後で再公開するために未確認メッセージを保存したりできます。
- 質問 2: 現在、リスナーとメッセージを送信するスレッドの 2 つのスレッドがあり、それらは非同期です。メッセージの送信後、リスナーはまだ実行中ですが、この時点で未確認のメッセージを見つけるにはどうすればよいでしょうか? ? ?
- ConcurrentLinkedQueue(
并发链路队列
)の使用 - 次のテストでは問題 2 は解決されますが、問題 1 は解決されていません。
- ConcurrentLinkedQueue(
非同期確認リリース用にコードを変更します。
package com.atguigu.rabbitmq.four;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import java.util.UUID;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
/**
* 发布确认模式
* 使用的时间比较哪种确认方式是最好的
* 1、单个确认
* 2、批量确认
* 3、异步批量确认
*/
public class ConfirmMessage {
//批量发消息的个数
public static final int MESSAGE_COUNT = 1000;
public static void main(String[] args) throws Exception {
//1、单个确认
//ConfirmMessage.publishMessageIndividually();//发布1000个单独确认消息,耗时989ms
//2、批量确认
//ConfirmMessage.publishMessageBatch(); //发布1000个批量确认消息,耗时78ms
//3、异步批量确认
ConfirmMessage.publishMessageAsync(); //发布1000个异步发布确认消息,耗时52ms
}
//3、异步批量确认
public static void publishMessageAsync() throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
//队列的声明
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, false, false, false, null);
//开启发布确认
channel.confirmSelect();
/**
* 线程安全有序的一个哈希表,适用于高并发的情况(Map集合)
* 此哈希表可以做以下3件事:
* 1.轻松的将序号与消息进行关联
* 2.轻松批量删除条目 只要给到序列号
* 3.支持并发访问
*/
ConcurrentSkipListMap<Long, String> outstandingConfirms = new
ConcurrentSkipListMap<>();
/**
* 消息确认成功 回调函数
* 1.消息序列号
* 2.true 可以确认小于等于当前序列号的消息
* false 确认当前序列号消息
*/
ConfirmCallback ackCallback = (deliveryTag,multiple)->{
if (multiple) {
//判断是否为批量确认,如果是则调用批量删除的方法
//2):删除掉已经确认的消息 剩下的就是未确认的消息
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(deliveryTag);//返回的是小于等于当前序列号的被确认消息 是一个 map
//清除该部分被确认消息
confirmed.clear();
}else{
//如果不是则调用单个删除的方法
//删除当前消息
outstandingConfirms.remove(deliveryTag);
}
System.out.println("确认的消息:"+deliveryTag);
};
//消息确认失败 回调函数
/**
* 1.消息的标记
* 2.是否为批量确认
*/
ConfirmCallback nackCallback = (deliveryTag,multiple)->{
//3):打印一下未确认的消息都有哪些 此处测试并没有未被确认的消息
String message = outstandingConfirms.get(deliveryTag);
System.out.println("未确认的消息是:"+message+"::::未被确认的消息是tag:"+deliveryTag);
};
/**
* 准备消息的监听器 监听那些消息成功了 那些消息失败了
* 位置:监听器要写在发送消息之前,如果写在发消息之后,有可能在发消息的
* 过程中通知你的时候就接收不到消息了。因为你必须发完1000条消息
* 才会触发监听器,所以要把监听器放到前面,在你没有发的时候就准备出来,
* 在发的过程中就有可能随时监听到哪些消息成功哪些消息失败。
* 参数1:监听哪些消息成功了
* 参数2:监听哪些消息失败了
* 参数类型:函数式接口,使用lambda代替匿名内部类的写法
*
*/
channel.addConfirmListener(ackCallback,nackCallback);//异步通知
//开始时间
long begin = System.currentTimeMillis();
for(int i =0;i<MESSAGE_COUNT;i++){
String message ="消息"+i;
//1):此处记录下所有要发送的消息 消息的总和
// 通过put方法向此map集合中添加k,v (通过信道调取发布的序号,发送的信息)
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
//发送消息
channel.basicPublish("", queueName, null, message.getBytes());
}
//结束时间
long end = System.currentTimeMillis();
System.out.println("发布" + MESSAGE_COUNT + "个异步发布确认消息,耗时" + (end - begin) + "ms");
}
}
4.2.6. 上記3つのリリース確認速度の比較
- メッセージを個別に投稿する
- 同期的に確認を待機します。シンプルですが、スループットは非常に限られています。
- メッセージをバッチで公開する
- バッチ同期は確認を待機し、シンプルで合理的なスループットを実現します。問題が発生すると、どのメッセージに問題があるかを推測するのは困難です。
- 非同期処理: (
推荐
)- 最適なパフォーマンスとリソース使用量。エラーが発生した場合は適切に制御されますが、達成するのが若干困難です
public static void main(String[] args) throws Exception {
//这个消息数量设置为 1000 好些 不然花费时间太长
publishMessagesIndividually();
publishMessagesInBatch();
handlePublishConfirmsAsynchronously();
}
//运行结果
发布 1,000 个单独确认消息耗时 50,278 ms
发布 1,000 个批量确认消息耗时 635 ms
发布 1,000 个异步确认消息耗时 92 ms
5.スイッチ
前のセクションでは、ワークキューを作成しました。ここで想定しているのは、ワーク キューの背後で、各タスクが 1 つのコンシューマー (ワーカー プロセス) にのみ配信されるということです。この部分では、まったく異なることを行います。複数の消費者にメッセージを伝えます。このパターンは「パブリッシュ/サブスクライブ」と呼ばれます。
要約:
-
以前のキューではデフォルトのスイッチが使用されており、メッセージは 1 つのコンシューマによってのみ使用できました。(
简单模式、工作模式
)
-
メッセージを 2 回消費したい場合は、スイッチを介して 2 つのキューにメッセージを割り当てることができます。1 つのキュー内のメッセージは 1 回だけ消費できます。(
发布订阅模式
)
このパターンを説明するために、単純なログ システムを構築します。これは 2 つのプログラムで構成されます。最初のプログラムはログ メッセージを出力し、2 番目のプログラムはコンシューマになります。そのうち 2 つのコンシューマーを起動します。1 つのコンシューマーはメッセージを受信した後、ログをディスクに保存します。もう 1 つのコンシューマーは、メッセージを受信した後、メッセージを画面に表示します。実際、最初のプログラムによって送信されたログ メッセージは、すべての消費者にブロードキャストされる。
5.1.交換
5.1.1.交換の概念
RabbitMQ のメッセージング モデルの中心的な考え方は、プロデューサーによって生成されたメッセージがキューに直接送信されることはないということです。実際、多くの場合、プロデューサーは、これらのメッセージがどのキューに配信されるのかさえ知りません。
代わりに、プロデューサーはメッセージをエクスチェンジに送信することしかできません。エクスチェンジの仕事は非常に単純です。一方ではプロデューサーからメッセージを受信し、他方ではメッセージをキューにプッシュします。スイッチは、受信したメッセージをどう処理するかを正確に認識している必要があります。これらのメッセージを特定のキューに入れるのか、多くのキューに入れるのか、それとも破棄するのか。これはスイッチのタイプによって異なります。
5.1.2.交換の種類
合計すると次の種類があります。
- direct(ダイレクト/ルーティングタイプ)、
- トピック、
- ヘッダー (ヘッダー/ヘッダー タイプ)、
- ファンアウト(ファンアウト/パブリッシュ・サブスクライブ型)
5.1.3. ネームレスエクスチェンジ(デフォルトタイプ)
このチュートリアルの前半では、交換について何も知りませんでしたが、それでもキューにメッセージを送信することができました。以前はそれが可能であったのは、デフォルトの交換を渡していたためです空字符串(“”)进行标识
。
最初のパラメータはスイッチの名前です。空の文字列は、デフォルトまたは名前のないスイッチを表します。メッセージは、routingKey (bindingkey) バインディング キー (存在する場合) によって指定されたキューにルーティングできます。
- つまり、デフォルトのスイッチを使用し、最初のパラメータとして空の文字列を書き込み、2 番目のパラメータとしてキュー名を書き込みます。スイッチを指定する場合、2 番目のパラメータは routingKey です。
5.2. 一時キュー
前の章では、特定の名前を持つキューを使用しました (hello と ack_queue を覚えていますか?)。キューの名前は私たちにとって非常に重要です。コンシューマがどのキューからメッセージを消費するかを指定する必要があります。
Rabbit に接続するときは常に、新しい空のキューが必要です。そのキューに対してランダムな名前のキューを作成することも、サーバーにランダムなキュー名を選択させることもできます。次に、コンシューマを切断すると、キューは自動的に削除されます。
要約:
-
一時キューは永続性のないキューであり、コンシューマーを切断すると、キューは自動的に削除されます。
-
一時キューを作成する方法は次のとおりです。
String queueName = channel.queueDeclare().getQueue();
-
作成後は次のようになります。
5.3.バインディング
バインディングとは何ですか? バインディングは実際には Exchange とキューの間の橋渡しであり、Exchange がそのキューにバインドされていることを示します。たとえば、下の図からわかることは、X が Q1 と Q2 にバインドされていることです。
5.4.ファンアウト (パブリッシュ/サブスクライブ モード)
5.4.1.ファンアウトの概要
ファンアウト このタイプは非常にシンプルです。名前から推測できるように、受信したすべてのメッセージを既知のすべてのキューにブロードキャストします。システムにはデフォルトでいくつかの交換タイプがあります
amq.fanout
: デフォルトでシステムによって提供されるパブリッシュ/サブスクライブ スイッチ。
5.4.2.ファンアウト実戦
- シナリオ: プロデューサがメッセージをエクスチェンジに送信し、エクスチェンジが 2 つのキューをバインドします (バインド条件は同じで、キューに送信されるメッセージも同じです)。メッセージ 1 はコンシューマ 1 によって受信され、メッセージ 2 はコンシューマ 1 によって受信されます。消費者が受け取る 2.
ログと一時キューのバインド関係は以下のとおりです。
- スイッチ名: ログ
- 2 キュー名: ランダムに生成されたキュー名。
- バインディング関係: 空の文字列
ReceiveLogs01 は、受信したメッセージをコンソール (コンシューマー 1) に出力します。
package com.atguigu.rabbitmq.five;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消息的接收 01
*/
public class ReceiveLogs01 {
//定义交换机的名称
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机:名称,类型(fanout:发布订阅类型)
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列 队列的名称是随机的
* 当消费者断开和该队列的连接时 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机与队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为空串
*/
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("消费者1:控制台打印接收到的消息"+message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
ReceiveLogs02 は受信したメッセージをディスクに保存します (コンシューマー 2)
package com.atguigu.rabbitmq.five;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import org.apache.commons.io.FileUtils;
import java.io.File;
/**
* 消息的接收 02
*/
public class ReceiveLogs02 {
//定义交换机的名称
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机:名称,类型(fanout:发布订阅类型)
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
/**
* 生成一个临时的队列 队列的名称是随机的
* 当消费者断开和该队列的连接时 队列自动删除
*/
String queueName = channel.queueDeclare().getQueue();
/**
* 绑定交换机与队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为空串
*/
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
File file = new File("D:\\work\\rabbitmq_info.txt");
/*
* FileUtils:文件操作工具类
* 存在于Common工具库中。包含一些工具类,它们基于File对象工作,包括读,写,拷贝和比较文件。
* writeStringToFile():把字符串写入文件
* 参数:file文件名 写入文件的内容 编码 append为true表示在文本内容后面添加内容而不会覆盖文件中的内容重写。
* windows系统换行:\r\n
* */
FileUtils.writeStringToFile(file,message + "\r\n","UTF-8",true);
System.out.println("消费者2:数据写入文件成功");
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
EmitLog は 2 つのコンシューマ (プロデューサー) にメッセージを送信します
package com.atguigu.rabbitmq.five;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.Scanner;
/**
* 生成者:发消息给交换机
*/
public class EmitLog {
//交换机的名称
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
//创建信道
try (Channel channel = RabbitMqUtils.getChannel()) {
/**
* 声明一个 exchange(交换机)
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//接收键盘输入的消息
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
while (sc.hasNext()) {
String message = sc.nextLine();
/**
*发送一个消息
*1.发送到那个交换机 如果是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 使用默认的交换机写的是:队列的名称 指定了交换机写的是:routingKey关键词
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
}
走る:
- プロデューサー:
- 消費者 1:
- 消費者 2:
5.5.直接交換(ルーティングモード)
5.5.1. レビュー
前のセクションでは、単純なログ システムを構築しました。ログ メッセージを多くの受信者にブロードキャストできます。このセクションでは、いくつかの特別な機能を追加します。たとえば、公開されたメッセージの一部を特定のコンシューマにのみ購読できるようにするとします。たとえば、すべてのログ メッセージをコンソールに出力できる一方で、重大なエラー メッセージのみをログ ファイルにリダイレクトします (ディスク領域を節約するため)。
バインディングとは何かをもう一度確認してみましょう。バインディングはスイッチとキューの間のブリッジ関係です。これは、次のように理解することもできます。キューは、バインドされているスイッチからのメッセージのみに関心があります。バインディングはパラメータ routingKey で表され、バインディング キーとも呼ばれます。バインディングを作成するには、次のコードを使用します: channel.queueBind(queueName, EXCHANGE_NAME, “routingKey”); バインド後の意味はその交換によって決まります。タイプ。
5.5.2.直接交換の導入
前のセクションのログ システムは、すべてのメッセージをすべてのコンシューマーにブロードキャストします。これにいくつかの変更を加えます。たとえば、ログ メッセージをディスクに書き込むプログラムが重大なエラー (errros) のみを受信し、警告 (warning) を保存しないようにします。または情報 (info) ログ メッセージを使用して、ディスク領域の無駄を回避します。Fanout 交換タイプでは、あまり柔軟性がありません - 無意識のブロードキャストのみを実行します。ここでは、直接タイプを使用して置き換えます。このタイプの仕組みは、メッセージがバインドされている routingKey にのみ送信されることです。待ち行列。
上の図では、X が 2 つのキューにバインドされており、バインド タイプが直接であることがわかります。キュー Q1 のバインディング キーはオレンジ色で、キュー Q2 のバインディング キーは 2 つあります。1 つのバインディング キーは黒で、もう 1 つのバインディング キーは緑です。
このバインディングの場合、プロデューサーは交換するメッセージをパブリッシュし、バインディング キーがオレンジのメッセージはキュー Q1 にパブリッシュされます。バインディング キーが黒緑色のメッセージはキュー Q2 にパブリッシュされ、他のメッセージ タイプのメッセージは破棄されます。
概要:
- パブリッシュ・サブスクライブモード:バインディングタイプはFanult、バインディング条件routingKeyは同じ(1人がメッセージを送信し、複数人が受信可能)
- ルーティング モード: バインディング タイプはダイレクトであり、バインディング条件 routingKey が異なります (1 人がメッセージを送信し、受信するように指定されており、他の人はメッセージを受信できません)
5.5.3. 多重結合
もちろん、交換のバインディング タイプが direct であるが、バインドされている複数のキューのキーがすべて同じである場合、この場合、バインディング タイプは direct ですが、ファンアウトに似た動作をし、ほぼブロードキャストに似ています。図1に示すように、
- バインディング タイプは直接であり、バインディング条件 routingKey は同じです。つまり、効果はパブリッシュ/サブスクライブ モードと一致します。
5.5.4. 実際の戦闘
- スイッチ:direct_logs
- キュー 1: コンソール
- キュー 2:ディスク
プロデューサー
package com.atguigu.rabbitmq.six;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import java.util.HashMap;
import java.util.Map;
/**
* 生产者
*/
public class EmitLogDirect {
//交换机的名称
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
//创建信道
try (Channel channel = RabbitMqUtils.getChannel()) {
/**
* 声明一个 exchange(交换机)
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//创建多个 bindingKey
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("info","普通 info 信息");
bindingKeyMap.put("warning","警告 warning 信息");
bindingKeyMap.put("error","错误 error 信息");
//debug 没有消费这接收这个消息 所有就丢失了
bindingKeyMap.put("debug","调试 debug 信息");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
/**
*发送一个消息
*1.发送到那个交换机 如果是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 使用默认的交换机写的是:队列的名称 指定了交换机写的是:routingKey关键词
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
channel.basicPublish(EXCHANGE_NAME,bindingKey, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
}
消費者 1:
package com.atguigu.rabbitmq.six;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消费者1:
*/
public class ReceiveLogsDirect01 {
//定义交换机的名称
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机:名称,类型(direct:路由类型)
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/**
* 生成一个队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
String queueName = "console";
channel.queueDeclare(queueName, false, false, false, null);
/**
* 绑定交换机与队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为info,warning
*/
channel.queueBind(queueName, EXCHANGE_NAME, "info");
channel.queueBind(queueName, EXCHANGE_NAME, "warning");
System.out.println("等待接收消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 消费者01 :"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
消費者 2:
package com.atguigu.rabbitmq.six;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 消费者1:
*/
public class ReceiveLogsDirect02 {
//定义交换机的名称
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机:名称,类型(direct:路由类型)
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/**
* 生成一个队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
String queueName = "disk";
channel.queueDeclare(queueName, false, false, false, null);
/**
* 绑定交换机与队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为error
*/
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println("等待接收消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 消费者02 :"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
テスト: 結果は、スイッチ タイプがルーティング モード (ダイレクト) であることを示しています。複数のタイプをバインドする場合、いずれかのバインド関係が満たされていれば、メッセージをキューに送信できます。バインディング関係は、送信する前に満たされている必要があります。結合関係が満たされていない場合は送信できません。
- プロデューサー
- 消費者 1:
- 消費者 2:
5.6.トピック(テーマモード)
5.6.1. 以前の種類の質問
前のセクションでは、ログ システムを改善しました。ランダムなブロードキャストのみを実行できるファンアウト スイッチを使用する代わりに、ログを選択的に受信するダイレクト スイッチを使用します。
ダイレクト スイッチの使用によりシステムは改善されましたが、まだ制限があります。たとえば、受信したいログ タイプは info.base と info.advantage で、特定のキューは info.base からのメッセージのみを必要としているため、ダイレクト スイッチはそれを受け取ります。できません。現時点ではトピックタイプのみ使用できます
5.6.2.トピックの要件
トピック スイッチに送信されるメッセージの routing_key は任意に記述することはできず、特定の要件を満たす必要があり、ドットで区切られた単語のリストである必要があります。これらの単語は、「stock.usd.nyse」、「nyse.vmw」、「quick.orange.rabbit」などの任意の単語にすることができます。もちろん、この単語リストは最大 255 バイトを超えることはできません。
このルール リストには、誰もが注意する必要がある 2 つの置換文字があります。
*(星号)可以代替一个单词
- #(ポンド記号) は 0 個以上の単語を置き換えることができます
概要: パブリッシュ/サブスクライブ モードやルーティング モードと比較して、より柔軟です。条件付きまたは一律に送信できます。原則として、ワイルドカードを使用してバインディング関係を記述します。
5.6.3.トピックマッチングの場合
下図の結合関係は以下の通りです
- Q1–>境界は
- 中央にオレンジ色の 3 つの単語の文字列 (
*.orange.*
)
- 中央にオレンジ色の 3 つの単語の文字列 (
- Q2–>バウンドは
- 最後の単語はウサギの3文字です(
*.*.rabbit
) - 最初の単語は、lazy (lazy.#) の複数の単語です。
- 最後の単語はウサギの3文字です(
上図はキューバインディングの関係図ですが、キュー間のデータ受信状況を見てみましょう。
quick.orange.rabbit 被队列 Q1Q2 接收到
lazy.orange.elephant 被队列 Q1Q2 接收到
quick.orange.fox 被队列 Q1 接收到
lazy.brown.fox 被队列 Q2 接收到
lazy.pink.rabbit 虽然满足两个绑定但只被队列 Q2 接收一次
quick.brown.fox 不匹配任何绑定不会被任何队列接收到会被丢弃
quick.orange.male.rabbit 是四个单词不匹配任何绑定会被丢弃
lazy.orange.male.rabbit 是四个单词但匹配 Q2
キューのバインド関係が次の場合は注意が必要です。
当一个队列绑定键是#,那么这个队列将接收所有数据,就有点像 fanout 了
如果队列绑定键当中没有#和*出现,那么该队列绑定类型就是 direct 了
5.6.4. 実際の戦闘
- スイッチ: topic_logs
- キュー 1: Q1
- キュー 2: Q2
プロデューサー:
package com.atguigu.rabbitmq.seven;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.util.HashMap;
import java.util.Map;
/**
* 生产者:
*/
public class EmitLogTopic {
//交换机的名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
//创建信道
try (Channel channel = RabbitMqUtils.getChannel()) {
/**
* 声明一个 exchange(交换机)
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
/**
* Q1-->绑定的是
* 中间带 orange 带 3 个单词的字符串(*.orange.*)
* Q2-->绑定的是
* 最后一个单词是 rabbit 的 3 个单词(*.*.rabbit)
* 第一个单词是 lazy 的多个单词(lazy.#)
*
*/
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("quick.orange.rabbit","被队列 Q1Q2 接收到");
bindingKeyMap.put("lazy.orange.elephant","被队列 Q1Q2 接收到");
bindingKeyMap.put("quick.orange.fox","被队列 Q1 接收到");
bindingKeyMap.put("lazy.brown.fox","被队列 Q2 接收到");
bindingKeyMap.put("lazy.pink.rabbit","虽然满足两个绑定但只被队列 Q2 接收一次");
bindingKeyMap.put("quick.brown.fox","不匹配任何绑定不会被任何队列接收到会被丢弃");
bindingKeyMap.put("quick.orange.male.rabbit","是四个单词不匹配任何绑定会被丢弃");
bindingKeyMap.put("lazy.orange.male.rabbit","是四个单词但匹配 Q2");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
/**
*发送一个消息
*1.发送到那个交换机 如果是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 使用默认的交换机写的是:队列的名称 指定了交换机写的是:routingKey关键词
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
channel.basicPublish(EXCHANGE_NAME,bindingKey, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message);
}
}
}
}
消費者 1:
package com.atguigu.rabbitmq.seven;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 声明主题交换机 以及相关队列
*
* 消费者C1
*/
public class ReceiveLogsTopic01 {
//定义交换机的名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机:名称,类型(topic:主题类型)
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
/**
* 生成一个队列Q1,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
String queueName="Q1";
channel.queueDeclare(queueName, false, false, false, null);
/**
* 绑定交换机与队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为
*/
channel.queueBind(queueName, EXCHANGE_NAME, "*.orange.*");
System.out.println("等待接收消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列 :"+queueName+" 绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
消費者 2:
package com.atguigu.rabbitmq.seven;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
/**
* 声明主题交换机 以及相关队列
*
* 消费者C2
*/
public class ReceiveLogsTopic02 {
//定义交换机的名称
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明一个交换机:名称,类型(topic:主题类型)
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
/**
* 生成一个队列Q2,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
String queueName="Q1";
channel.queueDeclare(queueName, false, false, false, null);
/**
* 绑定交换机与队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为
*/
channel.queueBind(queueName, EXCHANGE_NAME, "*.*.rabbit");
channel.queueBind(queueName, EXCHANGE_NAME, "lazy.#");
System.out.println("等待接收消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println(" 接收队列 :"+queueName+" 绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> {
});
}
}
テスト:
-
プロデューサー
-
消費者01
-
消費者02
6.デッドレターキュー
6.1. デッドレターの概念
まず概念的な説明からこの定義を明確にしましょう.デッドレター, 名前が示すように,消費できないメッセージです. 文字通りの意味は次のように理解できます. 一般に、プロデューサーはメッセージをブローカーに配信するか、キューに直接配信します. コンシューマ (consumer) はキューからメッセージを取り出して消費しますが、特定の理由によりキュー内の一部のメッセージを消費できない場合があります。そのようなメッセージはその後処理されないとデッドレターとなり、手紙には当然、配信不能キューが存在します。
アプリケーションシナリオ:
- 注文業務のメッセージデータが失われないようにするには、RabbitMQ のデッドレターキューメカニズムを使用する必要があり、メッセージ消費時に例外が発生した場合、メッセージはデッドレターキューに入れられます。
- メッセージを消費できない場合、メッセージは不良レター キューに入れられます。その目的は、環境が改善されて不良レター キュー内のメッセージが消費されるまで待機することです。
- たとえば、ユーザーがモールで注文に成功し、クリックして支払いを行った場合、指定された時間内に支払いが行われないと、注文は自動的に期限切れになります。
- これは、デッド レター キューに一定の遅延があり、デッド レターは一定期間内にコンシューマーによって消費される可能性があることを反映しています。
6.2. デッドレターの出所
- メッセージの TTL (生存時間) が期限切れになる
- キューが最大長に達しました (キューがいっぱいで、これ以上データを mq に追加できません)
- メッセージは拒否され (basic.reject または Basic.nack)、requeue=false になりました。
- 説明: メッセージ当事者が応答したときに、応答を拒否したか否定的に応答し、キューに戻されていませんでした。
6.3. デッドレターの練習
6.3.1. コードアーキテクチャ図
- プロデューサはメッセージを通常の交換機に送信し、交換機はメッセージをキューに送信して、コンシューマによって消費されます。送信されたメッセージがデッドレター メッセージである場合、デッドレター メッセージは通常のキューからデッドレター交換に送信され、その後デッドレター キューに送信されてコンシューマー 2 によって消費されます。
6.3.2. メッセージの TTL 有効期限
- テスト プロセス: プロデューサでメッセージが発生するたびに、10 秒のタイムアウトが発生します。この 10 秒以内にコンシューマによって消費されない場合、デッド レター メッセージはデッド レター スイッチに転送され、最終的にコンシューマ 2 によって消費されます。 。
- シャットダウン後にコンシューマ 1 を起動する目的: 起動後、通常のスイッチ、通常のキュー、デッド レター キュー、デッド レター キュー、および通常のスイッチとデッド レター スイッチとデッド レター スイッチの関係が作成されます。文字スイッチとルーティングキー。クローズ後、消費者 1 の死亡後 10 秒以内にメッセージがタイムアウトすることが保証されます。この時点で、デッド レター メッセージはデッド レター スイッチに転送されます。
プロデューサーコード
package com.atguigu.rabbitmq.eight;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
/**
* 死信队列之 生产者代码
*/
public class Producer {
//交换机的名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] argv) throws Exception {
//创建信道
try (Channel channel = RabbitMqUtils.getChannel()) {
/**
* 声明一个 exchange(交换机)
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置消息的 TTL(超时)时间 单位是ms毫秒 10000ms=10s
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
//该信息是用作演示队列个数限制
for (int i = 1; i <11 ; i++) {
/**
*发送一个消息
*1.发送到那个交换机 如果是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 使用默认的交换机写的是:队列的名称 指定了交换机写的是:routingKey关键词
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
String message="info"+i;
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", properties, message.getBytes());
System.out.println("生产者发送消息:"+message);
}
}
}
}
コンシューマ C1 コード ( 启动之后关闭该消费者 模拟其接收不到消息
)
package com.atguigu.rabbitmq.eight;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
/**
* 死信队列 实战
*
* 消费者01
*/
public class Consumer01 {
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明普通和死信交换机: 名称,类型(direct:路由模式)
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
/**
* 声明普通队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
* 设置消息为死信后转发到死信交换机,参数类型为Map集合
*/
String normalQueue = "normal-queue";
//设置消息为死信后转发到死信交换机,参数类型为Map集合
Map<String, Object> arguments = new HashMap<>();
//过期时间 10s=10000ms 在这个地方设置过期时间直接写死了,可以在生产者发送消息时设置过期时间,这样更灵活。
//arguments.put("x-message-ttl",10000);
//正常队列设置死信交换机是谁:参数key 是固定值,参数v是死信交换机的名字
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//设置死信 routing-key: 参数 key 是固定值,参数v关键词的名字跟图中保持一致
arguments.put("x-dead-letter-routing-key", "lisi");
channel.queueDeclare(normalQueue, false, false, false, arguments);
/**
* 绑定普通交换机与普通队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为
*/
channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
//声明死信队列
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue, false, false, false, null);
//绑定死信交换机与死信队列
channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
System.out.println("等待接收消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer01 接收到消息"+message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(normalQueue, true, deliverCallback, consumerTag -> {
});
}
}
コンシューマ C2 コード ( 以上步骤完成后 启动 C2 消费者 它消费死信队列里面的消息
)
package com.atguigu.rabbitmq.eight;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
/**
* 死信队列 实战
*
* 消费者02
*/
public class Consumer02 {
//死信交换机名称
private static final String deadQueue = "dead-queue";
public static void main(String[] argv) throws Exception {
//获取信道
Channel channel = RabbitMqUtils.getChannel();
System.out.println("等待接收死信队列消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("Consumer02 接收死信队列的消息" + message);
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(deadQueue, true, deliverCallback, consumerTag -> {
});
}
}
テストを開始します。最初にコンシューマ 01 を起動してからシャットダウンし、次にコンシューマ 02 を起動してから、プロデューサを起動します。
6.3.3. キューが最大長に達した
1. メッセージプロデューサーコードからTTL属性を削除します。
package com.atguigu.rabbitmq.eight;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
/**
* 死信队列之 生产者代码
*/
public class Producer {
//交换机的名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
public static void main(String[] argv) throws Exception {
//创建信道
try (Channel channel = RabbitMqUtils.getChannel()) {
/**
* 声明一个 exchange(交换机)
* 1.exchange 的名称
* 2.exchange 的类型
*/
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
//设置消息的 TTL(超时)时间 单位是ms毫秒 10000ms=10s
//AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().expiration("10000").build();
//该信息是用作演示队列个数限制
for (int i = 1; i <11 ; i++) {
/**
*发送一个消息
*1.发送到那个交换机 如果是入门级程序,没有考虑交换机,可以写空串,表示使用默认的交换机
*2.路由的 key 是哪个 使用默认的交换机写的是:队列的名称 指定了交换机写的是:routingKey关键词
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*/
String message="info"+i;
channel.basicPublish(NORMAL_EXCHANGE, "zhangsan", null, message.getBytes());
System.out.println("生产者发送消息:"+message);
}
}
}
}
2.C1 コンシューマーは次のコードを変更します ( 启动之后关闭该消费者 模拟其接收不到消息
)
//设置正常队列的长度限制
arguments.put("x-max-length", 6);
- 注: 有効期限 TTL テスト中、コンシューマ 1 はキューを生成するために開始されていますが、このときパラメータを変更して再度開始すると、エラーが報告されます。
- 解決策: 元のキューを削除し、再度開始してキューを再生成します。
3.C2 コンシューマ コードは変更されません (C2 コンシューマを開始)
6.3.4. メッセージの拒否
1. メッセージプロデューサーのコードは上記のプロデューサーのコードと同じです。
2.C1 コンシューマ コード ( 启动之后关闭该消费者 模拟其接收不到消息
)
package com.atguigu.rabbitmq.eight;
import com.atguigu.rabbitmq.utils.RabbitMqUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DeliverCallback;
import java.util.HashMap;
import java.util.Map;
/**
* 死信队列 实战
*
* 消费者01
*/
public class Consumer01 {
//普通交换机名称
private static final String NORMAL_EXCHANGE = "normal_exchange";
//死信交换机名称
private static final String DEAD_EXCHANGE = "dead_exchange";
public static void main(String[] argv) throws Exception {
//创建信道
Channel channel = RabbitMqUtils.getChannel();
//声明普通和死信交换机: 名称,类型(direct:路由模式)
channel.exchangeDeclare(NORMAL_EXCHANGE, BuiltinExchangeType.DIRECT);
channel.exchangeDeclare(DEAD_EXCHANGE, BuiltinExchangeType.DIRECT);
/**
* 声明普通队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
* 设置消息为死信后转发到死信交换机,参数类型为Map集合
*/
String normalQueue = "normal-queue";
//设置消息为死信后转发到死信交换机,参数类型为Map集合
Map<String, Object> arguments = new HashMap<>();
//过期时间 10s=10000ms 在这个地方设置过期时间直接写死了,可以在生产者发送消息时设置过期时间,这样更灵活。
//arguments.put("x-message-ttl",10000);
//正常队列设置死信交换机是谁:参数key 是固定值,参数v是死信交换机的名字
arguments.put("x-dead-letter-exchange", DEAD_EXCHANGE);
//设置死信 routing-key: 参数 key 是固定值,参数v关键词的名字跟图中保持一致
arguments.put("x-dead-letter-routing-key", "lisi");
//设置正常队列的长度限制
//arguments.put("x-max-length", 6);
channel.queueDeclare(normalQueue, false, false, false, arguments);
/**
* 绑定普通交换机与普通队列:
* 参数1:队列
* 参数2:交换机
* 参数3:关键词routingKey为
*/
channel.queueBind(normalQueue, NORMAL_EXCHANGE, "zhangsan");
//声明死信队列
String deadQueue = "dead-queue";
channel.queueDeclare(deadQueue, false, false, false, null);
//绑定死信交换机与死信队列
channel.queueBind(deadQueue, DEAD_EXCHANGE, "lisi");
System.out.println("等待接收消息.....");
//消费者接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
if(message.equals("info5")){
System.out.println("Consumer01 接收到消息" + message + "并拒绝签收该消息");
/*
* 拒绝应答:
* 1.消息标记 tag:每一个消息都有一个唯一的标识,表示应答的是哪一个消息。
* 2.是否重新放入队列, false:不放回,消息变为死信,该队列如果配置了死信交换机将发送到死信队列中
* true:消息放回队列
* */
channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false);
}else {
/**
* 确认手动应答:
* 1.消息标记 tag:每一个消息都有一个唯一的标识,表示应答的是哪一个消息。
* 2.是否批量应答未应答消息 false:不批量应答信道中的消息 true:批量
*/
System.out.println("Consumer01 接收到消息"+message);
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(normalQueue, false, deliverCallback, consumerTag -> {
});
}
}
3.C2 コンシューマ コードは変更されません。
启动消费者 1 然后再启动消费者 2
7. 遅延キュー
7.1.遅延キューの概念
- 遅延キューは、デッドレターキュー内のメッセージの TTL 有効期限です。
遅延キュー, キューは内部的に順序付けされており、その最も重要な機能は遅延属性に反映されています。遅延キュー内の要素は、指定された時間の前後に取り出され、処理されることが期待されます。簡単に言うと、遅延キューは指定された時間に処理する必要がある要素を格納するために使用されるキュー。
7.2. 遅延キューの使用シナリオ
- 10分以内に支払いがない注文は自動的にキャンセルされます
- 注文後、メッセージが遅延キューに送信されます。このメッセージの有効期限は 10 分です。10 分後、メッセージは遅延キューから出てきて、未払いの注文がキャンセルされます。
- 新しく作成したストアが 10 日以内に商品をアップロードしなかった場合、メッセージ リマインダーが自動的に送信されます。
- ユーザーが正常に登録した後、3 日以内にログインしない場合は SMS リマインダーが送信されます。
- ユーザーは払い戻しを開始しますが、3 日以内に処理されない場合は、関連する運用担当者に通知されます。
- 会議を予約した後、各参加者は会議に出席する予定時刻の 10 分前に通知を受ける必要があります。
これらのシナリオはすべて、イベント発生後またはイベント発生前の指定された時点で特定のタスクを完了する必要があるという特性を持っています。たとえば、注文生成イベントが発生すると、10 分後に注文の支払いステータスを確認し、未払いの注文を処理します。 order. 閉じる;スケジュールされたタスクを使ってデータを常時ポーリングして1秒に1回チェックして、処理が必要なデータを取り出して処理するという仕組みのようですが、これで完了ですよね?データ量が比較的少ない場合は確かに可能ですが、例えば「1週間以内に料金を支払わなかった場合は自動決済する」といった要件であれば、厳密な制限はなく1週間以内であれば可能です。大雑把に考えて、すべての未払い請求書をチェックするスケジュールされたタスクを実行することは、実際に実行可能な解決策です。ただし、「10 分以内に支払われない場合、注文はクローズされます」など、比較的大量のデータと強力な適時性を備えたシナリオの場合、短期的には未払いの注文データが大量に存在する可能性があります。イベント中には数百万、場合によっては数千万に達するレベルですが、このような膨大な量のデータに対してポーリング方式を引き続き使用することは明らかにお勧めできません。すべての注文を 1 秒以内に完了できない可能性があります。同時に、データベースに多大な負荷がかかり、ビジネス要件を満たすことができず、パフォーマンスも低下します。
- 要約:
- 少量のデータと幅広い適時性の場合 - タイマーを使用できます
- 大量のデータと強力な適時性の場合は、遅延キューを使用できます。
7.3. RabbitMQ の TTL
TTLとは何ですか? TTL は、RabbitMQ のメッセージまたはキューの属性であり、キュー内の 1 つのメッセージまたはすべてのメッセージの最大生存時間を示します。
単位はミリ秒です。つまり、メッセージに TTL 属性が設定されているか、TTL 属性が設定されたキューに入った場合、メッセージが TTL で設定された時間内に消費されない場合、そのメッセージは「デッド レター」になります。キューの TTL とメッセージの TTL の両方が設定されている場合は、小さい方の値が使用されます。TTL を設定するには 2 つの方法があります。
7.3.1. キュー設定 TTL
1 つ目は、キューの作成時にキューの「x-message-ttl」属性を設定することです。
7.3.2. メッセージ設定TTL
別の方法は、メッセージごとに TTL を設定することです。
7.3.3. 両者の違い
キューの TTL 属性が設定されている場合、メッセージの有効期限が切れると、メッセージはキューによって破棄されます(デッドレター キューが設定されている場合、メッセージはデッドレターキューにスローされます)。メッセージの有効期限が切れても、すぐには破棄されない可能性があります。メッセージの有効期限が切れたかどうかは、コンシューマに配信される前に判断されるため、破棄します。現在のキューに深刻なメッセージ バックログがある場合、期限切れのメッセージは長期間存続する可能性があります。さらに、注意すべき点は、TTL を設定しない場合は、メッセージが期限切れにならないことを意味し、TTL が 0 に設定されている場合は、その時点でコンシューマに直接配信できない限り、メッセージは破棄されることを意味します。
前のセクションでデッドレターキューを紹介し、TTL を紹介しました。この時点で、RabbitMQ を使用して遅延キューを実装するための 2 つの主要な要素が集まりました。次に、それらを融合し、少し味付けを加えるだけです。 、遅延行列. 焼きたてになります。考えてみてください、遅延キュー、メッセージの処理がどれだけ遅れているかを意味するものではありませんか? TTL は、メッセージがデッド レターになるまでにどれくらいの時間遅延するかを表します。一方、デッド レターになるメッセージ内部のメッセージはすべてすぐに処理されることが期待されるため、コンシューマはデッド レター キュー内のメッセージを消費し続けるだけで済みます。
7.4. スプリングブートの統合
7.4.1. プロジェクトの作成
7.4.2. 依存関係の追加
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--RabbitMQ 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--json转换包-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--swagger:界面测试包-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--RabbitMQ 测试依赖-->
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
7.4.3. 設定ファイルの変更
#配置连接rabbitmq的信息
spring.rabbitmq.host=192.168.10.120
#注意15672是访问管理界面的端口号
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
7.4.4. Swagger 設定クラスの追加
- インターフェースにアクセスするために使用されます。
package com.cn.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket webApiConfig(){
return new Docket(DocumentationType.SWAGGER_2)
.groupName("webApi")
.apiInfo(webApiInfo())
.select()
.build();
}
private ApiInfo webApiInfo(){
return new ApiInfoBuilder()
.title("rabbitmq 接口文档")
.description("本文档描述了 rabbitmq 微服务接口定义")
.version("1.0")
.contact(new Contact("enjoy6288", "http://atguigu.com",
"[email protected]"))
.build();
}
}
7.5. キューTTL
7.5.1. コードアーキテクチャ図
2 つのキュー QA と QB を作成し、2 つのキューの TTL をそれぞれ 10S と 40S に設定してから、スイッチ X とデッドレター スイッチ Y を作成します。これらのタイプは両方とも直接です。デッドレター キュー QD を作成します。それらのバインド関係は次のとおりです。 :
7.5.2. 設定ファイルのクラスコード
- 例証します:
- Springboot プロジェクトが以前に統合されていないときは、通常のスイッチ、遅延スイッチ、通常のキュー、遅延キューを含むすべての宣言がコンシューマで宣言されていました。
- Springboot プロジェクトを統合した後、これらのステートメントは別のクラス、つまり構成ファイル クラスに統合されます。
package com.cn.config;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* TTL队列 配置文件类代码
*/
@Configuration
public class TtlQueueConfig {
//普通交换机名称
public static final String X_EXCHANGE = "X";
//死信交换机名称
public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
//2个普通队列名称
public static final String QUEUE_A = "QA";
public static final String QUEUE_B = "QB";
//1个死信队列名称
public static final String DEAD_LETTER_QUEUE = "QD";
// 声明 xExchange(普通交换机)
@Bean("xExchange")
public DirectExchange xExchange(){
return new DirectExchange(X_EXCHANGE);
}
// 声明 yExchange(死信交换机)
@Bean("yExchange")
public DirectExchange yExchange(){
return new DirectExchange(Y_DEAD_LETTER_EXCHANGE);
}
//声明普通队列A ttl过期时间为10s 并绑定到对应的死信交换机
@Bean("queueA")
public Queue queueA(){
//指定map的长度,可以节省创建时间。
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL 单位是毫秒
args.put("x-message-ttl", 10000);
//durable:持久化的队列名 withArguments:设置参数
return QueueBuilder.durable(QUEUE_A).withArguments(args).build();
}
//声明普通队列B ttl过期时间为40s 并绑定到对应的死信交换机
@Bean("queueB")
public Queue queueB(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//声明队列的 TTL
args.put("x-message-ttl", 40000);
return QueueBuilder.durable(QUEUE_B).withArguments(args).build();
}
//声明死信队列 QD
@Bean("queueD")
public Queue queueD(){
return new Queue(DEAD_LETTER_QUEUE);
}
// 声明队列 A 绑定 X 交换机
@Bean
public Binding queueaBindingX(@Qualifier("queueA") Queue queueA,
@Qualifier("xExchange") DirectExchange xExchange){
//根据属性名注入参数
// 队列 交换机 关键词
return BindingBuilder.bind(queueA).to(xExchange).with("XA");
}
//声明队列 B 绑定 X 交换机
@Bean
public Binding queuebBindingX(@Qualifier("queueB") Queue queue1B,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queue1B).to(xExchange).with("XB");
}
//声明死信队列 QD 绑定关系
@Bean
public Binding deadLetterBindingQad(@Qualifier("queueD") Queue queueD,
@Qualifier("yExchange") DirectExchange yExchange){
return BindingBuilder.bind(queueD).to(yExchange).with("YD");
}
}
7.5.3. メッセージプロデューサーコード
package com.cn.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* 发送延迟消息 生产者
*
* http://localhost:8080/ttl/sendMsg/嘻嘻嘻
*/
@Slf4j
@RequestMapping("/ttl")
@RestController
public class SendMsgController {
//此对象用来发送消息:Springboot版本过高注入会报错,可以降低版本,在pom文件中修改springboot版本(2.6.0以下),之后重新加载jar包
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendMsg/{message}")
public void sendMsg(@PathVariable String message){
//{}为日志的占位符,后面的参数会替换掉占位符的位置
log.info("当前时间:{},发送一条信息给两个TTL队列:{}", new Date(), message);
/*
* 发送消息:
* 交换机
* routingKey
* 发送的消息
* */
rabbitTemplate.convertAndSend("X", "XA", "消息来自 ttl 为 10S 的队列: "+message);
rabbitTemplate.convertAndSend("X", "XB", "消息来自 ttl 为 40S 的队列: "+message);
}
}
Springboot のバージョンを下げます。
7.5.4. メッセージコンシューマーコード
package com.cn.consumer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Date;
/**
* 队列TTL 消费者
*/
@Slf4j
@Component //为了消费者能够实例化
public class DeadLetterQueueConsumer {
@RabbitListener(queues = "QD")//接收消息:队列名称
public void receiveD(Message message, Channel channel) throws IOException {
//将消息体转化为字符串
String msg = new String(message.getBody());
log.info("当前时间:{},收到死信队列信息{}", new Date().toString(), msg);
}
}
リクエストをするhttp://localhost:8080/ttl/sendMsg/嘻嘻嘻
最初のメッセージは 10 秒後にデッドレターメッセージになり、コンシューマによって消費され、二番目のメッセージは 40 秒後にデッドレターメッセージになり、消費されます。このようにして、遅延キューが完成します。
ただし、このように使用すると、新しい時間要件が追加されるたびに、新しいキューを追加する必要があることを意味しません。時間オプションは 10 秒と 40 秒の 2 つだけです。1 時間後に処理する必要がある場合キューは、会議室を予約して事前に通知するようなシナリオであれば、需要を満たすために無数のキューを追加する必要があるのではないでしょうか?
- 概要: 現在、10 秒の遅延でキューを書き込み、40 秒の遅延でキューを書き込みていますが、将来的には、遅延時間を増やすたびにキューを追加する必要がありますか? ? ? ?
- 回答: すべての要件を満たすキューを最適化して作成する必要があります。
7.6. 遅延キューの最適化
7.6.1. コードアーキテクチャ図
ここで新しいキュー QC が追加されますが、そのバインディング関係は次のとおりです。キューには TTL 時間が設定されず、遅延時間はメッセージ送信時にプロデューサーによって決定されます。
7.6.2. 設定ファイルのクラスコード
構成ファイル クラスに新しいキューを追加します。
- 新しいキュー名を定義します
- この新しいキューを作成します
- この新しく作成したキューを x スイッチにバインドします。
//普通队列名称
public static final String QUEUE_C = "QC";
//声明普通队列C ttl没有设置过期时间 并绑定到对应的死信交换机
@Bean("queueC")
public Queue queueC(){
Map<String, Object> args = new HashMap<>(3);
//声明当前队列绑定的死信交换机
args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
//声明当前队列的死信路由 key
args.put("x-dead-letter-routing-key", "YD");
//没有声明 TTL 属性
return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
}
//声明队列 C 绑定 X 交换机
@Bean
public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
@Qualifier("xExchange") DirectExchange xExchange){
return BindingBuilder.bind(queueC).to(xExchange).with("XC");
}
7.6.3. メッセージプロデューサーコード
- リクエストの送信時にタイムアウトを設定する新しいメソッドを作成します。
//开始发消息 消息 TTL
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
log.info("当前时间:{},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
/*
* 发送消息:
* 交换机
* routingKey
* 发送的消息
* 设置过期时间:类型为接口类型
* */
rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
//发送消息的时候 延迟时长
correlationData.getMessageProperties().setExpiration(ttlTime);
return correlationData;
});
}
リクエストをする
http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000
http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000
問題ないようですが、最初に、メッセージのプロパティで TTL を設定する方法 (メッセージごとに TTL を設定する) を使用すると、RabbitMQ はメッセージが時間通りに「終了」しない可能性があると紹介されました。最初のメッセージの有効期限が切れているかどうかを確認します。有効期限が切れている場合は、デッドレター キューにスローされます。最初のメッセージの遅延時間が非常に長く、2 番目のメッセージの遅延時間が非常に短い場合、2 番目のメッセージはメッセージは最初に実行されません。
-
現象: 最初のメッセージは 20 秒の遅延で設定され、2 番目のメッセージは 2 秒の遅延で設定されます。リクエストを実行したところ、確かに最初のデータは20秒近く遅れて実行されたことが分かりましたが、2番目のデータは明らかに2秒遅れて実行されるように設定されていたのに、実際には実行されていました。 14秒後。これは、2 番目のデータがすぐに実行される前に、最初のデータが実行されるのを待機することを意味します。
-
遅延キュー最適化の欠点: 遅延キューの最適化は、いつでも 1 つのキューを使用できるという要件をすでに満たしていますが、3 つ以上のメッセージが送信される場合にはシーケンスが発生し、2 番目のメッセージは後でしか送信できません。最初のメッセージが送信されます。
-
解決策: 遅延プラグインを使用する
7.7.Rabbitmq プラグインは遅延キューを実装します
上記の問題は確かに問題であり、TTL がメッセージ粒度で実装できず、設定された TTL 時間に間に合わなくなる場合は、一般的な遅延キューとして設計できません。では、どうやって解決するのでしょうか? 次に、この問題を解決していきます。
7.7.1. 遅延キュープラグインのインストール
公式https://www.rabbitmq.com/community-plugins.html
Web サイト、 rabbitmq_layed_message_exchange プラグインをダウンロードし、解凍して RabbitMQ の plugins ディレクトリに配置します。
- 公式ウェブサイトに入る
- MobaXterm を使用して、ダウンロードした圧縮パッケージを RabbitMQ インストール ディレクトリの下の plugins ディレクトリに置きます。
-
最初に: プラグイン ディレクトリ (
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins/
)を入力します。
-
2 番目: プラグインをこのディレクトリにアップロードします: (ここでは 3.8.0.ez バージョンが使用されます。プラグインのバージョンは、インストールされている Rabbitmq バージョンよりも低いことに注意してください)
-
RabbitMQ インストールディレクトリ配下の plugins ディレクトリに移動し、以下のコマンドを実行してプラグインを有効にし、RabbitMQ を再起動します。
- プラグイン ディレクトリを入力します。
cd /usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins/
- インストール (バージョン情報は必要ありません):
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- RabbitMQ を再起動します。
systemctl restart rabbitmq-server
- 効果
- 遅延メッセージの元の位置はキューにありますが、プラグインを使用すると、スイッチで遅延されます。
7.7.2. コードアーキテクチャ図
ここでは、新しいキュー遅延.queue とカスタム スイッチ遅延.exchange が追加されます。バインディング関係は次のとおりです。
7.7.3.設定ファイルのクラスコード
私たちのカスタム スイッチでは、これは新しい交換タイプです。このタイプのメッセージは遅延配信メカニズムをサポートしています。メッセージが配信された後、メッセージはターゲット キューにすぐには配信されませんが、mnesia (分散データ) に保存されます。 system) table. 配信時間に達すると、対象のキューに配信されます。
package com.cn.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
*
*/
@Configuration
public class DelayedQueueConfig {
//交换机
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
//队列
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
//routingkey
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
/**
* 声明交换机:
* 因为系统没有提供插件使用的交换机类型x-delayed-message,
* 所以只能使用自定义交换机来定义一个延迟交换机
*/
@Bean
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
args.put("x-delayed-type", "direct");//延迟类型
// 交换机的名称 类型 是否持久化 是否自动删除 其它的参数
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
//声明队列
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//交换机与队列进行绑定
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
@Qualifier("delayedExchange") CustomExchange delayedExchange) {
//根据属性名注入参数
//队列 交换机 关键词 构建
return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
7.7.4. メッセージプロデューサーコード
//开始发消息 基于插件的 消息 及 延迟的时间
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
log.info(" 当前时间:{},发送一条延迟{}毫秒的信息给队列delayed.queue:{}", new Date(),delayTime, message);
/*
* 发送消息:
* 交换机
* routingKey
* 发送的消息
* 设置过期时间:类型为接口类型
* */
rabbitTemplate.convertAndSend(DelayedQueueConfig.DELAYED_EXCHANGE_NAME, DelayedQueueConfig.DELAYED_ROUTING_KEY, message,
correlationData ->{
//发送消息的时候 延迟时长 单位:ms毫秒
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
}
7.7.5. メッセージコンシューマーコード
package com.cn.consumer;
import com.cn.config.DelayedQueueConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 消费者 基于插件的延迟消息
*/
@Slf4j
@Component
public class DelayQueueConsumer {
//监听消息
@RabbitListener(queues = DelayedQueueConfig.DELAYED_QUEUE_NAME) //队列名称
public void receiveDelayedQueue(Message message){
//将消息体转化为字符串
String msg = new String(message.getBody());
log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}
}
リクエストを行う:
http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000
http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000
期待どおり、2 番目のメッセージが最初に消費されました。
7.8. 概要
- 遅延メッセージ: 2 つの方法
- デッドレターに基づいて
- プラグインベース
遅延キューは、遅延処理が必要なシナリオで非常に役立ちます。RabbitMQ を使用して遅延キューを実装すると、信頼性の高いメッセージ送信、信頼性の高いメッセージ配信、メッセージが少なくとも 1 回消費されることを保証するデッドレター キューなどの RabbitMQ の機能を有効に活用できます。また、正しく処理されなかったメッセージは破棄されません。さらに、RabbitMQ クラスターの特性により、単一障害点の問題は適切に解決され、単一ノードの障害によって遅延キューが使用できなくなったり、メッセージが失われたりすることはありません。
もちろん、Java の DelayQueue を使用する、Redis の zset を使用する、Quartz を使用する、kafka のタイム ホイールを使用するなど、遅延キューには他にも多くのオプションがあります。これらの各メソッドには、適用可能なシナリオに応じて独自の特性があります。
8. リリース確認の詳細
運用環境では、不明な理由により、rabbitmq が再起動されます。RabbitMQ の再起動中に、プロデューサーのメッセージ配信が失敗し、メッセージが失われるため、手動の処理と回復が必要になります。そこで、RabbitMQ で信頼性の高いメッセージ配信を実現するにはどうすればよいかを考え始めました。特に、RabbitMQ クラスターが利用できないような極端な状況では、配信できないメッセージにどう対処するか:
- 例外の例:
应 用 [xxx] 在 [08-1516:36:04] 发 生 [ 错误日志异常 ] , alertId=[xxx] 。 由
[org.springframework.amqp.rabbit.listener.BlockingQueueConsumer:start:620] 触发。
应用 xxx 可能原因如下
服务名为:
异常为: org.springframework.amqp.rabbit.listener.BlockingQueueConsumer:start:620,
产 生 原 因 如 下 :1.org.springframework.amqp.rabbit.listener.QueuesNotAvailableException:
Cannot prepare queue for listener. Either the queue doesn't exist or the broker will not
allow us to use it.||Consumer received fatal=false exception on startup:
8.1. Springboot バージョンのリリースと確認
8.1.1. 確認メカニズム計画
- スイッチとキューのいずれかが存在しない限り、メッセージは失われます。
8.1.2. コードアーキテクチャ図
8.1.3.設定ファイル
設定ファイルに追加する必要があります
spring.rabbitmq.publisher-confirm-type=correlated
- NONE は
リリース確認モードを無効にします。これがデフォルト値です。 - CORRELATED (推奨)
コールバック メソッドは、メッセージが交換に正常にパブリッシュされた後にトリガーされます。 - SIMPLE (以前に学習した単一の同期確認と同等、より遅い) には、
次の 2 つの効果があることがテストされています。- エフェクトの 1 つは、 CORRELATED 値と同様にコールバック メソッドをトリガーします。
- 次に、メッセージのパブリッシュに成功した後、rabbitTemplate を使用して waitForconfirms または waitForconfirmsOrDie メソッドを呼び出し、ブローカー ノードが送信結果を返すのを待ち、返された結果に基づいて次のステップのロジックを決定します。メソッドが false を返すと、チャネルが閉じられ、次のステップに進むことができなくなります。ブローカーにメッセージを送信します。
- NONE は
#配置连接rabbitmq的信息
spring.rabbitmq.host=192.168.10.120
#注意15672是访问管理界面的端口号
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=123456
spring.rabbitmq.publisher-confirm-type=correlated
8.1.4. 設定クラスの追加
package com.cn.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类 发布确认(高级)
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
//队列
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//routingkey
public static final String CONFIRM_ROUTING_KEY = "key1";
//声明交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(CONFIRM_EXCHANGE_NAME);
}
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
//return new Queue(CONFIRM_QUEUE_NAME); //方式一
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build(); //方式二
}
// 声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
//根据属性名注入参数
// 队列 交换机 关键词
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
}
8.1.5. メッセージプロデューサー
package com.cn.controller;
import com.cn.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
/**
* 开始发消息 发布确认(高级)
* http://localhost:8080/confirm/sendMessage/大家好
*/
@RestController
@RequestMapping("/confirm")
@Slf4j
public class ProducerController {
//此对象用来发送消息:Springboot版本过高注入会报错,可以降低版本,在pom文件中修改springboot版本(2.6.0以下),之后重新加载jar包
@Autowired
private RabbitTemplate rabbitTemplate;
//发送消息
@GetMapping("sendMessage/{message}")
public void sendMessage(@PathVariable String message){
/*
* 正常情况下发送消息:交换机正常+队列正常
* 交换机
* routingKey
* 发送的消息
* 填写回调消息的ID及相关信息(有不同重载的构造方法)
* */
//指定消息 id 为 1
CorrelationData correlationData1 = new CorrelationData("1");
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY,
message, correlationData1);
log.info("发送消息内容:{}",message);
//指定消息 id 为 2
CorrelationData correlationData2 = new CorrelationData("2");
//异常情况下发送消息 交换机错误(写错交换机名字)+队列正常
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME+"123",ConfirmConfig.CONFIRM_ROUTING_KEY,
message, correlationData2);
log.info("发送消息内容:{}",message);
//指定消息 id 为 3
CorrelationData correlationData3 = new CorrelationData("3");
//异常情况下发送消息 交换机正常+队列错误(交换机绑定队列需要正确的关键词,写错routingKey,这样队列就接收不到消息了)
rabbitTemplate.convertAndSend(ConfirmConfig.CONFIRM_EXCHANGE_NAME,ConfirmConfig.CONFIRM_ROUTING_KEY+"2",
message, correlationData3);
log.info("发送消息内容:{}",message);
}
}
8.1.6.コールバックインターフェース
package com.cn.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* 回调接口
*/
@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
//实现的是RabbitTemplate的一个内部接口
/**
* 问题:当前类实现的的是RabbitTemplate的一个内部接口,这样会导致当前
* 类MyCallBack不在这个RabbitTemplate接口对象中,所以导致RabbitTemplate
* 在调用自身接口ConfirmCallback时,根本调不到这个实现类MyCallBack。
*
* 解决:把这个实现类在注入到RabbitTemplate接口中
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 此方法并没有被调用,所以加上@PostConstruct注解,作用是:
* @PostConstruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,
* 也可以理解为在spring容器初始化的时候执行该方法。
*/
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息 交换机接收到了 回调
* 1.1 correlationData 保存回调消息的ID及相关信息 它是来自于生成者发送消息时自己填写的
* 1.2 交换机是否收到消息 ack = true 表示收到消息
* 1.3 失败的原因:如果发送成功,则此参数为null
*
* 2.发消息 交换机接收失败了 回调
* 2.1 correlationData 保存回调消息的ID及相关信息
* 2.2 交交换机是否收到消息 ack = false 表示没有收到消息
* 2.3 cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//获取id
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
}
}
}
8.1.7. メッセージコンシューマ
package com.cn.consumer;
import com.cn.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 接收消息 发布确认(高级)
*/
@Component
@Slf4j
public class ConfirmConsumer {
//监听消息
@RabbitListener(queues = ConfirmConfig.CONFIRM_QUEUE_NAME) //队列名称
public void receiveMsg(Message message){
//将消息体转化为字符串
String msg=new String(message.getBody());
log.info("接受到队列 confirm.queue 消息:{}",msg);
}
}
8.1.8. 結果の分析
- リクエストの送信:
http://localhost:8080/confirm/sendMessage/大家好
- ケース1: 通常の状況でのメッセージの送信: スイッチは正常 + キューは正常
効果: スイッチとキューの両方がメッセージを受信します
- 状況2: 異常な状況下でメッセージを送信するときのスイッチのエラー (間違ったスイッチ名) + キューの通常の
影響: スイッチのキューはメッセージを受信できません
- シナリオ3: 異常な状況でメッセージを送信すると、スイッチは正常 + キュー エラーになります (スイッチはキューをバインドするために正しいキーワードが必要ですが、routingKey が正しく書き込まれていないため、キューはメッセージを受信できません)。はメッセージを受信しますが、キューは
メッセージを受信できません。(メッセージの RoutingKey がキューの BindingKey と一致せず、他のキューがメッセージを受信できないため、メッセージは直接破棄されます。)
- ケース1: 通常の状況でのメッセージの送信: スイッチは正常 + キューは正常
質問: スイッチで例外が発生しましたが、コールバック インターフェイスを通じて、どこで例外が発生したか (スイッチが確認して応答したか) を知ることができます。キューで例外が発生した場合、メッセージは直接失われ、エラー メッセージは表示されません (キューは確認も応答も行いません)。? ?
解決策: Mandatory パラメータを使用してフォールバック メッセージを設定します。
8.2. ロールバックメッセージ
8.2.1.必須パラメータ
プロデューサ確認メカニズムのみが有効な場合、スイッチはメッセージを受信した後、確認メッセージをメッセージ プロデューサに直接送信します。メッセージがルーティングできない(スイッチがメッセージをキューに送信できない)ことが判明した場合、メッセージはこの時点では、プロデューサーはメッセージが破棄されたことを知りません。では、ルーティングできないメッセージを処理する方法を見つけるにはどうすればよいでしょうか? 少なくとも私が自分で対処できるように知らせてください。必須パラメータを設定すると、メッセージ配信中に宛先に到達できない場合にメッセージをプロデューサーに返すことができます。
- 概要: スイッチで例外が発生した場合、コールバック インターフェイスを介してメッセージをロールバックできます。ただし、キューで例外が発生した場合、メッセージは直接失われます。例外が発生した場合にメッセージをロールバックしたい場合は、キューで、これを実現するために必須パラメータを設定できます。
8.2.2. 設定クラスに設定を追加する
#发布退回消息:一旦消息发送不到队列,它会回退消息给生产者
spring.rabbitmq.publisher-returns=true
8.2.3. メッセージプロデューサーコード
同上 8.1.5
8.2.4. コールバックインターフェースの変更
package com.cn.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* 回调接口
*/
@Component
@Slf4j
//实现的是RabbitTemplate的两个内部接口
//ConfirmCallback:交换机确认回调接口
//ReturnsCallback:消息发送失败,回退消息接口
public class MyCallBack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
/**
* 问题:当前类实现的的是RabbitTemplate的一个内部接口,这样会导致当前
* 类MyCallBack不在这个RabbitTemplate接口对象中,所以导致RabbitTemplate
* 在调用自身接口ConfirmCallback时,根本调不到这个实现类MyCallBack。
*
* 解决:把这个实现类在注入到RabbitTemplate接口中
*/
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 此方法并没有被调用,所以加上@PostConstruct注解,作用是:
* @PostConstruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,
* 也可以理解为在spring容器初始化的时候执行该方法。
*/
@PostConstruct
public void init(){
//注入
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
/**
* 交换机确认回调方法
* 1.发消息 交换机接收到了 回调
* 1.1 correlationData 保存回调消息的ID及相关信息 它是来自于生成者发送消息时自己填写的
* 1.2 交换机是否收到消息 ack = true 表示收到消息
* 1.3 失败的原因:如果发送成功,则此参数为null
*
* 2.发消息 交换机接收失败了 回调
* 2.1 correlationData 保存回调消息的ID及相关信息
* 2.2 交交换机是否收到消息 ack = false 表示没有收到消息
* 2.3 cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
//获取id
String id=correlationData!=null?correlationData.getId():"";
if(ack){
log.info("交换机已经收到 id 为:{}的消息",id);
}else{
log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
}
}
//可以在当消息传递过程中不可达目的地时 将消息返回给生产者
// 只有不可达目的地的时候 才进行回退
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
log.error(" 消息:{},被交换机:{}退回,退回原因:{},路由key:{}",
new String(returnedMessage.getMessage().getBody()),returnedMessage.getExchange(),
returnedMessage.getReplyText(),returnedMessage.getRoutingKey());
}
}
8.2.5. 結果の分析
- リクエストを送信:
http://localhost:8080/confirm/sendMessage/大家好
- 効果: メッセージをキューに送信できない場合でも、メッセージが失われないようにロールバック処理を実行できます。
8.3.バックアップスイッチ
必須パラメータとフォールバック メッセージを使用すると、配信できないメッセージを認識できるようになり、プロデューサーのメッセージが配信できない場合に検出して処理する機会が得られます。しかし、場合によっては、これらのルーティングできないメッセージを処理する方法がわからない場合もあり、せいぜいログに記録し、アラームをトリガーして、手動で処理することしかできません。特にプロデューサが配置されているサービスに複数のマシンがある場合、これらのルーティング不可能なメッセージをログを介して処理するのは非常に洗練されておらず、ログを手動でコピーするのはより面倒でエラーが発生しやすくなります。さらに、必須パラメータを設定するとプロデューサの複雑さが増すため、これらの返されたメッセージを処理するロジックを追加する必要があります。メッセージを失いたくないが、プロデューサーの複雑さは増やしたくない場合はどうすればよいでしょうか? デッド レター キューの設定に関する前回の記事で、処理に失敗したメッセージを保存するキューにデッド レター スイッチを設定できると述べましたが、これらのルーティング不可能なメッセージはキューに入る機会がありません。したがって、デッドレターキューをメッセージの保存に使用することはできません。RabbitMQ には、この問題にうまく対処できるバックアップ スイッチ メカニズムがあります。バックアップスイッチとは何ですか? バックアップ スイッチは、RabbitMQ ではスイッチの「スペア タイヤ」として理解できます。特定のスイッチに対応するバックアップ スイッチを宣言すると、そのスペア タイヤが作成されます。スイッチがルーティングできないメッセージを受信すると、メッセージは次のようになります。バックアップ スイッチに転送され、バックアップ スイッチがそれらを転送して処理します。通常、バックアップ スイッチのタイプはファンアウトであるため、すべてのメッセージはそれにバインドされたキューに配信できます。その後、キューをバインドして、ルーティングできないすべてのメッセージが送信されるようにします。元のスイッチによってこのキューに入ります。もちろん、アラーム キューを作成し、独立したコンシューマを使用して監視とアラームを行うこともできます。
要約:
- 以前: スイッチに問題があり、メッセージを受信できない場合、スイッチはメッセージの解放を確認し、プロデューサーにメッセージを再送信させる必要がありました。キューに問題がある場合は、メッセージもロールバックして再送信する必要があります。
- 現在: バックアップ スイッチを使用することも可能です。バックアップ スイッチを使用すると、メッセージをプロデューサーにロールバックする必要がなくなります。スイッチにメッセージを届けられなくなった場合は、スイッチからバックアップスイッチにメッセージを送信し、バックアップスイッチからルーティングキューを介してコンシューマにメッセージを送信することで、メッセージを失わないという目的も達成できます。
- 利点: メッセージを監視し、警告を発することができます。
8.3.1. コードアーキテクチャ図
- プロデューサ、確認スイッチ、確認キュー、バインディング関係、およびコンシューマがすべて記述されているので、あとはバックアップ スイッチ、2 つのキュー (バックアップ キュー、アラーム キュー)、および 2 つのコンシューマ (バックアップ コンシューマ、アラーム コンシューマ) を追加するだけです。アラーム コンシューマが受信できる限り、バックアップ コンシューマもそれを受信できるため、アラーム コンシューマは書き込む必要はありません。)
8.3.2. 設定クラスの変更
package com.cn.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类 发布确认(高级)
*/
@Configuration
public class ConfirmConfig {
//交换机
public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
//队列
public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
//routingkey
public static final String CONFIRM_ROUTING_KEY = "key1";
//备份交换机
public static final String BACKUP_EXCHANGE_NAME = "backup.exchange";
//备份队列
public static final String BACKUP_QUEUE_NAME = "backup.queue";
//警告队列
public static final String WARNING_QUEUE_NAME = "warning.queue";
//声明确认交换机,以及确认交换机无法投递的消息将发送给备份交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
//确认交换机 持久化 参数(备份交换机) 构建
return ExchangeBuilder.directExchange(CONFIRM_EXCHANGE_NAME).durable(true)
.withArgument("alternate-exchange",BACKUP_EXCHANGE_NAME).build();
}
// 声明确认队列
@Bean("confirmQueue")
public Queue confirmQueue(){
//return new Queue(CONFIRM_QUEUE_NAME); //方式一
return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build(); //方式二
}
// 声明确认队列绑定关系
@Bean
public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
@Qualifier("confirmExchange") DirectExchange exchange){
//根据属性名注入参数
// 队列 交换机 关键词
return BindingBuilder.bind(queue).to(exchange).with("key1");
}
//声明备份交换机
@Bean("backupExchange")
public FanoutExchange backupExchange(){
return new FanoutExchange(BACKUP_EXCHANGE_NAME);
}
// 声明备份队列
@Bean("backQueue")
public Queue backQueue(){
return QueueBuilder.durable(BACKUP_QUEUE_NAME).build();
}
// 声明警告队列
@Bean("warningQueue")
public Queue warningQueue(){
return QueueBuilder.durable(WARNING_QUEUE_NAME).build();
}
// 声明备份队列绑定关系
@Bean
public Binding backupBinding(@Qualifier("backQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
//发布订阅(扇出)类型就没必要写RoutingKey,它是以广播形式发送给所有队列。
return BindingBuilder.bind(queue).to(backupExchange);
}
// 声明报警队列绑定关系
@Bean
public Binding warningBinding(@Qualifier("warningQueue") Queue queue,
@Qualifier("backupExchange") FanoutExchange backupExchange){
return BindingBuilder.bind(queue).to(backupExchange);
}
}
8.3.3. アラームコンシューマー
package com.cn.consumer;
import com.cn.config.ConfirmConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* 报警消费者
*/
@Component
@Slf4j
public class WarningConsumer {
//接收报警消息
@RabbitListener(queues = ConfirmConfig.WARNING_QUEUE_NAME)
public void receiveWarningMsg(Message message) {
String msg = new String(message.getBody());
log.error("报警发现不可路由消息:{}", msg);
}
}
8.3.4. 試験上の注意事項
プロジェクトを再起動するときは、バインディング プロパティを変更したため、元のconfirm.exchange (確認スイッチ) を削除する必要があります。削除しないと、次のエラーが報告されます。
8.3.5. 結果の分析
プロデューサーは上記の通り:
テストリクエスト:http://localhost:8080/confirm/sendMessage/大家好
効果:
必須パラメータとバックアップ スイッチを一緒に使用できる場合、両方が同時に有効になっている場合、メッセージはどこに送信されますか? どちらの優先度が高いですか? 上記の結果は、バックアップ スイッチの優先度が最も高いという答えを示しています。
9.RabbitMQ その他の知識ポイント
9.1. べき等性
9.1.1. コンセプト
同じ操作に対してユーザーが開始した 1 つのリクエストまたは複数のリクエストの結果は一貫しており、複数回のクリックによって引き起こされる副作用はありません。最も単純な例は支払いです。ユーザーは商品を購入した後に支払います。代金は正常に引き落とされますが、結果が返されるとネットワークに異常があります。この時点でお金は引き落とされています。ユーザーがもう一度ボタンをクリックすると、支払いは正常に返され、残高を確認するとさらに金額が引き落とされ、取引記録も 2 つになっていることがわかりました。従来のシングルアプリケーションシステムでは、データ操作をトランザクションに落とし込み、エラー発生時に即座にロールバックするだけで済みましたが、クライアントへの応答時にネットワークの中断や例外が発生する場合もあります。
9.1.2. メッセージの繰り返しの消費
コンシューマーが MQ でメッセージを消費すると、MQ はメッセージをコンシューマーに送信しました。コンシューマーが MQ に ack 応答を返したときにネットワークが中断されました。そのため、MQ は確認情報を受信せず、メッセージは他のサーバーに再送信されます。または、ネットワークが再接続された後に再度コンシューマに送信されますが、実際にはコンシューマはメッセージを正常に消費しているため、コンシューマは重複したメッセージを消費することになります。
9.1.3. ソリューションのアイデア
MQ コンシューマーの冪等性に対する解決策は、一般的に、グローバル IDを使用するか、タイムスタンプや UUID などの一意の識別子を書き込むか、コンシューマーに MQ 内のメッセージを消費するように命令することです。また、MQ の ID を使用して判断することも、生成することもできます。独自のルールに基づく 1 つ グローバルに一意の ID メッセージが消費されるたびに、この ID を使用して、メッセージが消費されたかどうかが最初に判断されます。
9.1.4. 消費者側での冪等性の保証
大量の注文が発生するビジネスのピーク時には、プロダクション側でメッセージが繰り返されることがあります。このとき、コンシューマ側では冪等性を実装する必要があります。つまり、同じニュースを受け取ったとしても、メッセージが複数回消費されることはありません。 。業界では 2 つの主流の冪等操作があります: a. データベースの主キーを使用して重複を排除する固有の ID + フィンガープリント コード メカニズム、および b. Redis のアトミック性を使用して達成する
9.1.5. 固有の ID + 指紋コードのメカニズム
フィンガープリント コード: 弊社のルールまたはタイムスタンプの一部に、他のサービスから提供される一意の情報コードを加えたものです。必ずしも弊社のシステムによって生成されるわけではありません。基本的に弊社のビジネス ルールから継ぎ足されますが、一意性が保証されている必要があります。その後、クエリ ステートメントを使用します。 ID がデータベース内に存在するかどうかを確認します。利点は、実装が簡単で、スプライスを 1 つだけ実行し、それが繰り返されているかどうかを確認するクエリを実行できることです。欠点は、同時実行性が高く、単一のデータベースの場合、もちろん、データベースとテーブルを分割することもできますが、パフォーマンスは向上しますが、最も推奨される方法ではありません。
9.1.6.Redis のアトミック性 (推奨)
redis を使用して setnx コマンドを実行することは、当然冪等です。繰り返し消費しないように
9.2.優先キュー
9.2.1.使用シナリオ
当社のシステムには注文リマインダーのシナリオがあります。お客様が Tmall で注文すると、タオバオは時間内に注文をプッシュします。ユーザーが設定した時間内に支払いが行われない場合は、テキスト メッセージのリマインダーがプッシュされます。ユーザー。非常に単純な機能ですよね。しかし、私たちにとって、Tmall 販売者は大口顧客と小規模顧客に分けられる必要がありますよね? たとえば、 Apple や Xiaomi のような大規模販売者は、少なくとも 1 年で私たちに多大な利益をもたらしてくれます。 , したがって、当然のことながら、その注文は優先的に処理される必要があります。以前は、私たちのバックエンド システムは定期的なポーリングを保存するために redis を使用していました。redis は単純なメッセージ キューを作成するためにのみ List を使用できることを誰もが知っていますが、これは実装できません。優先シナリオ、したがって、注文量が多い場合、RabbitMQ は変換と最適化に使用されます. 注文が大口顧客からのものであることが判明した場合は、比較的高い優先度が与えられますが、それ以外の場合は、デフォルトの優先度になります。
- メッセージNo.7の送信優先度は3、メッセージNo.9の送信優先度は4…
- 通常の状況では、キューからコンシューマに送信されるメッセージは先入れ先出しになります。優先キューが使用されると、キュー内のメッセージはキューに入れられます。数値が大きいほど優先度が高く、最初に消費されます。 。
9.2.2.追加方法
1) コンソール ページを追加する (方法 1: 非推奨)
2) コード方式(方式2:推奨)
a. キュー内のコードに優先順位を追加します。
Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);
b. メッセージ内のコードに優先順位を追加します。
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
3) 注意事項
キューが優先順位を実装するために行う必要があることは、次のとおりです。キューを優先キューとして設定する必要があり、メッセージにメッセージの優先順位を設定する必要があり、コンシューマはメッセージを待つ必要があります。この方法では、メッセージがソートされる可能性があるため、消費する前にキューに送信されます。
- コンシューマーがメッセージを消費する前にメッセージがキューに送信されるまで待機する必要があることを確認するには、最初にプロデューサーを開始し、次にコンシューマーを開始する必要があります。このようにして、100 個のデータがキューに送信された後、メッセージはキューに送信されます。消費される前に、高い優先度に従ってソートされます。最初にコンシューマを開始し、次にプロデューサを開始すると、1 つのメッセージを受信して 1 つを消費することと同じになるため、優先順位の効果は見られません。
9.2.3. 実際の戦闘
- コードを書き直すのではなく、以前のコードを使用します。
a.メッセージプロデューサー
package com.atguigu.rabbitmq.one;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.HashMap;
import java.util.Map;
/**
* 生产者:发消息
*/
public class Producer {
//队列名称
private final static String QUEUE_NAME = "hello";
//发消息
public static void main(String[] args) throws Exception{
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//工厂ip 连接RabbitMQ的队列
factory.setHost("192.168.10.120");
//用户名
factory.setUsername("admin");
//密码
factory.setPassword("123456");
//创建连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
//入门级测试,这里直接连接的是队列,没有连接交换机,用的是默认的交换机。
/**
* 生成一个队列,参数解释:
* 1.queue:队列名称
* 2.durable:是否持久化,当MQ重启后,还在 (true:持久化,保存在磁盘 false:不持久化,保存在内存)
* 3.exclusive: false不独占,可以多个消费者消费。true只能供一个消费者进行消费
* 功能1:是否独占。只能有一个消费者监听这个队列
* 功能2:当Connection关闭时,是否删除队列
* 4.autoDelete:是否自动删除 最后一个消费者端开连接以后 该队列是否自动删除 true自动删除 false不自动删除
* 5.arguments:其它参数,在高级特性中讲,暂时设置为null
*/
Map<String, Object> params = new HashMap();
//官方允许是0-255之间 此处设置10 允许优化级范围为0-10 不要设置过大 浪费CPU与内存
params.put("x-max-priority", 10);
channel.queueDeclare(QUEUE_NAME,true,false,false,params);
/* //发消息
String message = "hello world"; //初次使用
*//**
*发送一个消息
*1.发送到那个交换机 本次是入门级程序,没有考虑交换机,可以写空串
*2.路由的 key 是哪个 本次是队列的名称(前提:使用默认交换机)
*3.其他的参数信息 本次没有
*4.发送消息的消息体 不能直接发消息,需要调用它的二进制。
*//*
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕"); */
//改为发送多条消息
for(int i=1;i<11;i++){
String message = "info"+i;
//设置第5条消息的优先级,其它正常发布
if(i==5){
//构建优先级
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
//发送消息
channel.basicPublish("",QUEUE_NAME,properties,message.getBytes());
}else {
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
}
System.out.println("消息发送完毕");
}
}
b.メッセージコンシューマ
- 消費者は以前と同じ
package com.atguigu.rabbitmq.one;
import com.rabbitmq.client.*;
/**
* 消费者:接收消息的
*/
public class Consumer {
//队列名称
private final static String QUEUE_NAME = "hello";
//接收消息
public static void main(String[] args) throws Exception {
//创建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.10.120");//设置ip
factory.setUsername("admin");//设置用户名
factory.setPassword("123456");//设置密码
Connection connection = factory.newConnection();//创建连接
Channel channel = connection.createChannel();//通过连接创建信道
System.out.println("等待接收消息 ");
//推送的消息如何进行消费的接口回调 使用lambda表达式代替匿名内部类的写法
DeliverCallback deliverCallback = (consumerTag, message) -> {
//直接输出message参数是对象的地址值,通过方法获取message对象的消息体并转化为字符串输出。
String mes = new String(message.getBody());
System.out.println(mes);
};
//取消消费的一个回调接口 如在消费的时候队列被删除掉了
CancelCallback cancelCallback = (consumerTag) -> {
System.out.println("消息消费被中断");
};
/**
*消费者接收消息
*1.消费哪个队列
*2.消费成功之后是否要自动应答 true 代表自动应答 false 手动应答
*3.消费者接收消息的回调
*4.消费者取消消息的回调
*
* 执行流程:在basicConsume()方法中接收到消息后,会把消息放到声明消息的接口里deliverCallback,
* 在使用此接口的实现类打印。
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
テスト: 最初にコンシューマを開始し、次にプロデューサを開始すると、優先度の高いメッセージが最初に消費されることがわかります。
9.3. レイジーキュー
9.3.1.使用シナリオ
RabbitMQ は、バージョン 3.6.0 から遅延キューの概念を導入しました。遅延キューは、メッセージをできる限りディスクに保存し、コンシューマが対応するメッセージを消費するときにのみメモリにロードされます。その重要な設計目標の 1 つは、より長いキューをサポートできるようにすること、つまり、より多くのメッセージをサポートできるようにすることです。ストレージ。遅延キューは、コンシューマがさまざまな理由 (コンシューマがオフラインになったり、ダウンタイムになったり、メンテナンスのためシャットダウンしたりするなど)によりメッセージを長期間消費できない場合に必要です。
デフォルトでは、プロデューサが RabbitMQ にメッセージを送信するとき、キュー内のメッセージは可能な限りメモリに保存されるため、メッセージはコンシューマにより速く送信されます。永続的なメッセージでも、ディスクに書き込まれる間、コピーがメモリ内に保持されます。RabbitMQ がメモリを解放する必要がある場合、メモリ内のメッセージをディスクにページングしますが、この操作には時間がかかり、キューの操作もブロックされるため、新しいメッセージを受信できなくなります。RabbitMQ の開発者は関連アルゴリズムをアップグレードしてきましたが、特にメッセージ量が特に大きい場合、その効果は依然として理想的ではありません。
- 短所: mq にメッセージを送信するとき、mq のキューが遅延キューである場合、メッセージはすぐにメモリではなくディスクに保存されます。コンシューマは、メッセージをディスクからメモリに読み取る前に、まずメッセージをディスクからメモリに読み込む必要があります。その後は消費が遅くなります。
9.3.2. 2 つのモード
キューにはデフォルトと遅延の 2 つのモードがあります。デフォルト モードはデフォルトであり、3.6.0 より前のバージョンでは変更の必要はありません。遅延モードは遅延キューのモードです。channel.queueDeclare メソッドを呼び出すときにパラメータで設定することも、ポリシーを通じて設定することもできます。両方のメソッドを同時に使用してキューを設定した場合、ポリシーはメソッドの方が優先されます。宣言を通じて既存のキューのモードを変更する場合は、最初にキューを削除してから、新しいキューを再宣言することしかできません。
キューを宣言する際、「x-queue-mode」パラメータを通じてキューのモードを設定できます。値は「default」と「lazy」です。次の例は、遅延キューの宣言の詳細を示しています。
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);
9.3.3. メモリオーバーヘッドの比較
100 万個のメッセージが送信され、各メッセージが約 1KB を占有する場合、通常のキューが占有するメモリは 1.2GB ですが、遅延キューは 1.5MB しか占有しません。
10.RabbitMQ クラスター
10.1.クラスタリング
10.1.1. クラスターを使用する理由
冒頭で、RabbitMQ サービスのインストールと実行方法を紹介しましたが、これらはスタンドアロン バージョンであり、現在の実際のアプリケーションの要件を満たすことができません。RabbitMQ サーバーでメモリ破損、マシンの停電、またはマザーボードの障害が発生した場合はどうすればよいでしょうか? 単一の RabbitMQ サーバーは 1 秒あたり 1,000 メッセージのスループットに対応できますが、アプリケーションが RabbitMQ サービスに 1 秒あたり 100,000 メッセージのスループットを要求する場合はどうなるでしょうか。スタンドアロンの RabbitMQ サービスのパフォーマンスを向上させるために高価なサーバーを購入するのは困難であり、実際的な問題を解決するには RabbitMQ クラスターの構築が鍵となります。
クラスター構築原理:
- 単一の Rabbitmq のパフォーマンスには限界があり、プロデューサーから送信される大量のメッセージに対処するには、クラスターを構築する必要があります。
- 図のように、ノード 1 にクラスタ No.2 とクラスタ No.3 が結合し、3 つで全体を形成しており、いずれかのノードにアクセスすることは、すべてのノードにアクセスすることと同等になります。将来的には、ノード 3 または 2 にノード 4 を追加すると、全体に追加するのと同じになります。
10.1.2.構築手順
- さらに 2 つの Rabbitmq のクローンを作成します
- IP を次のように変更します。
192.168.10.120、192.168.10.121、192.168.10.122
vim /etc/sysconfig/network-scripts/ifcfg-ens33
1. 3台のマシン(node1、node2、node3)のホスト名を変更します。
vim /etc/hostname
注: 再起動すると有効になります ( reboot
)
2. 各ノードが相互に認識できるように各ノードのhostsファイルを設定します。
- 3 つの Rabbitmq すべてに次の 3 つの情報を設定します。
# 配置ip对应主机名
vim /etc/hosts
192.168.10.120 node1
192.168.10.121 node2
192.168.10.122 node3
3. 各ノードのCookieファイルが同じ値であることを確認するノード1
でリモート操作コマンドを実行する接続を確認する:はい2回目の接続パスワード:123456(ログインユーザー名とパスワードを設定する:root 123456)
scp /var/lib/rabbitmq/.erlang.cookie root@node2:/var/lib/rabbitmq/.erlang.cookie
scp /var/lib/rabbitmq/.erlang.cookie root@node3:/var/lib/rabbitmq/.erlang.cookie
4. RabbitMQ サービスを開始し、同時に Erlang 仮想マシンと RBbitMQ アプリケーション サービスを開始します (3 つのノードでそれぞれ次のコマンドを実行します)。
rabbitmq-server -detached
5. ノード 2 で実行します。
#先关掉服务(rabbitmqctl stop 会将Erlang 虚拟机关闭,rabbitmqctl stop_app 只关闭 RabbitMQ 服务)
rabbitmqctl stop_app
#重置服务
rabbitmqctl reset
#把节点2加入到一号节点中
rabbitmqctl join_cluster rabbit@node1
#再重启(只启动应用服务)
rabbitmqctl start_app
6. ノード 3 で実行します。
rabbitmqctl stop_app
rabbitmqctl reset
#把节点3加入到2号节点中(因为2号节点已经加入到1号节点中了相当于是一个整体,那么3号加点加入到1号或者2号都可以)
#前提是2号节点已经启动好了
rabbitmqctl join_cluster rabbit@node2
rabbitmqctl start_app
7. クラスタのステータス
現在の 3 つのノードはすでに全体となっているため、誰でもアクセスできます。
rabbitmqctl cluster_status
8. ユーザーをリセットする必要があります
注: 3 つの Rabbitmq がクラスターを形成しているため、いずれかのノードでアカウントを作成すると、他のノードにもアカウントが作成されます。
- アカウントを作成する
rabbitmqctl add_user admin 123
- ユーザーの役割を設定する
rabbitmqctl set_user_tags admin administrator
- ユーザー権限を設定する
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*"
任意のアカウントにログインすると、元の 1 つのノードが現在の 3 つのノードに変更されたことがわかります。
9. クラスターノードを非アクティブ化します (ノード 2 とノード 3 のマシンで別々に実行します)。
# 想要解除2号节点,以下4个命令就在2号节点下执行
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl cluster_status
#之后在1号机上执行,让1号机忘记集群2号机
rabbitmqctl forget_cluster_node rabbit@node2(node1 机器上执行)
10.2. ミラーキュー(未定)
10.2.1. ミラーリングを使用する理由
RabbitMQ クラスター内に Broker ノードが 1 つしかない場合、このノードに障害が発生すると、サービス全体が一時的に使用できなくなり、メッセージの損失が発生する可能性もあります。すべてのメッセージを永続的に設定でき、対応するキューの永続属性も true に設定できますが、それでもキャッシュによって引き起こされる問題を回避することはできません。メッセージの送信時間とメッセージの送信時間の間にギャップがあるためです。ディスクに書き込まれ、フラッシュ操作が実行されますが、これは短いですが問題のある時間枠です。Publisherconfirm メカニズムにより、どのメッセージがディスクに保存されているかがクライアントに確実に認識されますが、一般的には、単一障害点が原因でサービスが利用できなくなることは避けたいものです。
ミラー キュー メカニズムを導入すると、キューをクラスター内の他のブローカー ノードにミラーリングできます。クラスター内のノードに障害が発生した場合、キューは自動的にミラー内の別のノードに切り替わり、サービスの継続性が確保されます。
- 問題: 現在、mq クラスターはまったく再利用できません。1 台目のマシンで作成されたキューは 2 台目のマシンには存在しません。1 台目のマシンがダウンすると、キューは消滅します。キューがあるからといって使用されることはありません。マシンが 3 つあります。キューは 3 つありますが、キューは 1 つだけです。キューはそのマシン上に作成され、他のマシンにはキューがまったくありません。
- 解決策: ミラー キューを使用します。つまり、マシンごとにバックアップを作成します。少なくとも 1 つのメッセージが 1 つのノードだけに送信されるべきではありません。たとえば、ノード No. 1 にメッセージが存在する場合、ノード No. に送信されるとメッセージは失われます。 . 1 が下がります。メッセージをノード 1 に送信すると、ノード 1 はメッセージをノード 2 にバックアップします。
- 注: ノード 2 とノード 3 の両方をバックアップすることもできますが、リソースが無駄になるだけです。メッセージが 100 万件もある場合、複数回バックアップするのはスペースの無駄です。
10.2.2.構築手順
1. 3 つのクラスター ノードを起動します。
2. ノードを見つけてポリシーを追加します (ポリシー)
3. メッセージを送信するためのキューをノード 1 に作成します。このキューにはミラー キューがあります。
4. ノード 1 を停止すると、ノード 2 がミラー キューになることがわかります。
5. クラスター全体にマシンが 1 台しか残っていない場合でも、キュー内のメッセージは消費される可能性があります。
これは、キュー内のメッセージがミラー キューによって対応するマシンに配信されることを意味します。
10.3.Haproxy+Keepalive は高可用性ロード バランシングを実現します
- 問題: 現在のコードはハードコードされており、1 つの MQ にしか接続できません。接続されている現在の MQ がダウンしている場合、プロデューサーはまだ現在の MQ に接続されており、メッセージを送信できません。(プロデューサーが MQ クラスターに接続するときに大きな問題が発生します。MQ に接続する IP は変更できません)
- 解決策: Haproxy などのサードパーティ ツールを使用します。
10.3.1. 全体的なアーキテクチャ図
10.3.2.Haproxy は負荷分散を実装します
HAProxy は、高可用性、負荷分散、TCPHTTP アプリケーションベースのプロキシを提供し、仮想ホストをサポートします。これは、Twitter、Reddit、StackOverflow、GitHub などの多くの有名なインターネット企業によって使用されている無料、高速、信頼性の高いソリューションです。HAProxy は、非常に多数の直接接続をサポートするイベント駆動型の単一プロセス モデルを実装しています。
nginx、lvs、haproxy の違いを拡張します。http://www.ha97.com/5646.html
10.3.3.構築手順
1. haproxy をダウンロードします (ノード 1 およびノード 2)
yum -y install haproxy
2.node1とnode2のhaproxy.cfgを変更します。
vim /etc/haproxy/haproxy.cfg
赤色の IP を現在のマシンの IP に変更する必要があります
3. 両方のノードで haproxy を開始します。
haproxy -f /etc/haproxy/haproxy.cfg
ps -ef | grep haproxy
4.アクセスアドレス
http://10.211.55.71:8888/stats
10.3.4.Keepalivedによりデュアルマシン(アクティブとバックアップ)のホットバックアップを実現
以前に構成した HAProxy ホストが突然クラッシュしたり、ネットワーク カードに障害が発生した場合、RbbitMQ クラスターには障害がないにもかかわらず、外部クライアントに対するすべての接続が切断され、致命的な結果が生じることを想像してください。ここで、独自のヘルスチェック機能とフェイルオーバーを実現するリソース引き継ぎ機能によって高可用性 (デュアルマシンホットバックアップ) を実現できる Keepalived を導入する必要があります。
10.3.5. 構築手順
1.キープアライブをダウンロードする
yum -y install keepalived
2.ノードnode1設定ファイル
vim /etc/keepalived/keepalived.conf
情報内の keepalived.conf を変更して置き換えます。
3.ノードnode2設定ファイル
- global_defs の router_id を変更する必要があります (nodeB など)。
- 次に、vrrp_instance_VI の状態を「BACKUP」に変更します。
- 最後に、優先度を 100 未満の値に設定します。
4.haproxy_chk.shを追加します。
(HAProxy サービスがハングアップした後も、バックアップに切り替えずに Keepalived が正常に動作しないようにするには、HAProxy サービスのステータスを検出するスクリプトをここに記述する必要があります。HAProxy サービスがハングアップすると、スクリプトは自動的にサービスを再起動します。 HAProxy サービス。成功しない場合は、バックアップに切り替えて作業を続行できるように、Keepalived サービスを閉じます)
vim /etc/keepalived/haproxy_chk.sh(可以直接上传文件)
修改权限 chmod 777 /etc/keepalived/haproxy_chk.sh
5. keepaliveコマンドを開始します(node1とnode2が開始します)。
systemctl start keepalived
6. Keepalived ログを観察する
tail -f /var/log/messages -n 200
7. 最後に追加された VIP を観察する
ip add show
8.node1 はキープアライブの閉じた状態をシミュレートします
systemctl stop keepalived
9. vip アドレスを使用して RabbitMQ クラスターにアクセスします
10.4.フェデレーションエクスチェンジ(フェデレーションエクスチェンジ)
10.4.1. 使用する理由
(ブローカー北京)、(ブローカー深セン)は互いに遠く離れており、ネットワークの遅延は直面しなければならない問題です。北京にビジネス (クライアント北京) があり、(ブローカー北京) に接続して、その中の取引所 A にメッセージを送信する必要があります。この時点のネットワーク遅延は非常に小さいです。(クライアント北京) はすぐにメッセージを取引所 A に送信できます。 Publisherconfirm 機構やトランザクション機構を使用すると、確認情報も迅速に受信できます。現時点では、深センに別のビジネス (クライアント深セン) が ExchangeA にメッセージを送信する必要があります。その後、(クライアント深セン) と (ブローカー北京) の間に大きなネットワーク遅延が発生します。(クライアント深セン) は一定量の遅延を経験します。 Exchange にメッセージを送信するときの時間 A. 遅延、特にパブリッシャー確認メカニズムまたはトランザクション メカニズムが有効になっている場合、(クライアント深セン)は(ブローカー北京)から確認情報を受信するまでに長い遅延が発生するため、必然的にこの送信のパフォーマンスが発生します。糸が減少し、ある程度の詰まりを引き起こすこともあります。
ビジネス (クライアント深セン) を北京のコンピューター室に展開することでこの問題は解決できますが、(クライアント深セン) が呼び出す他のサービスが深センに展開されると、新たな遅延の問題が発生し、すべてのサービスを展開できるとは限りません。コンピュータ室では、災害復旧をどのように実現できるでしょうか? ここでフェデレーション プラグインを使用すると、この問題をうまく解決できます。
- 問題: 2 つの地域はかなり離れているため、北京の顧客が北京のコンピュータ ルームのスイッチにアクセスする場合は問題ありませんが、北京の顧客が深センのコンピュータ ルームのスイッチにアクセスする場合、ネットワーク遅延が比較的大きくなります。
- 解決策: 北京のコンピューター ルームの情報を深センに同期し、深センのコンピューター ルームの情報を北京に同期して、2 つのコンピューター ルームの情報の一貫性を確保することで、北京の顧客は北京を訪問する際にデータ情報にすぐにアクセスできるようになります。----Federation Exchange (Federation Exchange) を使用する
10.4.2.構築手順
1. 各ノードが独立して実行されるようにする必要があります。
2. 各マシンでフェデレーション関連のプラグインを有効にします。
rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management
3. 概略図 (最初にコンシューマーを実行して、node2 で fed_exchange を作成します)
4. アップストリーム (node1) をダウンストリーム (node2) に構成します。
5.ポリシーの追加
6. 成功の前提条件
10.5.フェデレーションキュー
- スイッチ同期またはキュー同期を使用できます。
10.5.1. 使用する理由
フェデレーテッド・キューは、複数のブローカー・ノード (またはクラスター) 間で単一キューのロード・バランシングを提供できます。フェデレーテッド・キューは、1 つ以上の上流キューに接続し、これらの上流キューからメッセージを取得して、メッセージを消費するローカル・コンシューマーのニーズを満たすことができます。
10.5.2.構築手順
1. 概略図
2. アップストリームの追加 (上記と同じ)
3. ポリシーの追加
10.6.シャベル
10.6.1. 使用する理由
フェデレーションにも同様のデータ転送機能があり、Shovel は 1 つのブローカー (ソース、つまりソースとして) のキューからデータを確実かつ継続的に取得し、別のブローカーの交換機 (宛先、つまり、目的地)。送信元としてのキューと宛先としての交換は、同時に同じブローカー上に配置することも、異なるブローカー上に配置することもできます。Shovel は「シャベル」と訳せますが、これはより鮮明な比喩であり、この「シャベル」は一方の当事者からもう一方の当事者へのメッセージを「シャベル」することができます。Shovel は、ソースと宛先の接続、メッセージの読み取りと書き込み、接続エラーの処理を担当する優れたクライアント アプリケーションのように動作します。
- この機能は、ソース エンドを直接設定できることを除いて、フェデレーテッド スイッチおよびフェデレーテッド キューの機能と同じです。
10.6.2. 構築手順
1. プラグインを有効にする (必要なすべてのマシンを有効にする)
rabbitmq-plugins enable rabbitmq_shovel
rabbitmq-plugins enable rabbitmq_shovel_management
2. 原理図(送信元で送信されたメッセージは直接宛先キューに入る)
3. シャベルのソースと宛先を追加します