RabbitMQ公式サイト:https://www.rabbitmq.com
I.はじめに
メッセージ キュー (Message Queue) は、文字通りに理解すると、まずキューです。FIFO 先入れ先出しデータ構造 - キュー。メッセージキューとは、メッセージを格納するいわゆるキューです。メッセージ キューは、メッセージを保存するというキューの目的を解決するものではありませんが、通信の問題を解決します。
例えば、電子商取引の注文システムを例に挙げると、サービス間で同期通信を行うと、時間がかかるだけでなく、途中のネットワーク変動の影響も受けてしまい、高い成功率が保証できません。 。したがって、非同期通信方式を使用してアーキテクチャを変革します。
非同期通信 を使用してモジュール間の呼び出しを分離すると、システムのスループットを迅速に向上させることができます。上流システムはメッセージ送信業務の実行直後に結果を取得し、複数の下流サービスがそのメッセージをサブスクライブして自ら消費します。メッセージ キューを通じて、基礎となる通信プロトコルが保護されるため、切り離しと並列消費を実現できます。
2. RabbitMQ の概要
市場で人気のあるいくつかの MQ:
ActiveMQ、RocketMQ、Kafka、RabbitMQ。
- 言語サポート: ActiveMQ と RocketMQ は Java 言語のみをサポートしますが、Kafka は複数の言語をサポートでき、RabbitMQ は複数の言語をサポートします。
- 効率の観点では、ActiveMQ、RocketMQ、Kafka はすべてミリ秒レベルですが、RabbitMQ はマイクロ秒レベルです。
- メッセージ損失とメッセージの重複の問題: RabbitMQ には、メッセージの永続性と重複の問題に対して比較的成熟したソリューションがあります。
- 学習コスト: RabbitMQ は非常にシンプルです。RabbitMQ は Rabbit を利用しています
RabbitMQ は、Rabbit Company、最終的には Pivotal によって開発および保守されます。
RabbitMQ は、高度なメッセージ キュー プロトコルである AMQP プロトコルに厳密に従い、プロセス間で非同期メッセージを配信できるようにします。
3. RabbitMQのインストール(Dockerのインストール)
1. コンテナを起動します
docker run -d -p 15672:15672 -p 5672:5672 \
-e RABBITMQ_DEFAULT_VHOST=rabbitmq \
-e RABBITMQ_DEFAULT_USER=admin \
-e RABBITMQ_DEFAULT_PASS=admin \
--hostname myRabbit --name rabbitmq \
rabbitmq
パラメータの説明:
- -d : コンテナーをバックグラウンドで実行することを示します。
- -p : コンテナーのポート 5672 (パス ポート) と 15672 (バックエンド管理ポート) をホストにマップします。
- -e : 環境変数を指定します。
- RABBITMQ_DEFAULT_VHOST : デフォルトの仮想マシン名。
- RABBITMQ_DEFAULT_USER : デフォルトのユーザー名。
- RABBITMQ_DEFAULT_PASS : デフォルトのユーザーパスワード。
- --hostname :ホスト名を指定します ( RabbitMQ の重要な点は、いわゆる ノード名に基づいてデータを保存することです 。デフォルトはホスト名です)。
- --name Rabbitmq : コンテナ名を設定します。
- Rabbitmq : コンテナーによって使用されるイメージ名。
2.スタート rabbitmq_management
docker exec -it rabbitmq rabbitmq-plugins enable rabbitmq_management
3. RabbitMQ
バックエンド管理へのアクセス
- ブラウザにアドレスを入力すると、
http://虚拟机IP地址:15672
バックグラウンド管理ページにアクセスできます。 - デフォルトのユーザー名とパスワードは両方とも次のとおりです
admin
(コンテナーの作成時にユーザー名とパスワードを指定します)。
注: クラウド サーバーの場合は、関連するポートを忘れずに開いてください。
4. RabbitMQ アーキテクチャ
- パブリッシャー- プロデューサー: RabbitMQ で Exchange にメッセージをパブリッシュします。
- Consumer - Consumer: RabbitMQ のキュー内のメッセージをリッスンします。
- Exchange - スイッチ: プロデューサとの接続を確立し、プロデューサからメッセージを受信します。
- キュー- キュー: Exchange は指定されたキューにメッセージを配布し、キューとコンシューマーが対話します。
- ルート- ルーティング: スイッチはメッセージをキューにパブリッシュするためにどのような戦略を使用しますか?
1. シンプルなアーキテクチャ
2. RabbitMQ の完全なアーキテクチャ図
3. グラフィカル インターフェイスを表示し、仮想ホストを作成します。
仮想ホストは、rabbitmq を内部的に複数のホストに分割し、異なるユーザーが競合せずに使用できるようにするために使用されます。
新しいテスト ユーザーを作成し、/test 仮想ホストを追加し、/test を操作する権限を持つようにテスト ユーザーを設定します。
5. RabbitMQのキューモード
1. RabbitMQの通信方法
2.HelloWorldモード - シンプルキューモード
1) 新しい Maven プロジェクトを作成して、プロデューサーとコンシューマーの管理を容易にします。
2) メッセージプロデューサーを作成する(メッセージを送信する)
ステップ:
- my-priduer-demo という名前の SpringBoot プロジェクトを作成します。
- 依存関係を導入する
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>>5.10.0</version>
</dependency>
- 書き込みプロデューサー
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyProducerDemoApplication {
public static final String QUEUE_NAME = "my_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.连接Broker
// 1.1 获得连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("虚拟机地址");
factory.setPort(5672);
factory.setUsername("test_user");
factory.setPassword("test");
factory.setVirtualHost("/test"); //设置连接的虚拟主机
factory.setHandshakeTimeout(3000000);
// 1.2 从连接工厂获得连接对象
Connection connection = factory.newConnection();
// 1.3 获取chanel,用于之后发送消息的对象
Channel channel = connection.createChannel();
// 1.4 声明队列 (队列不存在则创建,存在则使用)
/*
* queue – 队列的名称 the name of the queue
* durable – 是否开启持久化 true if we are declaring a durable queue (the queue will survive a server restart)
* exclusive – 是否独占连接(只允许当前客户端连接) true if we are declaring an exclusive queue (restricted to this connection)
* autoDelete – 是否自动删除(长时间空闲未使用) true if we are declaring an autodelete queue (server will delete it when no longer in use)
* arguments – 用于封装描述队列中的其他数据 other properties (construction arguments) for the queue
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 1.5 定义消息
String message = "hello,rabbitmq!";
// 1.6 发送消息
/*
* exchange – 交换机(Hello world模式下,一定是空串,不能为Null) the exchange to publish the message to
* routingKey – 路由键(当exchange为空串时,路由键为队列名称) the routing key
* immediate – 立即的 true if the 'immediate' flag is to be set. Note that the RabbitMQ server does not support this flag.
* mandatory – 强制的 true if the 'mandatory' flag is to be set
* props – 封装描述消息的数据 other properties for the message - routing headers etc
* body – 消息体 the message body
*/
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送完毕!");
// 1.7 断开连接
channel.close();
connection.close();
}
}
クライアントで表示:
3) メッセージコンシューマを作成する
- 依存関係を導入する
- 書き込みコンシューマ
import java.nio.charset.StandardCharsets;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
public class MyConsumer {
public static final String QUEUE_NAME = "my_queue";
public static void main(String[] args) throws IOException, TimeoutException {
// 1.连接Broker
// 1.1 获得连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("虚拟机ip");
factory.setPort(5672);
factory.setUsername("test_user");
factory.setPassword("test");
factory.setVirtualHost("/test"); //设置连接的虚拟主机
factory.setHandshakeTimeout(3000000);
// 1.2 从连接工厂获得连接对象
Connection connection = factory.newConnection();
// 1.3 获取chanel,用于之后发送消息的对象
Channel channel = connection.createChannel();
// 1.4 创建一个Consumer对象,来处理消息----打印消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println(new String(body));
}
};
// 设置消费者监听某个队列
channel.basicConsume(QUEUE_NAME,consumer);
}
}
- コンソールを起動して表示する
- 管理ページを開くと、メッセージが消費用に確認されていないことがわかります。コンシューマを再起動すると、このメッセージが表示されます。
コンシューマがキューをリッスンしているときに AutoAck を true に設定するだけです。
// 设置消费者监听某个队列
channel.basicConsume(QUEUE_NAME,true,consumer);
単純なキューの問題:
複数のコンシューマーが同じキューを消費する場合。このとき、rabbitmq の公平なスケジューリング機構が有効になっているため、コンシューマの消費能力に関係なく、各コンシューマは公平に同じ数のメッセージを受信することができ、能力のある方がより多く働くという事態は 起こりません。
手動 ACK の問題:
コンシューマーが消費を終了したかどうかに関係なく、消費が完了したことをブローカーに通知するためにすぐに ACK が送信されます。つまり、ブローカーはすぐに次のメッセージをコンシューマーにプッシュします。コンシューマーの消費能力が弱い場合は、ブローカーはすぐに次のメッセージをプッシュします。メッセージの蓄積を引き起こすか、メッセージ キュー全体に影響を及ぼします。
解決策:手動 ACK
3. ワークキューモード:より多くのワークモードを実行できる人
自動 ACK を手動 ACK に変更する
- 消費者 1
public class MyConsumer2 {
public static final String QUEUE_NAME = "my_work_queue";
public static void main(String[] args) throws Exception {
// 1.连接Broker
// 1.1 获得连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("虚拟机ip");
factory.setPort(5672);
factory.setUsername("test_user");
factory.setPassword("test");
factory.setVirtualHost("/test"); //设置连接的虚拟主机
factory.setHandshakeTimeout(3000000);
// 1.2 从连接工厂获得连接对象
Connection connection = factory.newConnection();
// 1.3 获取chanel,用于之后发送消息的对象
Channel channel = connection.createChannel();
// 声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 声明一次只能消费一条消息
channel.basicQos(1);
// 1.4 创建一个Consumer对象,来处理消息----打印消息
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println(new String(body));
// 手动ASC,告诉Broker这条消息已经被消费,可以被移除队列,并且不需要批量确认消费
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 设置消费者监听某个队列,并修改ASC模式为手动
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
- 消費者 2
コンシューマ 1 に基づいて、コンシューマ 2 の消費電力がコンシューマ 1 よりも弱いことを反映するために、現在のスレッドを 3 秒間スリープします。
channel.basicQos(1) :一度に 1 つのメッセージのみを消費できることを宣言します
channel.basicAck(envelope.getdeliveryTag(),false) :手動 ASC。このメッセージは消費されておりメッセージ キューから削除でき、消費のバッチ確認は必要ないことをブローカーに伝えます。
- プロデューサー (100 個のメッセージをキューに送信)
public class MyProducerDemoApplication {
public static final String QUEUE_NAME = "my_work_queue";
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("虚拟机ip地址");
factory.setPort(5672);
factory.setUsername("test_user");
factory.setPassword("test");
factory.setVirtualHost("/test"); //设置连接的虚拟主机
factory.setHandshakeTimeout(3000000);
// 1.2 从连接工厂获得连接对象
Connection connection = factory.newConnection();
// 1.3 获取chanel,用于之后发送消息的对象
Channel channel = connection.createChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
for (int i = 0;i<=99;i++) {
// 1.5 定义消息
String message = "hello,rabbitmq!"+i;
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
}
System.out.println("发送完毕!");
// 1.7 断开连接
channel.close();
connection.close();
}
}
結果: 消費者 1 (通常の消費力) は 97 メッセージを消費し、消費者 2 (消費力が弱い) は 3 メッセージを消費しました。これは、作業モードでの
「より多くの仕事ができる人」を反映しています。
4.モデルファンアウトのパブリッシュとサブスクライブ
以前のキュー モデルでは、1 つのメッセージが複数のコンシューマーによって同時に消費されるという問題を解決する方法がありませんでした。したがって、パブリッシュ/サブスクライブ モデルを使用して実装します。
ステップ:
プロデューサはエクスチェンジを宣言し、メッセージをエクスチェンジに送信します (それ以上のメッセージはキューに送信されません)。
-->
コンシューマはキューとスイッチを宣言し、キューをスイッチにバインドします。
- 書き込みプロデューサー
public class MyProducer {
// 定义交换机名称
public static final String EXCHANGE_NAME = "my_fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = RabbitUtil.getConnection();
// 获取channel通道
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
// 2、生产消息,发送给交换机
for (int i = 0; i < 10; i++) {
String message = "message:"+i;
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes(StandardCharsets.UTF_8));
}
System.out.println("消息已全部发送!");
}
}
- コンシューマ 1 を書き込む
主なアクション:
キューの作成
スイッチの作成
キューをスイッチにバインドする
消費者にキューを聞いてもらう
public class MyConsumer1 {
private static String EXCHANGE_NAME = "my_fanout_exchange";
private static String QUEUE_NAME = "my_fanout_queue_1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println("消费者1:"+new String(body));
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
- コンシューマ 2 を書き込む
public class MyConsumer2 {
private static String EXCHANGE_NAME = "my_fanout_exchange";
private static String QUEUE_NAME = "my_fanout_queue_2";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"fanout");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"");
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println("消费者2:"+new String(body));
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
2 つのコンシューマと 1 つのプロデューサをそれぞれ起動し、両方のコンシューマがすべてのメッセージを受信したことを確認します。
5.ルーティングモードダイレクト
主なアクション:
プロデューサがメッセージを送信するときにルーティング キーを指定します
コンシューマがキューとスイッチ間のバインディング関係を宣言するときに、ルーティング キーを指定します。
- ライティングプロデューサー
スイッチにメッセージを送信するときは、ルーティング キーをapple として指定します。
public class MyProducer {
// 定义交换机名称
public static final String EXCHANGE_NAME = "my_routing_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 获取连接对象
Connection connection = RabbitUtil.getConnection();
// 获取channel通道
Channel channel = connection.createChannel();
// 1、声明路由模式的交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
// 2、生产消息,发送给交换机
String message = "apple-message:";
channel.basicPublish(EXCHANGE_NAME,"apple",null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("消息已全部发送!");
// 3、关闭连接
channel.close();
connection.close();
}
}
- コンシューマ 1 を書き込む
スイッチとキューをバインドするときは、ルーティング キーを apple として指定します
public class MyConsumer1 {
private static String EXCHANGE_NAME = "my_routing_exchange";
private static String QUEUE_NAME = "my_routing_queue_1";
private static String ROUTING_KEY = "apple";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定,并指定routingKey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUTING_KEY);
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println(ROUTING_KEY+":"+new String(body));
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
- コンシューマ 2 を書き込む
スイッチとキューをバインドする場合は、ルーティング キーをバナナとして指定します
public class MyConsumer2 {
private static String EXCHANGE_NAME = "my_routing_exchange";
private static String QUEUE_NAME = "my_routing_queue_2";
private static String ROUTING_KEY = "banana";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"direct");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定,并指定routingKey
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUTING_KEY);
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println(ROUTING_KEY+":"+new String(body));
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
それぞれ 2 つのコンシューマと 1 つのプロデューサを開始し、キューにバインドされたルーティング キーを持つコンシューマのみがメッセージを受信することがわかり、メッセージは指定された queue に送信されます。
6.トピックモード
ルーティング モードに基づいて、ルーティング キーにワイルドカードが使用され、一致範囲とプレイアビリティが向上します。
- *。オレンジ。*
- *.*.rabbit は単一レベルのみをサポートします
- Lazy.# はマルチレベルのルーティングキーをサポートできます
product.* がバインディング関係で使用されている場合、メッセージを送信するときは次のようになります。
- product.add OK product.del OK
- product.add.one は問題ありません
product.# がバインディング関係で使用されている場合、メッセージを送信するときは次のようになります。
- 製品を追加します。OK
- 製品.追加.1 つ OK
プロデューサーを書きます:
トピック モードを使用し、ルーティング キーをマルチレベルに設定します。
public class MyProducer {
public static final String EXCHANGE_NAME = "my_topic_exchange";
public static void main(String[] args) throws Exception {
// 获得连接对象与通道
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 发送消息
channel.basicPublish(EXCHANGE_NAME,"product.add.one",false,false,null,"hello,topic".getBytes(StandardCharsets.UTF_8));
// 关闭连接
channel.close();
connection.close();
}
}
コンシューマ 1 を書き込みます:
トピック モードを使用し、ルーティング キーを product に設定します。* (単一レベル)
public class MyConsumer1 {
// 交换机名称
private static String EXCHANGE_NAME = "my_topic_exchange";
// 队列名称
private static String QUEUE_NAME = "my_topic_queue_1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定,并指定routingKey,为 product.任意字符 都能接收
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"product.*");
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println("product.*消费者:"+":"+new String(body));
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
コンシューマ 2 を書き込みます:
トピック モードを使用し、ルーティング キーを product.# (マルチレベル) に設定します。
public class MyConsumer2 {
// 交换机名称
private static String EXCHANGE_NAME = "my_topic_exchange";
// 队列名称
private static String QUEUE_NAME = "my_topic_queue_2";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定,并指定routingKey,为 product.任意字符 都能接收
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"product.#");
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println("product.*消费者:"+":"+new String(body));
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
2 つのコンシューマと 1 つのプロデューサがそれぞれ起動され、キュー バインドされたルーティング キーが product.# であるコンシューマのみがメッセージを受信したことがわかりました。
6. Springboot での RabbitMQ の使用
1.依存関係を導入する
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
2. 設定ファイルの書き込み
server.port=8091
spring.rabbitmq.addresses=虚拟机ip地址
spring.rabbitmq.port=5672
spring.rabbitmq.username=test_user
spring.rabbitmq.password=test
spring.rabbitmq.virtual-host=/test #虚拟主机名
3. パブリッシュ/サブスクライブ モデルを使用する
レビューの公開と購読:
コンシューマはスイッチとキューを定義し、その 2 つをバインドします。
プロデューサはスイッチにメッセージを送信します。
1) 書き込みコンシューマ
- 構成クラスの書き込み
スイッチとキューを定義し、スイッチとキューをバインドする
/**
* RabbitMQ消费者配置类
* springBoot实现消息订阅模式
*/
@Configuration
public class MyRabbitConfig {
public static final String EXCHANGE_NAME = "my_boot_fanout_exchange";
public static final String QUEUE_NAME = "my_boot_fanout_queue1";
/**
* 声明交换机
*/
@Bean
public FanoutExchange exchange(){
return new FanoutExchange(EXCHANGE_NAME,true,false);
}
/**
* 声明队列
*/
@Bean
public Queue queue(){
return new Queue(QUEUE_NAME,true,false,false);
}
/**
* 绑定交换机与队列
*/
@Bean
public Binding queueBinding(Queue queue,FanoutExchange exchange){
return BindingBuilder.bind(queue).to(exchange);
}
}
- 消費メッセージの書き方
キー: このアノテーションを使用して、リスニング・キューを指定します @RabbitListener(queues = "リスニングするキューの名前")
@Component
public class MyConsumer {
/**
* 监听队列:当队列中有消息,则监听器工作,处理接收到的消息
* @param message 消息体
*/
@RabbitListener(queues = "my_boot_fanout_queue1")
public void process(Message message){
byte[] messageBody = message.getBody();
System.out.println(new String(messageBody));
}
}
2) プロデューサーを書きます
- 構成クラスの書き込み
プロデューサはスイッチにメッセージを送信するだけでよいため、スイッチを宣言するだけで済みます。
@Configuration
public class MyProducerConfig {
public static final String EXCHANGE_NAME = "my_boot_fanout_exchange";
/**
* 声明交换机
*/
@Bean
public FanoutExchange exchange(){
return new FanoutExchange(EXCHANGE_NAME,true,false);
}
}
- RabbitTemplate を使用してメッセージを送信する
@Autowired
RabbitTemplate rabbitTemplate;
public static final String EXCHANGE_NAME = "my_boot_fanout_exchange";
@Test
void testSendMsg(){
String msg = "Hello,SpringBootRabbitMQ!";
rabbitTemplate.convertAndSend(EXCHANGE_NAME,"",msg);
System.out.println("消息发送成功!");
}
4. トピックモードを使用する
パブリッシュ/サブスクライブ モードと比較して、トピック モードではルーティング キーが追加で使用されます。
1) コンシューマ構成クラスを調整します (ルーティングキーを指定する必要があります)
/**
* RabbitMQ消费者配置类
* springBoot实现Topic模式
*/
@Configuration
public class MyRabbitTopicConfig {
public static final String TOPIC_EXCHANGE_NAME = "my_boot_topic_exchange";
public static final String TOPIC_QUEUE_NAME = "my_boot_topic_queue";
/**
* 声明交换机
*/
@Bean
public TopicExchange exchange(){
return new TopicExchange(TOPIC_EXCHANGE_NAME,true,false);
}
/**
* 声明队列
*/
@Bean
public Queue queue(){
return new Queue(TOPIC_QUEUE_NAME,true,false,false);
}
/**
* 绑定交换机与队列
*/
@Bean
public Binding queueBinding(Queue queue,TopicExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("product.*"); // 能接受到routing-key为product.任意字符的消息(*单层 #多层)
}
}
また、コンシューマーがリッスンするキューを、ここで宣言されたキュー名に変更する必要があります。
2) プロデューサーを書きます
- プロデューサの構成クラスを調整する
public class MyTopicProducerConfig {
public static final String TOPIC_EXCHANGE_NAME = "my_boot_topic_queue";
/**
* 声明交换机
*/
@Bean
public TopicExchange exchange(){
return new TopicExchange(TOPIC_EXCHANGE_NAME,true,false);
}
}
- メッセージ送信時にルーティングキーを運ぶ
@Autowired
RabbitTemplate rabbitTemplate;
public static final String TOPIC_EXCHANGE_NAME = "my_boot_fanout_exchange";
@Test
void testSendMsg(){
String msg = "Hello,SpringBootRabbitMQ!";
rabbitTemplate.convertAndSend(TOPIC_EXCHANGE_NAME,"product.add",msg); //指定routing-key
System.out.println("消息发送成功!");
}
5. 手動ACKの実装
- 手動ACK設定を設定ファイルに追加します。
spring.rabbitmq.listener.direct.acknowledge-mode=manual
- コンシューマでの消費後に手動確認を実行する
@RabbitListener(queues = "my_boot_topic_queue")
public void process(Message message, Channel channel) throws IOException {
System.out.println("接收到的消息"+message.toString());
// 手动ACK,告知Broker确认已被消费的消息id(DeliveryTag)
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
このうち、メッセージのリクエスト ヘッダーにある 2 つのキーと値のペアは次のとおりです。
- spring_listener_return_correlation : この属性は、メッセージが返されたときにどのリスナーが呼び出されるかを決定するために使用されます。
- spring_returned_message_correlation : この属性は、確認される返されたメッセージの一意の識別子を参照します。
7. メッセージの信頼性の高い配信
メッセージの信頼性に対する 3 つの保証:
1. プロデューサはメッセージをスイッチに正確に配信します (確認メカニズムを使用)。
2. スイッチは、(Return メカニズムを使用して) メッセージをキューに正確に配信します。
3. キューはメッセージをコンシューマに正確にプッシュします (コンシューマ手動 ACK)
1. プロデューサーメッセージが確認メカニズムを通じて MQ に配信できることを確認する
- Spring プロジェクトでプロデューサーにメッセージを送信するときに確認を使用する
public class MyProducer {
public static final String EXCHANGE_NAME = "my_topic_exchange";
public static void main(String[] args) throws Exception {
// 获得连接对象与通道
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 开启confirm机制
channel.confirmSelect();
// 设置confirm监听器
channel.addConfirmListener(new ConfirmListener() {
// 消息被Broker确认接收了,将会回调此方法
@Override
public void handleAck(long l, boolean b) throws IOException {
// 消息发送成功
System.out.println("消息被成功投递!");
}
// 消息被Broker接收失败了,将会回调此方法
@Override
public void handleNack(long l, boolean b) throws IOException {
//开启重试机制,重试达到阈值,则考虑人工介入
System.out.println("消息投递失败!");
}
});
byte[] msg = "hello,confirm message".getBytes(StandardCharsets.UTF_8);
// 发送消息
channel.basicPublish(EXCHANGE_NAME,"product.add",false,false,null,msg);
}
}
メッセージを送信する前に、 channel.confirmSelect() を使用して確認メカニズムをオンにします 。
channel.addconfirmListener を使用して 確認リスナーを設定する
このうち、 handleAck は スイッチに正常に配信されたメッセージのコールバック関数です。
handleNack は 、スイッチに正常に配信されなかったメッセージのコールバック関数です。
- SpringBoot での確認の使用
ステップ 1: プロデューサ構成を変更します。
server.port=8091
spring.rabbitmq.addresses=虚拟机ip地址
spring.rabbitmq.port=5672
spring.rabbitmq.username=test_user
spring.rabbitmq.password=test
spring.rabbitmq.virtual-host=/test
spring.rabbitmq.publisher-confirm-type: correlated
Publisher-confirm-type: 3 つの構成があります。
- シンプル: ack 判定の単純な実行; メッセージのパブリッシュに成功した後、rabbitTemplate を使用して waitForconfirms メソッドまたは waitForconfirmsOrDie メソッドを呼び出し、ブローカー ノードが送信結果を返すのを待ち、返された結果に基づいて次のステップのロジックを判断します。ただし、waitForconfirmsOrDie メソッドが false を返した場合、チャネルは閉じられることに注意してください。
- 関連: ack を実行すると、データ (メッセージのメタデータ) も送信されます。
- none: リリース確認モードを無効にします、デフォルト
ステップ 2: confirmCallback の実装クラス (リスナー) を作成し、RabbitTemplateに注入する
@Component
public class MyConfirmCallfack implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
// 将监听器注入到RabbitTemplate中
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
}
/**
* @param correlationData 消息元数据(消息id,消息内容)
* @param ack 布尔值,Broker是否成功接收到消息
* @param cause 投递失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
// 消息的id
String id = correlationData.getId();
if (ack){
// 消息投递成功
System.out.println("消息投递成功,id为:"+id);
}else{
// 消息投递失败,可对失败的消息进行定时重试
System.out.println("消息投递失败,原因为:"+cause);
}
}
}
スイッチを存在しないスイッチに変更すると、メッセージ配信失敗のフィードバックが得られることがわかりましたが、間違ったルーティング キーが変更されてメッセージが正常に配信されなかった場合は、メッセージ配信失敗のフィードバックが得られません。これは、確認がプロデューサとスイッチの間のメッセージ配信状況のみに注意を払うためです。
2. 戻りメカニズムを使用して、メッセージが Rabbitmq のキューに正常に配信されることを確認します。
プロデューサは、確認メカニズムによって保証されて、メッセージを mq スイッチに配信します。
スイッチがメッセージをキューに配信できない場合は、Return メカニズムを通じて再試行できます。
ステップ 1: 構成ファイルを変更する
復帰機構がオンになっている場合。Mandatory を true に設定する必要があります。
server.port=8091
spring.rabbitmq.addresses=虚拟机IP地址
spring.rabbitmq.port=5672
spring.rabbitmq.username=test_user
spring.rabbitmq.password=test
spring.rabbitmq.virtual-host=/test
spring.rabbitmq.publisher-confirm-type: correlated
spring.rabbitmq.publisher-returns: true
ステップ 2: リスニング クラスに RabbitTemplate.ReturnCallback インターフェイスを実装する
import org.springframework.amqp.core.Message;
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
public class MyConfirmCallfack implements RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
// 将监听器注入到RabbitTemplate中
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* @param correlationData 消息元数据(消息id,消息内容)
* @param ack 布尔值,Broker是否成功接收到消息
* @param cause 投递失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
// 消息的id
String id = correlationData.getId();
if (ack){
// 消息投递成功
System.out.println("消息投递成功,id为:"+id);
}else{
// 消息投递失败,可对失败的消息进行定时重试
System.out.println("消息投递失败,原因为:"+cause);
}
}
/**
当消息未成功被投递到队列,调用此方法
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("消息"+new String(message.getBody()+"没有被成功投递到队列"));
}
}
3. 手動のACK、NACK、拒否の違い
1) ACK を行わない
RabbitMQ はメッセージを未確認としてマークします。この時点で、MQ はコンシューマが確認するのを待っています。コンシューマがセッションを失うと、メッセージは準備完了状態に戻り、他のコンシューマによって消費されます。
2)確認
受信を確認した後、メッセージはキューから削除されます。
3)拒否
「拒否」とは、このメッセージを拒否することを意味します。
「拒否」は、一度に 1 つのメッセージの処理のみをサポートします。メッセージが拒否され、再キューが false に設定されると、メッセージはデッドレター キューに入ります。requeue が true に設定されている場合、キューに戻りますが、この状況が使用されることはほとんどありません。
4)ナック
public class MyConsumer1 {
// 交换机名称
private static String EXCHANGE_NAME = "my_topic_exchange";
// 队列名称
private static String QUEUE_NAME = "my_topic_queue_1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定,并指定routingKey,为 product.任意字符 都能接收
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"product.*");
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
System.out.println("product.*消费者:"+":"+new String(body));
//⼿动ack
// channel.basicAck(envelope.getDeliveryTag(),false);
//reject拒签消息 ⼀次只⽀持处理⼀条消息
// channel.basicReject(envelope.getDeliveryTag(),false);
//nack 拒签消息 ⽀持批处理多条消息
channel.basicNack(envelope.getDeliveryTag(), true,false);
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,true,consumer);
}
}
5) メッセージメタデータのカプセル化
プロデューサがメッセージを送信する前に、メッセージが永続的かどうか、メッセージの有効期限、メッセージ ID、カスタマイズされたマップ データなどのメッセージのメタデータを構築できます。
プロデューサー側:
public class MyProducer {
public static final String EXCHANGE_NAME = "my_topic_exchange";
public static void main(String[] args) throws Exception {
// 获得连接对象与通道
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 创建消息的元数据
HashMap<String, Object> map = new HashMap<>();
map.put("name","zhangsan");
map.put("age","18");
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.deliveryMode(2) //消息是否支持持久化:1不支持2支付
.messageId(UUID.randomUUID().toString()) //定义消息的业务id
.expiration("100000000") // 定义消息的过期时间
.headers(map) // 头信息
.build();
// 发送消息
channel.basicPublish(EXCHANGE_NAME,"product.#",false,false,properties,"hello,topic".getBytes(StandardCharsets.UTF_8));
// 关闭连接
channel.close();
connection.close();
}
}
新しい AMQP.BasicProperties.Builderを通じてメッセージ メタデータを構築する
消費者側:
public class MyConsumer1 {
// 交换机名称
private static String EXCHANGE_NAME = "my_topic_exchange";
// 队列名称
private static String QUEUE_NAME = "my_topic_queue_1";
public static void main(String[] args) throws Exception {
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 1、声明交换机
channel.exchangeDeclare(EXCHANGE_NAME,"topic");
// 2、声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 3、将队列与交换机进行绑定,并指定routingKey,为 product.任意字符 都能接收
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"product.add");
// 4、创建消费者
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 消费消息
Map<String, Object> map = properties.getHeaders();//获取消息元数据
System.out.println(map);
//⼿动ack
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
// 5、让消费者监听队列
channel.basicConsume(QUEUE_NAME,false,consumer);
}
}
AMQP.BasicPropertiesを通じてメッセージ メタデータを取得する
8. メッセージの繰り返し消費の問題
1.冪等性とは何ですか?
冪等性: 複数の演算の結果は一貫しています。非冪等操作の冪等性を確保するにはどうすればよいですか? ——分散ロックを使用します。
分散ロックを使用して、ネットワーク ジッターによる手動 ACK なしでメッセージが繰り返し消費される問題を解決します。アイデア: メッセージに対してグローバルに一意の ID を生成します。プロデューサーは、メッセージを送信するときにこの ID を伝えます。コンシューマーがメッセージを正常に消費した後、メッセージのIDが使用されますが、Redisのsetnxキャッシュを通じて、繰り返し利用する場合に備えて、そのIDがあるかどうかをredisから判断します。
9. デッドレターキュー - 「遅延」キュー
1. デッドレターキューの概要
デッド レター キューを使用すると、特定の条件が満たされた場合、メッセージをデッド レターにして別のスイッチに送信して消費することができます。このプロセスはデッドレターキューの役割です。デッドレターキューには、キューを「遅延」させる効果がある可能性があります。たとえば、注文がタイムアウトして支払いが行われない場合、注文ステータスを「キャンセル」に変更できますが、この操作はデッドレターキューを使用して完了できます。メッセージのタイムアウトを設定します。メッセージがタイムアウトすると、メッセージはデッドレターになるため、デッドレターキューを監視しているコンシューマは注文をキャンセルします。
知っておくべきことが 2 つあります。
- メッセージはどのようにしてデッドレターになるのでしょうか? デッドレターになる条件
- デッドレターキューを作成し、デッドレターキューの効果を実現する方法
2. メッセージがデッドレターになる条件
- メッセージが拒否され、キューに戻されなかった場合、メッセージはデッドレターになります。(nack、reject、requeue は false)
- メッセージの有効期限が切れたため、デッドレターになります。
- キューの長さは制限されており、メッセージを保存することはできません。保存できないメッセージはデッドレターになります。
3. デッドレターキューを作成する
キーポイント: 通常のキューをデッドレタースイッチにバインドするだけです。注: このデッドレター スイッチは、実際には通常のスイッチです。
消費者):
Connection connection = RabbitUtil.getConnection();
Channel channel = connection.createChannel();
// 声明普通交换机、普通队列 声明死信交换机、死信队列 建立他们的关系
String normalExchangeName = "normal.exchange";
String exchangeType = "topic";
String normalQueueName = "normal.queue";
String routingKey = "dlx.#";
// 声明死信队列
String dlxExchangeName = "dlx.exchange";
String dlxQueueName = "dlx.queue";
// 声明普通交换机
channel.exchangeDeclare(normalExchangeName,exchangeType,true,false,null);
// 为队列绑定死信交换机
Map<String,Object> queueArgs = new HashMap<>();
queueArgs.put("x-dead-letter-exchange",dlxExchangeName);//正常队列绑定⼀个交换机,让该交换机是死信交换机
queueArgs.put("x-max-length",4); //设置队列的⻓度是4
// 声明普通队列,并将带有死信交换机的消息元数据
channel.queueDeclare(normalQueueName,true,false,false,queueArgs);
channel.queueBind(normalQueueName,normalExchangeName,routingKey);
//创建死信队列
channel.exchangeDeclare(dlxExchangeName,exchangeType,true,false,null);
channel.queueDeclare(dlxQueueName,true,false,false,null);
channel.queueBind(dlxQueueName,dlxExchangeName,"#");
4. 遅延キュー
デッド レター キューをリッスンするコンシューマを作成します。メッセージがデッド レター キューに入ると、ビジネス処理 (タイムアウト後の注文のキャンセルなど) のためにメッセージのメタデータが取得され、メッセージが正常に消費された後に手動で ACK が返されます。デッドレターキュー。