記事ディレクトリ
1 はじめに
RabbitMQ は、効率的なメッセージング システムの構築に役立つ強力なメッセージング ミドルウェアです。前回の記事では、 RabbitMQ が提供するクライアントを Java で使用する方法を紹介しましたが、この記事では、公式 Java クライアントを使用して、RabbitMQ 公式 Web サイトが提供する 7 つのメッセージ利用シナリオを示し、各シナリオの用途と利点を探ります。次のコード環境は、管理バックグラウンド構築プロセスで使用される構成を引き続き使用します。
2. 基本モード(Hello World エクスペリエンスモード)
「Hello World」パターンは、メッセージ キューでメッセージを送受信する方法を示す、単純な RabbitMQ エントリーレベルの例です。この例では、 helloという名前のキュー (Queue)にメッセージを送信するプロデューサー (Producer) と、同じキューからメッセージを受信して処理するコンシューマー (Consumer) を作成します。次の図は公式 Web サイトの相互作用図です。P はプロデューサー、C はコンシューマーです。このモードでは、プロデューサー、キュー、コンシューマーのみが存在し、スイッチはありません。
2.1 仮想マシンの作成
この記事で使用する仮想マシンのシナリオは、管理バックグラウンドで事前に作成する必要があります
。作成が完了したら、ユーザーに切り替えて、ユーザーに権限があるかどうかを確認します。
2.2 一般的な接続
以下は接続を取得するためのコードです。この構成はこの記事で使用されているため、ツール クラスにカプセル化されており、後でこのメソッドを通じて接続を取得できます。
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ConnectionUtils {
private static Connection connection;
//获取连接
public static Connection getConnection() throws Exception {
if (null == connection) {
ConnectionFactory factory = new ConnectionFactory();
//服务地址
factory.setHost("192.168.1.11");
//服务所在端口,不填默认5672
factory.setPort(ConnectionFactory.DEFAULT_AMQP_PORT);
//管理员账号
factory.setUsername("tom");
//管理员密码
factory.setPassword("abc123");
//虚拟机
factory.setVirtualHost("scenarios");
connection = factory.newConnection();
}
return connection;
}
//关闭连接
public static void closeConnection() throws Exception {
if (null != connection) {
connection.close();
}
}
}
2.3 プロデューサーがメッセージを送信する
プロデューサは、uuid を含むメッセージをhelloキューに送信します。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", true, false, false, null);
channel.basicPublish("", "hello", null, UUID.randomUUID().toString().getBytes());
channel.close();
ConnectionUtils.closeConnection();
実行が完了したら、管理バックグラウンドに移動してキューのステータスを確認すると、キューにメッセージが受信されたことがわかります。
2.4 消費者消費ニュース
ここで、 Helloキューのメッセージは、コンシューマーからアクティブにプルすることによって取得されます。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
GetResponse getResponse = channel.basicGet("hello", true);
byte[] body = getResponse.getBody();
System.out.println("消息内容:" + new String(body));
channel.close();
ConnectionUtils.closeConnection();
実行後のコンソールの内容
メッセージの内容: 1573f3da-7e49-4fbe-b01c-9ccc9644a16c
非同期で取得する
new Thread(() -> {
try {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.basicConsume("hello",
true,
new DefaultConsumer(channel) {
@Override
public void handleConsumeOk(String consumerTag) {
System.out.println("消费者启动成功");
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("消息内容:" + new String(body));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}).start();
3 ワークキュー (ワークキューモード)
ワーク キュー(別名タスク キュー) の主な考え方は、リソースを大量に消費するタスクの即時実行を避け、それらが完了するまで待つことです。「タスク キュー」モードまたは「ワーク キュー」モードとも呼ばれるこのモードは、分散システムでタスクを非同期処理するための一般的なメッセージ キュー アプリケーション モードです。このモードでは、タスク (またはメッセージ) がプロデューサーによってキューにパブリッシュされ、複数のコンシューマーがこれらのタスクを同時に受信して処理します。複数のコンシューマがキュー内のタスクを同時に処理できるため、タスクの並列処理と負荷分散が実現します。以下は公式 Web サイトで提供されている対話図ですが、コンシューマーは 2 つ以上存在し、同じキュー内のメッセージの消費を共有する、つまり協力してタスクを完了します。
3.1 プロデューサーがメッセージを送信する
10 個のメッセージをワーカーキューに送信します
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String queueName = "worker";
channel.queueDeclare(queueName, true, false, false, null);
for (int i = 0; i < 10; i++){
channel.basicPublish("", queueName, null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();
3.2 消費者消費ニュース
このモードでは、2 つのプロセスが有効になり、メッセージ消費のための 2 つのコンシューマをシミュレートします。
public static void main(String[] args) throws Exception {
String queueName = "worker";
new Thread(() -> {
asyncGet(queueName);
}, "worker1").start();
new Thread(() -> {
asyncGet(queueName);
}, "worker2").start();
}
private static void asyncGet(String queueName) {
try {
String workerName = Thread.currentThread().getName();
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
channel.basicConsume(queueName,
true,
new DefaultConsumer(channel) {
@Override
public void handleConsumeOk(String consumerTag) {
System.out.printf("消费者%s - 启动成功%n", workerName);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.printf("消息者%s - 获取消息:%s%n", workerName, new String(body));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
4. パブリッシュ/サブスクライブ (パブリッシュ/サブスクライブ モード)
「パブリッシュ/サブスクライブ」は、メッセージのブロードキャストとマルチキャストを実現するために使用され、複数のコンシューマーが同じメッセージを同時に受信して処理できるようにします。「パブリッシュ/サブスクライブ」モードでは、メッセージはキューに直接ではなく、プロデューサーによってエクスチェンジ( Exchange) にパブリッシュされます。交換機は、受信したメッセージをそれにバインドされているすべてのキューにブロードキャストする責任があります。各キューには独自のコンシューマがあり、交換局がメッセージをブロードキャストすると、そのキューにバインドされているすべてのコンシューマが同時にメッセージを受信します。
4.1 プロデューサーがメッセージを送信する
プロデューサは、以前のようにキューにメッセージを送信する代わりに、スイッチにメッセージを送信することのみが可能ですが、publishSubscribe スイッチはプロデューサからメッセージを受信することのみを担当し、プロデューサはどのキューに送信するかについては考慮しません。スイッチはファンアウトタイプである必要があります。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "publishSubscribe";
channel.exchangeDeclare(exchangeName,"fanout");
for (int i = 0; i < 10; i++){
channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();
4.2 消費者消費ニュース
「パブリッシュ/サブスクライブ」モードでは、各キューが独自のコンシューマを持つことができ、コンシューマは互いに独立しています。このようにして、メッセージのブロードキャストとマルチキャストを実現できるため、複数のコンシューマが同じメッセージを同時に受信して処理できます。交換のステートメントはプロデューサーと一致している必要があり、最後に、メッセージを正しく消費するために、取得したキューを交換にバインドする必要があります。
public static void main(String[] args) throws Exception {
String exchangeName = "publishSubscribe";
new Thread(() -> {
asyncGet(exchangeName);
}, "Subscribe1").start();
new Thread(() -> {
asyncGet(exchangeName);
}, "Subscribe2").start();
}
private static void asyncGet(String exchangeName) {
try {
String workerName = Thread.currentThread().getName();
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(exchangeName,"fanout");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, "");
channel.basicConsume(queueName,
true,
new DefaultConsumer(channel) {
@Override
public void handleConsumeOk(String consumerTag) {
System.out.printf("消费者%s - 启动成功%n", workerName);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.printf("消息者%s - 获取消息:%s%n", workerName, new String(body));
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
5. ルーティング
「ルーティング」モードは、メッセージ キュー アプリケーション モードであり、ルーティング キー (Routing Key) の一致関係に従って、交換局 (Exchange) から指定されたキューにメッセージをルーティングするために使用され、より洗練されたモードと言えます。パブリッシュ/サブスクライブ カスタマイズのバージョン。このモードでは、サブスクリプション後にメッセージを使用することはできませんが、ルーティング キーと一致する必要もあります。(同時にスイッチタイプがファンアウトからダイレクトに変更されました)
5.1 プロデューサーがメッセージを送信する
プロデューサによって発行されたメッセージには、特定のルーティング キーがあります。Exchange は、このルーティング キーに基づいて、メッセージをバインドされた特定のキューにルーティングします。このようにして、コンシューマは特定のキューに注目するだけで済み、そのルーティング キーに一致するメッセージのみを受信して処理することができます。スイッチのタイプをdirectに設定する必要があります。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "routing";
channel.exchangeDeclare(exchangeName,"direct");
for (int i = 0; i < 10; i++){
//发送到demo的路由
channel.basicPublish(exchangeName, "demo", null, UUID.randomUUID().toString().getBytes());
//发送到test的路由
channel.basicPublish(exchangeName, "test", null, UUID.randomUUID().toString().getBytes());
//发送到super的路由
channel.basicPublish(exchangeName, "super", null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();
5.1 消費者消費ニュース
交換は、メッセージのルーティング キーとキューにバインドされたルーティング キーに基づいて照合します。メッセージのルーティング キーがキューにバインドされたルーティング キーと一致する場合、メッセージはそのキューにルーティングされ、そのキュー上のコンシューマーによって処理されます。次の例では、demoとtestのサポートを定義しています。これら 2 つのルーティング ルールが含まれている限り、メッセージを消費できます。
public static void main(String[] args) throws Exception {
String exchangeName = "routing";
new Thread(() -> {
asyncGet(exchangeName);
}, "Subscribe").start();
}
private static void asyncGet(String exchangeName) {
try {
String workerName = Thread.currentThread().getName();
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(exchangeName,"direct");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, "demo");
channel.queueBind(queueName, exchangeName, "test");
channel.basicConsume(queueName,
true,
new DefaultConsumer(channel) {
@Override
public void handleConsumeOk(String consumerTag) {
System.out.printf("消费者%s - 启动成功%n", workerName);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.printf("消息者%s - 获取消息:%s,路由:%s%n", workerName, new String(body),envelope.getRoutingKey());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
コンソールの印刷を通じて、コンシューマーがスーパールートのメッセージを取得できないことがわかります。
Consumer サブスクライブ - 正常に開始されました
サブスクライバー - メッセージの取得: a8a6d061-20a9-4af0-ba5e-e9f484180b8d、ルート: デモ メッセー
ジャー サブスクライブ - メッセージの取得: 9eb5e5ce-6530-4de3-bd31-06390d13d2d2、ルート: テスト メッセー
ジャー サブスクライブ - メッセージの取得: 954bf76f- 7b3e-429c-8761-1d064530b1c2、ルート: デモ
メッセージャー 購読 - メッセージを取得: 811e8891-4945-4972-8a5c-340362e4b76b、ルート: テスト メッセージャー
購読 - メッセージを取得: 240083af-ce7b-458 7-8bec -cfd75c4ca69d 、ルート: デモ
messager 購読 - メッセージを取得: a38c46fd-e5c6-4a60-bd7b-000cb258c10e、ルート: test
6.トピックス(テーマモード)
「トピック」モードは、柔軟かつ強力なメッセージ キュー アプリケーション モードで、プロデューサーがワイルドカード ルーティング キー (ルーティング キー) を使用してメッセージを交換機に公開したり、メッセージを別のキューにルーティングしたりすることができます。これは、キーのルール照合をサポートするルーティング モードのパーソナライズされたカスタマイズとみなすこともできます。
6.1 プロデューサーがメッセージを送信する
「トピック」モードでは、プロデューサーによって発行されたメッセージにはルーティング キーが含まれます。ルーティング キーは、ドットで区切られた文字列です (例: "user.create"、"order.update" など)。交換は、メッセージのルーティング キーをキューにバインドされたルーティング キーと照合し、ワイルドカードを使用してメッセージを 1 つ以上の一致するキューにルーティングします。
「トピック」パターンでは 2 つのワイルドカードを使用します。
- * (アスタリスク): 単語を表すワイルドカード文字。たとえば、「user.*」のルーティング キーは、「user.create」、「user.update」などと一致します。
- # (シャープ記号): 0 個以上の単語を表すワイルドカード文字。たとえば、ルーティング キー「order.#」は、「order」、「order.update」、「order.details」などと一致する可能性があります。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "topics";
channel.exchangeDeclare(exchangeName,"topic");
for (int i = 0; i < 3; i++){
//发送到路由带有order.create的队列
channel.basicPublish(exchangeName, "order.create", null, UUID.randomUUID().toString().getBytes());
//发送到路由带有user.create的队列
channel.basicPublish(exchangeName, "user.create", null, UUID.randomUUID().toString().getBytes());
//发送到路由带有user.vip.create的队列
channel.basicPublish(exchangeName, "user.vip.create", null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();
6.2 消費者消費ニュース
「トピック」モードでは、交換機はメッセージのルーティング キーとキューにバインドされたルーティング キーに基づいて柔軟なワイルドカード マッチングを実行します。コンシューマは、自身が宣言したルーティング ルールを通じて交換機にバインドし、ルーティング ルールに一致するメッセージを取得します。
public static void main(String[] args) throws Exception {
String exchangeName = "topics";
String routingKey1 = "*.create";
new Thread(() -> {
asyncGet(exchangeName, routingKey1);
}, "Subscribe1").start();
String routingKey2 = "#.create";
new Thread(() -> {
asyncGet(exchangeName, routingKey2);
}, "Subscribe2").start();
}
private static void asyncGet(String exchangeName, String routingKey) {
try {
String workerName = Thread.currentThread().getName();
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
channel.exchangeDeclare(exchangeName, "topics");
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, routingKey);
channel.basicConsume(queueName,
true,
new DefaultConsumer(channel) {
@Override
public void handleConsumeOk(String consumerTag) {
System.out.printf("消费者%s - 启动成功%n", workerName);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.printf("消息者%s - 获取消息:%s,路由:%s%n", workerName, new String(body), envelope.getRoutingKey());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
コンソールから、 Subscribe1 はルーティング キーがorder.createおよびuser.createであるメッセージのみを消費できるのに対し、 Subscribe2 はルーティング キーが order.create 、 user.create 、および user.vip.create 情報であるメッセージを消費できることがわかります。
Consumer Subscrip1 - 成功した
コンシューマを開始します Subscribe2 - 成功した
メッセージを開始します 7C789C6E-456F
-4682-B7EC-01bA4DB489B4 、ルート: user.create
messager Subscribe1 - メッセージを取得します: 9aaf5e63-d994-4fd4-b285-4e2be6f27018、ルート: order.create
messager Subscribe2 - メッセージを取得: d3a35a50-7d3e-4506-8e69-b732f43dfe1c、ルート: user.vip .create
messager Subscribe1 - メッセージを取得: 7c789c6e-456f-4682-b7ec-01ba4db489b4、ルート: user.create
messager Subscribe1 - メッセージを取得: 27848958-6824-49e2-953e-0d37443b69b6、ルート: order.create
messager Subscribe1 - メッセージを取得: ed0c9123-7d44-458c-859f-e825de42f01a、ルート: user.create
messager Subscribe2 - メッセージを取得: 27848958-6824-49e 2-953e- 0d37443b69b6、ルート: order.create
messager Subscribe2 - メッセージを取得: ed0c9123-7 d44 -458c-859f-e825de42f01a、ルート: user.create
Subscribe2 - メッセージの取得: 95f28ecb-f79a-4758-92df-043b4d443621、ルート: user.vip.create
Subscribe2 - メッセージの取得: 9bf03771-72fa-4580-a421-f311dd8fb1ce、ルート: order.create
Subscribe1 - メッセージの取得: 9bf03771-72fa -4580-a421-f311dd8fb1ce、ルート: order.create
messager Subscribe1 - メッセージを取得: 393dc53f-d93c-4c8d-93f9-5b42d030376f、ルート: user.create
messager Subscribe2 - メッセージを取得: 393dc53f-d 93c -4c8d-93f9-5b42d0303 76f、ルート: user.create
messager Subscribe2 - メッセージを取得: 17bb98f8-12cd-4ac1-84a9-f33006abc125、ルート: user.vip.create
7. rpc (リモートプロシージャコールモード)
前のワーク キュー モードでは、ワーク キューを使用して、時間のかかるタスクを複数のワーカー プロセスに分散する方法を学びました。しかし、リモート コンピューター上で関数を実行し、結果を待つ必要がある場合はどうすればよいでしょうか? それは別のニーズです。このパターンは、リモート プロシージャ コール (RPC) と呼ばれることがよくあります。RPC モードでは、クライアントとスケーラブルな RPC サーバー。
上記の対話図からわかるように、RabbitMQサーバーは、メッセージ エージェントとして、クライアントのリクエストをサーバーに転送し、サーバーの応答を受信してクライアントに返す責任を負います。クライアント(クライアント) は、リクエストを次の宛先に送信します。 RabbitMQ サーバーにアクセスし、応答を待ちます。サーバー(サーバー) は、RabbitMQ サーバー上のキューをリッスンし、クライアントからの要求を処理し、対応するリモート メソッドを実行し、結果を応答としてクライアントに返します。
7.1 サーバーが起動してリッスンする
rpc モードでは、プロデューサーとコンシューマーの概念はありません。代わりにサーバーとクライアントがあります。以下のサンプル コードでは、サービスは rpc_queue キューをリッスンします。メッセージ (整数) を受信すると、計算が呼び出されます。フィボナッチ数列メソッドはメッセージ内のデータを計算し、クライアントに応答します。
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
String queueName = "rpc_queue";
channel.queueDeclare(queueName, false, false, false, null);
//清空队列中的所有消息
channel.queuePurge(queueName);
channel.basicQos(1);
//接收消息的回调
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
String response = "";
try {
String message = new String(delivery.getBody(), StandardCharsets.UTF_8);
Integer i = Integer.parseInt(message);
System.out.printf("服务端 - 获取消息:%s%n", message);
response += fib(i);
} catch (RuntimeException e) {
System.out.println(" [.] " + e);
} finally {
//响应客户端
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes(StandardCharsets.UTF_8));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
}
};
//监听队列
channel.basicConsume(queueName, false, deliverCallback, (consumerTag -> {
}));
}
//计算斐波那契数列
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
7.2 クライアントのリクエストと待機
クライアントは整数をキューに送信し、サービスの計算結果を待ちます。
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
String replyQueueName = channel.queueDeclare().getQueue();
String corrId = UUID.randomUUID().toString();
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
String queueName = "rpc_queue";
String message = new Random().nextInt(30) + "";
//发送消息
channel.basicPublish("", queueName, props, message.getBytes(StandardCharsets.UTF_8));
CompletableFuture<String> response = new CompletableFuture<>();
//等待结果
String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response.complete(new String(delivery.getBody(), StandardCharsets.UTF_8));
}
}, consumerTag -> {
});
channel.basicCancel(ctag);
String result = response.get();
System.out.printf("客户端收到响应:%s", result);
ctagは、消費者を一意に識別するために使用される消費者タグ (Consumer Tag) です。
ReplyQueueNameは、クライアントがサーバー側のrpc_queueキューにメッセージを送信するときに保持される応答キューで、後続のサーバーはこのキューを通じて結果をクライアントに応答します。
8. パブリッシャーの確認 (送信確認メカニズム)
パブリッシャー確認モードは、メッセージ送信確認メカニズム (RabbitMQ バージョン 3.0 以降でサポート) であり、メッセージの信頼性の高い配信を保証するために使用されます。従来のメッセージ送信モードでは、プロデューサーは RabbitMQ にメッセージを送信し、メッセージが正常に送信されたと考えますが、実際には、ネットワークなどの要因により、送信中にメッセージが失われ、メッセージが送信される可能性があります。正しく送信されていないため、キューに投稿されました。
この問題を解決するために、RabbitMQ は発行者確認メカニズムを導入しました。パブリッシャー確認モードでは、プロデューサが RabbitMQ にメッセージを送信すると、RabbitMQ からの確認フィードバックを待ちます。メッセージが RabbitMQ によって正常に受信され、キューに配信されると、RabbitMQ は確認メッセージをプロデューサーに送信します。メッセージが正しく処理されなかった場合、RabbitMQ は否定的なメッセージをプロデューサーに送信します。RabbitMQ から確認フィードバックを受信した後、プロデューサーはメッセージが正常に配信されたかどうかを判断できます。
パブリッシャー確認モードでは、プロデューサは次の 2 つの方法でメッセージの配信ステータスを確認できます。
8.1 同期確認
プロデューサはメッセージを送信した後、確認メッセージを受信するかタイムアウトになるまで、RabbitMQ からの確認メッセージを待ちます。この方法では、メッセージの信頼性の高い配信が保証されますが、確認メッセージを待機する過程でプロデューサがブロックされ、メッセージ送信のパフォーマンスに影響を与える可能性があります。
8.1.1 シングル同期確認
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "syncProducer";
channel.exchangeDeclare(exchangeName,"fanout");
//启用确认机制
channel.confirmSelect();
for (int i = 0; i < 3; i++){
channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
channel.waitForConfirmsOrDie(5_000);
}
channel.close();
ConnectionUtils.closeConnection();
8.1.2 一括同期確認
同期確認は結果を待つためにブロックする必要があるため、一度の確認では送信効率に影響しますが、データの整合性をあまり気にしない場合は、一括確認を使用することで送信効率を向上させることができます。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "bactcSyncProducer";
channel.exchangeDeclare(exchangeName,"fanout");
//启用确认机制
channel.confirmSelect();
//批量确认数量
int batchSize = 3;
//积累的发送数量
int outstandingMessageCount = 0;
for (int i = 0; i < 10; i++){
channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
outstandingMessageCount++;
//当积累的发送数量达到批量确认数量时,进行批量确认
if (outstandingMessageCount == batchSize) {
//等待确认消息的超时时间 5000毫秒
channel.waitForConfirmsOrDie(5_000);
outstandingMessageCount = 0;
}
}
if (outstandingMessageCount > 0) {
channel.waitForConfirmsOrDie(5_000);
}
channel.close();
ConnectionUtils.closeConnection();
バッチ同期確認モードでは、後続のコードの実行を続行する前に、すべてのメッセージが正しく確認される必要があります。メッセージが正しく確認されなかった場合、バッチ送信プロセス全体が失敗したとみなされ、プロデューサーは特定の状況に応じて適切に処理する必要があります。 、失敗したメッセージの送信を再試行するか、後続の処理のために失敗したメッセージを記録することが考えられます。
8.2 非同期確認
プロデューサはメッセージを送信した後、確認メッセージの待機をブロックせず、次のメッセージの送信を続行します。同時に、プロデューサーは、RabbitMQ によって送信された確認メッセージを処理するコールバック関数を登録します。これによりメッセージ送信のパフォーマンスが向上しますが、メッセージの信頼性を確保するために、確認メッセージを処理するコールバック関数に注意する必要があります。
public static void main(String[] args) throws Exception {
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "asyncProducer";
channel.exchangeDeclare(exchangeName, "fanout");
//启用确认机制
channel.confirmSelect();
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
//消息被RabbitMQ 确认接收后调用的回调方法
ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
if (multiple) {
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
sequenceNumber, true
);
confirmed.clear();
} else {
outstandingConfirms.remove(sequenceNumber);
}
};
ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
//获取未被确认的消息
String body = outstandingConfirms.get(sequenceNumber);
//没有被确认执行的方法,先移除
ackCallback.handle(sequenceNumber, multiple);
//TODO 再做其他逻辑处理
};
//添加确认监听
channel.addConfirmListener(ackCallback, nackCallback);
int msgCount = 30;
long start = System.nanoTime();
for (int i = 0; i < msgCount; i++) {
String body = String.valueOf(i);
outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
}
//等待所有消息被确认,超时时间为60秒
if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
//TODO 存在未被确认的消息,做其他处理
}
channel.close();
ConnectionUtils.closeConnection();
}
static boolean waitUntil(Duration timeout, BooleanSupplier condition) throws InterruptedException {
int waited = 0;
while (!condition.getAsBoolean() && waited < timeout.toMillis()) {
Thread.sleep(100L);
waited += 100;
}
return condition.getAsBoolean();
}
この例では、確認モニターがチャネルに追加されます。チャネル上でメッセージが送信されるたびに、行レコードがOutstandingconfirmsに追加されます。キーはメッセージのシリアル番号であり、シリアル番号は増加する整数です。メッセージが送信されるたびにシリアル番号が増加します。RabbitMQ Java クライアントによって提供されます。最後に、すべてのメッセージが送信された後、60 秒待って未確認のメッセージがあるかどうかを確認します。
9. Consume confirms (消費確認メカニズム)
消費確認メカニズムについては公式には言及されていませんが、おそらくそれは比較的単純であり、公式は彼の特徴的な使用シナリオの紹介にそれを含めていません。
クライアントのプログラミング モードでは、メッセージを確認する方法が 2 つあります。1
つは、消費を呼び出すときに autoAck を true に設定し、デフォルトの応答メカニズムを有効にすることです。
channel.basicConsume("q-1",true,new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
}
});
2 つ目は、BasicAck を使用して手動で応答することです。
basicAck(long deliveryTag, boolean multiple)
パラメータの説明:
-
deliveryTag : メッセージの一意の識別子。各メッセージには、キュー内のメッセージの位置を識別する一意のdeliveryTagがあります。
-
multiple : 複数のメッセージを確認するかどうか。false に設定すると、指定したdeliveryTagを持つメッセージのみが確認応答されることを意味し、trueに設定した場合は、指定した値以下のdeliveryTagを持つすべてのメッセージが確認応答することを意味します。
なお、メッセージ確認機構はコンシューマ側で設定する必要があるため、channel.basicConsume(queue, autoAck, Consumer)false
メソッドを呼び出す際に、メッセージ確認関数にautoAckパラメータを設定してください。
10. ヘッダーキュー (ヘッドルーティングモード)
チュートリアル モードにはそのようなモードはありませんが、公式 github アドレスの例にはあります。Java クライアントの列挙クラスBuiltinExchangeTypeのヘッダースイッチ タイプも確認できます。
public enum BuiltinExchangeType {
DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
...
}
ヘッダー モードでは、交換機 (Exchange) はメッセージのルーティング キーを使用してメッセージを送信するキューを決定しませんが、メッセージの Header (ヘッダー) 属性に従ってキューを照合します。Header プロパティはキーと値のペアを含むディクショナリであり、メッセージ送信者はメッセージのヘッダーに複数のキーと値のペアを設定できます。
10.1 プロデューサーがメッセージを送信する
メッセージがヘッダー タイプの交換機に送信されると、交換機はメッセージの Header プロパティがバインド (バインディング) キューの Header プロパティと一致するかどうかを確認します。メッセージの Header プロパティがバインドされたキューの Header プロパティと正確に一致する場合にのみ、メッセージは対応するキューにルーティングされます。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "headers";
// 声明交换器类型为headers
channel.exchangeDeclare(exchangeName,BuiltinExchangeType.HEADERS);
// 设置消息头
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("project", "rabbitmq");
headers.put("env", "test");
// 设置消息属性
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
// 设置消息持久化,如果声明队列时durable为true时,消息将自动继承队列的持久性属性,即使不单独设置消息的 Delivery Mode 属性,消息仍然会被持久化到磁盘
builder.deliveryMode(MessageProperties.PERSISTENT_TEXT_PLAIN.getDeliveryMode());
//消息的优先级用于标记消息的重要性,范围从 0 到 9,0 为最低优先级,9 为最高优先级。
builder.priority(MessageProperties.PERSISTENT_TEXT_PLAIN.getPriority());
// Add the headers to the builder.
builder.headers(headers);
AMQP.BasicProperties theProps = builder.build();
for (int i = 0; i < 3; i++){
channel.basicPublish(exchangeName, "head", theProps, UUID.randomUUID().toString().getBytes());
channel.basicPublish(exchangeName, "head", theProps, UUID.randomUUID().toString().getBytes());
channel.basicPublish(exchangeName, "head", theProps, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();
スイッチがヘッダー タイプとして宣言された後は、管理バックグラウンドでも表示できます。
10.2 消費者消費ニュース
ヘッダー モードでは、コンシューマーはキューのバインド (バインディング) を設定することによってメッセージを消費します。他のモードとは異なり、ヘッダー モードでは、交換局 (Exchange) はメッセージ ルーティングにメッセージのルーティング キーを使用せず、メッセージの Header プロパティに従ってキューを照合します。
public static void main(String[] args) throws Exception {
String exchangeName = "headers";
// 设置Subscribe1的消息头
Map<String, Object> headers1 = new HashMap<>();
headers1.put("project", "java");
headers1.put("env", "test");
new Thread(() -> {
asyncGet(exchangeName,headers1);
}, "Subscribe1").start();
// 设置Subscribe2的消息头
Map<String, Object> headers2 = new HashMap<>();
headers2.put("project", "rabbitmq");
headers2.put("env", "test");
new Thread(() -> {
asyncGet(exchangeName,headers2);
}, "Subscribe2").start();
}
private static void asyncGet(String exchangeName,Map<String, Object> headers) {
try {
String workerName = Thread.currentThread().getName();
Connection connection = ConnectionUtils.newConnection();
Channel channel = connection.createChannel();
// 声明交换器类型为headers
channel.exchangeDeclare(exchangeName,BuiltinExchangeType.HEADERS);
String queueName = channel.queueDeclare().getQueue();
channel.queueBind(queueName, exchangeName, "head",headers);
channel.basicConsume(queueName,
true,
new DefaultConsumer(channel) {
@Override
public void handleConsumeOk(String consumerTag) {
System.out.printf("消费者%s - 启动成功%n", workerName);
}
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.printf("消息者%s - 获取消息:%s,路由:%s,headers:%s%n", workerName, new String(body),envelope.getRoutingKey(),properties.getHeaders().toString());
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
実行後、コンソール結果の出力から、 Subscribe2のみがメッセージを消費できることがわかります。これは、デフォルトのルールではメッセージを消費するには完全一致が必要であり、 headers.put("x-match", "any");
1 つのルールが一致する限りメッセージを消費できるように設定できるためです。。
注: 通常、Direct、Topic、またはFanoutモードを使用すると、ほとんどのメッセージ ルーティングのニーズを満たすことができるため、ヘッダー モードは一般的には使用されません。ヘッダー モードは、豊富な情報をメッセージのヘッダー属性に含める必要があり、一致ルールが比較的複雑な特殊なルーティング要件に適しています。ほとんどの場合、メッセージのルーティングと配布を実装するには、ダイレクト、トピック、またはファンアウト モードを使用することをお勧めします。
11. まとめ
これまで、このブログ投稿では、さまざまなシナリオでの RabbitMQ メッセージ キューのアプリケーションを包括的に紹介してきました。基本モード(Hello World)、ワークキュー、パブリッシュ/サブスクライブ、ルーティング、トピック、rpc、パブリッシャー確認、消費確認からヘッダーキューまで、各パートでその使用法と例を詳しく説明します。これらのさまざまなモードと機能を通じて、RabbitMQ の機能と柔軟性をより深く理解し、信頼性の高いメッセージング システムを構築するためのガイダンスを提供したいと考えています。