記事ディレクトリ
I.はじめに
モジュール間の結合が高すぎるため、モジュールがダウンするとすべての機能が使用できなくなり、さらに同期通信のコストが高すぎてユーザー エクスペリエンスが低下します。
RabbitMQ の概要 |
---|
2. RabbitMQ の概要
市場で人気のあるいくつかの MQ:
ActiveMQ、RocketMQ、Kafka、RabbitMQ。
言語サポート: ActiveMQ と RocketMQ は Java 言語のみをサポートしますが、Kafka は複数の言語をサポートでき、RabbitMQ は複数の言語をサポートします。
効率の観点では、ActiveMQ、RocketMQ、Kafka はすべてミリ秒レベルですが、RabbitMQ はマイクロ秒レベルです。
メッセージ損失とメッセージの重複の問題: RabbitMQ には、メッセージの永続性と重複の問題に対して比較的成熟したソリューションがあります。
学習コスト: RabbitMQ は非常にシンプルです。
RabbitMQ は Rabbit Company によって開発および保守され、最終的に Pivotal で Erlang 言語 (並行プログラミング言語) によって開発されます。
RabbitMQ は AMQP プロトコル (アドバンスト メッセージ キュー プロトコル) に厳密に従い、プロセス間で非同期メッセージを配信できるようにします。
RabbitMQ は、Erlang で実装された、同時実行性と信頼性の高い AMQP メッセージ キュー サーバーです。メッセージ永続性、トランザクション、輻輳制御、負荷分散、その他の機能をサポートすることで、RabbitMQ はより幅広いアプリケーション シナリオを実現します。RabbitMQ は Erlang および AMQP に関連しています。ここでは Erlang と AMQP について簡単に紹介します。
Erlang は動的に型付けされた関数型プログラミング言語であり、Erlang 仮想マシンによって解釈および実行されるインタープリター言語でもあります。言語モデルの観点から見ると、Erlang は Actor モデルの実装に基づいています。アクター モデルでは、すべてがアクターであり、各アクターは内部状態をカプセル化しており、アクターはメッセージ パッシングを通じてのみ相互に通信できます。Erlang に対応して、各アクターは Erlang プロセスに対応し、プロセスはメッセージ パッシングを通じて通信します。共有メモリと比較して、メッセージ パッシングを介したプロセス間通信の直接的な利点は、(Erlang 仮想マシンの基盤となる実装におけるロック アプリケーションに関係なく) 直接ロックのオーバーヘッドが排除されることです。
AMQP (Advanced Message Queue Protocol) は、メッセージング システム仕様を定義します。この仕様では、分散システム内のサブシステムがメッセージを通じてどのように対話するかを説明します。RabbitMQ は、アーランベースの AMQP 実装です。AMQP は分散システム内のさまざまなサブシステムを分離し、サブシステム間の依存関係がなくなりました。サブシステムはメッセージのみに依存します。サブシステムは、メッセージの送信者も受信者も気にしません。
アドバンテージ
1. デカップリング: システムモジュールの結合を削減します。
2. システムの応答時間を改善する
3. 非同期メッセージ
4. 過負荷保護、トラフィックピーククリッピング
1. アプリケーションのデカップリング
シナリオ: ダブル 11 ショッピング中、ユーザーが注文した後、注文システムは在庫システムに通知する必要があります。従来のアプローチでは、注文システムが在庫システムのインターフェイスを呼び出します。
このアプローチには次のような欠点があります。
- 在庫システムに障害が発生すると、注文は失敗します。
- 受注システムと在庫システムは高度に連携しています。
メッセージキューの導入
注文システム: ユーザーが注文すると、注文システムは永続化処理を完了し、メッセージをメッセージ キューに書き込み、ユーザーの注文成功を返します。
在庫システム: 注文メッセージを購読し、注文メッセージを取得し、ライブラリ操作を実行します。インベントリ システムに障害が発生した場合でも、メッセージ キューにより、メッセージ損失を引き起こすことなくメッセージを確実に配信できます。
2. 非同期処理
シナリオの説明: ユーザーは登録後、登録電子メールと登録 SMS を送信する必要があります。従来の 2 つの方法があります: 1. シリアル方式 2. パラレル方式シリアルモード: データベースに登録情報を書き込んだ後、登録メールを送信し、登録テキスト メッセージを送信し、上記 3 つのタスクが完了した後でのみ、登録情報がクライアントに返されます。これに関する 1 つの問題は、電子メールや SMS は必要なく、単なる通知であり、このアプローチではクライアントが必要のないものを待つ可能性があることです。
並列方式:登録情報をデータベースに書き込んだ後、メールとテキストメッセージを同時に送信し、上記3つの作業が完了した後、クライアントに返送することで処理時間を短縮できます。
3 つのビジネス ノードがそれぞれ 50 ミリ秒を使用し、シリアル使用時間が 150 ミリ秒、並列使用時間が 100 ミリ秒であると仮定します。処理時間は改善されましたが、前述したように、電子メールやテキスト メッセージは Web サイトの通常の使用に影響を与えず、登録が成功したことが表示されるまでクライアントが送信の完了を待つ必要はありません。 Yingai がデータベースに書き込まれます。
メッセージキュー
メッセージキューの導入後、メールやテキストメッセージの送信に必要のないビジネスロジックは非同期で処理され、メッセージキューの導入後、ユーザーの応答時間はデータベースへの書き込み時間 + 時間になります。メッセージ キューへの書き込みの時間 (これは無視できます) メッセージ キューの導入 後処理後の応答時間はシリアルの 3 倍、パラレルの 2 倍になります
3. トラフィックのピーククリッピング
トラフィック ピーク シェービングは一般に、フラッシュ セールス活動で広く使用されています。
シナリオ: フラッシュ セール アクティビティでは、通常、過剰なトラフィックによりアプリケーションがハングします。この問題を解決するために、通常はアプリケーションのフロントエンドにメッセージ キューが追加されます。
機能:
1. アクティブな人数を制御でき、この一定のしきい値を超える注文は直接破棄されます。2. アプリケーションを圧倒する短期的な高トラフィックを軽減できます (アプリケーションは最大処理能力に応じて注文を取得します)。
このように、キューの仕組みを利用することで、スーパーマーケットで決済するときと同じように、レジに殺到するのではなく、次々と決済の列に並び、同時決済なので飛び込むことができずに処理することができます。これだけの数しか達成できません。
3. RabbitMQのインストール
ドッカーのインストール
version: "3.1"
services:
rabbitmq:
image: daocloud.io/library/rabbitmq:management
restart: always
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672
volumes:
- ./data:/var/lib/rabbitmq
[root@192 ~]# cd /opt
[root@192 opt]# mkdir docker_rabbitmq
[root@192 opt]# cd docker_rabbitmq/
[root@192 docker_rabbitmq]# vim docker-compose.yml
[root@192 docker_rabbitmq]# docker-compose up -d
Creating network "docker_rabbitmq_default" with the default driver
Pulling rabbitmq (daocloud.io/library/rabbitmq:management)...
management: Pulling from library/rabbitmq
01bf7da0a88c: Pull complete
f3b4a5f15c7a: Pull complete
57ffbe87baa1: Pull complete
5ef3ef76b1b5: Pull complete
82a3ce07c0eb: Pull complete
1da219d9bd70: Pull complete
446554ac749d: Pull complete
8e4c09e200e7: Pull complete
7a8620611ebf: Pull complete
c70a2924b273: Pull complete
3b0b9e36b4e9: Pull complete
7619a9a42512: Pull complete
965a8e1f1b1c: Pull complete
Digest: sha256:4cc2267788b21e0f34523b4f2d9b32ee1c2867bf2de75d572158d6115349658c
Status: Downloaded newer image for daocloud.io/library/rabbitmq:management
Creating rabbitmq ... done
ブラウザ アクセス: http://ip:15672 (注: ip は現在のクラウド サーバーのアドレスを指します。クラウド サーバーでポート 15672 と 5672 を開くことを忘れないでください)
デフォルトのユーザー名とパスワードは次のとおりです: guest
4. RabbitMQ アーキテクチャ
4.1 公式の簡単なアーキテクチャ図
パブリッシャー - プロデューサー: RabbitMQ で Exchange にメッセージをパブリッシュします。
Consumer - Consumer: RabbitMQ のキュー内のメッセージをリッスンします。
Exchange - スイッチ: プロデューサとの接続を確立し、プロデューサからメッセージを受信します。
キュー - キュー: Exchange は指定されたキューにメッセージを配布し、キューとコンシューマーは対話します。
ルート - ルーティング: スイッチはメッセージをキューにパブリッシュするためにどのような戦略を使用しますか?
単純なアーキテクチャ図 |
---|
4.2 RabbitMQ の完全なアーキテクチャ図
完全なアーキテクチャ図
完全なアーキテクチャ図 |
---|
4.3 RabbitMQの通信方式
https://www.rabbitmq.com/getstarted.html
4.4 Hello-World ケースのデモ
- 依存関係をインポートする
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.15.0</version>
</dependency>
- プロデューサーの作成 パブリッシャー
package com.guo.rabbitmq;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
//生产者
public class Publisher {
public static void main(String[] args) throws Exception{
System.out.println("Publisher...");
//配置连接参数
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.25.132");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//获取连接
Connection connection = factory.newConnection();
//获取Channel
Channel channel = connection.createChannel();
//配置队列参数
//参数1:queue - 指定队列的名称
//参数2:durable - 当前队列是否需要持久化,值为true时表示持久化,rabbitmq宕机或重启后,队列依然在
//参数3:exclusive - 当前队列是否为排他队列,值为true时表示与当前连接(connection)绑定,连接关闭,队列消失,排他队列会对当前队列加锁,其他通道channel是不能访问的,否则会报异常
//参数4:autoDelete - 当前队列是否自动删除,值为true时表示队列中的消息一旦被消费,该队列会消失
//参数5:arguments - 指定当前队列的相关参数
channel.queueDeclare("helloworldQueue",false,false,false,null);
//发布消息到exchange,同时指定路由的规则
// 参数1:指定exchange,目前测试没有创建交换机,使用""
// 参数2:指定路由的规则,或者使用具体的队列名称
// 参数3:指定传递的消息所携带的properties,目前测试不需要,使用null
// 参数4:指定发布的具体消息,byte[]类型,目前测试需要,传递数据进行类型转换
channel.basicPublish("","helloworldQueue",null,"helloworld".getBytes());
//关闭资源
channel.close();
connection.close();
}
}
- コンシューマコンシューマの作成
package com.guo.rabbitmq;
import com.rabbitmq.client.*;
import java.io.IOException;
//消费者
public class Consumer {
public static void main(String[] args)throws Exception {
System.out.println("Consumer...");
//配置连接参数
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.25.132");
factory.setPort(5672);
factory.setUsername("guest");
factory.setPassword("guest");
//获取连接
Connection connection = factory.newConnection();
//获取Channel
Channel channel = connection.createChannel();
//监听队列
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("来自生产者的消息:"+new String(body));
}
};
//消费消息
//参数1:queue - 指定消费哪个队列
//参数2:autoAck - 指定是否自动ACK (true表示接收到消息后,会立即告知RabbitMQ,false表示不告知)
//参数3:consumer - 指定消费回调
channel.basicConsume("helloworldQueue",true,defaultConsumer);
//设置延迟关闭,否则可能无法显示相关信息,消费方一般不关,目的是为了可以及时处理消息
Thread.sleep(5000);
//关闭资源
channel.close();
connection.close();
}
}
テストのためにプロデューサーとコンシューマーを別々に起動します (一度消費する前に一度生産します)
4.5 基本原則
RabbitMQ はメッセージ キューの実装ですが、メッセージ キューには正確に何が必要なのでしょうか? 答えは queue、つまり Queue です。その後、次のすべての名詞がこのQueueの周りに展開されます。
RabbitMQ に関する限り、Queue は論理実装の 1 つです。キューを操作してビジネス関数を実装するには、RabbitMQ に接続する必要があるため、 Connectionが存在します。一度接続するためのメッセージを送信しますが、これは明らかに無駄です。接続を確立する処理も非常に時間がかかるので、接続を管理させるものを作ります。これを使用すると、確立された接続を直接取り出してメッセージを送信でき、ConnectionFactory が来ます存在する。
次に、プログラムを開発するときに、複数のキューが使用される場合があります。オーダー キュー、メッセージ キュー、タスク キューなどが存在する場合があります。その場合、メッセージを異なるキューに送信する必要があるため、各キューに 1 つのキューを接続するという概念があります。チャネルと呼ばれます。
さらに、開発時にこのような関数を使用することがあります。つまり、メッセージを送信するときに、複数のキューで受信する必要があります。では、この問題をどのように解決するかというと、各キューにメッセージを与える必要があるのでしょうか?キューはメッセージを 1 回送信しますか? それは帯域幅とリソースの無駄ではないでしょうか? 何が考えられますか? もちろん、それを RabbitMQ サーバーに一度送信し、その後、RabbitMQ サーバーにどのキューに送信する必要があるかを分析させます。その後、Exchange がこれを実行します。しかし、私たちはそれをExchangeに
送信します。メッセージを送信するキューをどのようにして知るのでしょうか? ここでは、 RoutingKey と BindingKeyが使用されます
。BindingKey は、Exchange と Queue のバインディングのルールの説明です。この説明は、Exchange がメッセージを受信したときに解析するために使用されます。Exchange が受信したメッセージには、RoutingKey フィールドがあります。Exchange は、この RoutingKey と現在のExchange バインドされているすべての BindingKey が一致し、要件が満たされると、BindingKey でバインドされた Queue にメッセージが送信されます。このようにして、RabbitMQ にメッセージを一度送信し、別の Queue に分散するというプロセスを解決します。この時点で、すべての名詞を入力し、概要を説明します。
- ブローカー: メッセージ キュー サーバー エンティティ。その役割は、指定された方法でデータが送信されることを保証するために、プロデューサーからコンシューマーへのルートを維持することです。
- ConnectionFactory: RabbitMQ サーバーへの接続のマネージャー。
- 接続: RabbitMQ サーバーへの TCP 接続。
- チャネル: Exchange への接続。接続には複数のチャネルを含めることができます。Channel が必要な理由は、多重化のために TCP 接続の確立と解放に非常にコストがかかるためです。RabbitMQ では、クライアント スレッド間でチャネルを共有しないことをお勧めしますが、接続は可能な限り共有することをお勧めします。
- キュー: メッセージのキャリア。各メッセージは 1 つ以上のキューに入れられます。
- Exchange: メッセージ プロデューサーからメッセージを受け入れ、メッセージの RoutingKey、Exchange にバインドされた BindingKey、およびバインディング ルールに基づいて、メッセージをサーバーのキューにルーティングします。ExchangeType は、Exchange ルーティング メッセージの動作を決定します。たとえば、RabbitMQ では、ExchangeType には、direct、Fanout、Topic の 3 つのタイプがあります。Exchange ルーティングのタイプが異なると、動作も異なります。
- メッセージ キュー: コンシューマによって消費されていないメッセージを保存するために使用されるメッセージ キュー。
- メッセージ: ヘッダーと本文で構成されます。ヘッダーは、メッセージが永続化されるかどうか、どのメッセージ キューがメッセージを受け入れるか、優先度は何かなど、プロデューサーによって追加されたさまざまな属性のコレクションです。Body は、実際に送信する必要がある APP データです。
- RoutingKey: プロデューサがメッセージを送信するときに指定され、現在のメッセージを受け入れる人を指定します。
- BindingKey: Exchange と Message Queue をバインドするときにコンシューマによって指定され、現在の Exchange で現在バインドされているキューにどのような種類の RoutingKey が割り当てられるかを指定します。
- バインディング: Exchange とメッセージ キューに接続します。Exchange は、複数のメッセージ キューとバインドした後、ルーティング テーブルを生成します。ルーティング テーブルには、メッセージ キューに必要なメッセージの制約、つまりバインド キーが格納されます。Exchange がメッセージを受信すると、そのヘッダーを解析してルーティング キーを取得し、ルーティング キーとExchange Typeに基づいてメッセージをメッセージ キューにルーティングします。バインディング キーは Exchange と Message Queue をバインドするときにコンシューマによって指定され、ルーティング キーはメッセージを送信するときにプロデューサによって指定され、両者の一致方法は Exchange タイプによって決まります。
- サーバー: クライアント接続を受け入れ、AMQP メッセージ キューとルーティング機能を実装するプロセス。
- 仮想ホスト: これは実際には仮想概念であり、権限制御グループに似ています。仮想ホスト権限は、コマンドを通じてユーザーに割り当てることができます。デフォルトのゲスト ユーザーには管理者権限があります。最初のスペースは / です。複数の Exchange および Queue が存在する場合があります。ただし、アクセス許可制御の最小粒度は仮想ホストです。
5. SpringBoot は RabbitMQ の使用を統合します
5.1 依存関係のインポート
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
5.2 application.properties に設定を追加する
#对于rabbitMQ的支持
spring.rabbitmq.host=192.168.153.136
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
5.3 Hello-World シンプルキュー
プロデューサー、デフォルトのエクスチェンジ、キュー、およびコンシューマー
構造図 |
---|
1) キューオブジェクトを作成するための構成クラスを作成します。
package com.guo.simple;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SimpleQueueConfig {
@Bean
public Queue simple(){
return new Queue("simpleQueue");
}
}
2) プロデューサーを作成する
package com.guo.simple;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class SimpleQueueProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(){
System.out.println("SimpleQueueProducer");
//发送消息,第一个参数为队列名称,第二参数为消息内容
rabbitTemplate.convertAndSend("simpleQueue","简单模式");
}
}
3) 消費者の作成
package com.guo.simple;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SimpleQueueCustomer {
@RabbitListener(queues="simpleQueue")//监听指定的消息队列
public void receive(String content){
System.out.println("SimpleQueueCustomer");
System.out.println("来SimpleQueueProducer的信息:"+content);
}
}
4) src\test\java\com\guo\Rabbitmq01ApplicationTests.java でテストします。
package com.guo;
import com.guo.simple.SimpleQueueProducer;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Rabbitmq01ApplicationTests {
@Test
void contextLoads() {
}
@Autowired
private SimpleQueueProducer simpleQueueProducer;
@Test
public void testSimpleQueueProducer(){
simpleQueueProducer.send();
}
}
JavaBean オブジェクトを渡す場合、エンティティ クラスはシリアル化インターフェイスを実装する必要があり、具体的なプロセスは次のとおりです。
- lombok の依存関係をインポートし、User クラスを作成する
package com.guo.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private String username;
private String password;
}
- プロデューサーのコードを変更する
package com.guo.simple;
import com.guo.pojo.User;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
//生产者
@Component
public class SimplePublisher {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(){
System.out.println("SimplePublisher...");
//rabbitTemplate.convertAndSend("","simpleQueue","简单模式");
rabbitTemplate.convertAndSend("","simpleQueue",new User("张三","123"));
}
}
- コンシューマ内のコードを変更する
package com.guo.simple;
import com.guo.pojo.User;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
//消费者
@Component
public class SimpleConsumer {
// @RabbitListener(queues = "simpleQueue")
// public void receive(String content){
// System.out.println("SimpleConsumer...");
// System.out.println("来自SimplePublisher的消息:"+content);
// }
@RabbitListener(queues = "simpleQueue")
public void receive(User user){
System.out.println("SimpleConsumer...");
System.out.println("来自SimplePublisher的消息:"+user);
}
}
- テストクラスを実行するだけです。
5.4 ワークワークキュー
1 つのプロデューサー、1 つのデフォルト エクスチェンジ、1 つのキュー、2 つのコンシューマー
構造図 |
---|
1) キューオブジェクトを作成するための構成クラスを作成します。
package com.guo.work;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WorkQueueConfig {
@Bean
public Queue work(){
return new Queue("workQueue");
}
}
2) プロデューサーを作成する
package com.guo.work;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class WorkQueueProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(){
System.out.println("WorkQueueProducer");
rabbitTemplate.convertAndSend("workQueue","工作队列模式");
}
}
3) コンシューマの作成 この場合、2 つのコンシューマが作成されます。
package com.guo.work;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class WorkQueueCustomer_01 {
@RabbitListener(queues="workQueue")
public void receive(String content){
System.out.println("WorkQueueCustomer_01:"+content);
}
}
package com.guo.work;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class WorkQueueCustomer_02 {
@RabbitListener(queues="workQueue")
public void receive(String content){
System.out.println("WorkQueueCustomer_02:"+content);
}
}
4) テスト用にオブジェクトとメソッドをテストクラスに追加します。
@Autowired
private WorkQueueProducer workQueueProducer;
@Test
public void testWorkQueueProducer(){
for (int i = 0; i<100; i++){
workQueueProducer.send();
}
}
5.5 パブリッシュ/サブスクライブ パブリッシュおよびサブスクライブ モデル
1 つのプロデューサー、1 つのエクスチェンジ、2 つのキュー、2 つのコンシューマー
構造図 |
---|
このモードを使用するにはスイッチを使用する必要があり、プロデューサーはメッセージをスイッチに送信し、スイッチを介してキューに到達します。
スイッチは 4 つあります: direct/topic/headers/fanout。デフォルトのスイッチは direct です。パブリッシングとサブスクリプションの実装では、4 番目のスイッチ タイプのファンアウトが使用されます。
スイッチを使用する場合、各コンシューマーは独自のキューを持ち、プロデューサーはメッセージをスイッチ (X) に送信し、各キューはスイッチにバインドされます。
この例では:
2つのメッセージキューを作成する
ファンアウト スイッチ オブジェクトを作成する
スイッチとキューをバインドする
1) 構成クラスの作成
package com.guo.fanout;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
//创建两个队列
@Bean
public Queue fanoutQueue1(){
return new Queue("fanoutQueue1");
}
@Bean
public Queue fanoutQueue2(){
return new Queue("fanoutQueue2");
}
//创建一个交换机
@Bean
public FanoutExchange fanoutExchange(){
return new FanoutExchange("fanoutExchange");
}
//将两个队列绑定到交换机上
@Bean
public Binding bindingFanoutQueue1(){
return BindingBuilder.bind(fanoutQueue1()).to(fanoutExchange());
}
@Bean
public Binding bindingFanoutQueue2(){
return BindingBuilder.bind(fanoutQueue2()).to(fanoutExchange());
}
}
2) プロデューサーを作成する
package com.guo.fanout;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class FanoutProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(){
System.out.println("FanoutProducer");
//第一个参数是交换机的名称 ,第二个参数是routerKey 这里设置为空字符串即可,第三个参数是要发送的消息
rabbitTemplate.convertAndSend("fanoutExchange","","发布/订阅");
}
}
3) コンシューマの作成 この場合、2 つのコンシューマが作成されます。
package com.guo.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class FanoutCustomer_01 {
@RabbitListener(queues = "fanoutQueue1")
public void receive(String content){
System.out.println("FanoutCustomer_01:"+content);
}
}
package com.guo.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class FanoutCustomer_02 {
@RabbitListener(queues = "fanoutQueue2")
public void receive(String content){
System.out.println("FanoutCustomer_02:"+content);
}
}
4) テスト用にオブジェクトとメソッドをテストクラスに追加します。
@Autowired
private FanoutProducer fanoutProducer;
@Test
public void testFanoutProducer(){
fanoutProducer.send();
}
5.6 ルーティングルーティングモード
1 つのプロデューサー、1 つのエクスチェンジ、2 つのキュー、2 つのコンシューマー
構造図 |
---|
プロデューサはメッセージをダイレクト スイッチに送信します (ルーティング モードはダイレクト スイッチを使用して実装する必要があります)。キューとスイッチをバインドするときにルーティング キーがあります。プロデューサによって送信されるメッセージはルーティング キーを指定します。の場合、メッセージは対応するキーにのみ送信されます。同じキーを持つキューは、キューのコンシューマをリッスンしてメッセージを消費します。つまり、消費者がメッセージを選択的に受信できるようにするためです。
この例では:
2つのメッセージキューを作成する
ダイレクトスイッチオブジェクトを作成する
スイッチとキューをバインドする
1) 構成クラスの作成
package com.guo.direct;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DirectConfig {
@Bean
public Queue directQueue1(){
return new Queue("directQueue1");
}
@Bean
public Queue directQueue2(){
return new Queue("directQueue2");
}
@Bean
public DirectExchange directExchange(){
return new DirectExchange("directExchange");
}
@Bean
public Binding bingDirectQueue1(){
return BindingBuilder.bind(directQueue1()).to(directExchange()).with("zhangsan");
}
@Bean
public Binding bingDirectQueue2(){
return BindingBuilder.bind(directQueue2()).to(directExchange()).with("lisi");
}
}
2) プロデューサーを作成する
package com.guo.direct;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class DirectProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private DirectExchange directExchange;
public void send(){
System.out.println("DirectProducer");
rabbitTemplate.convertAndSend(directExchange.getName(),"zhangsan","zhangsanContent");
rabbitTemplate.convertAndSend(directExchange.getName(),"lisi","lisiContent");
}
}
3) 2 つのコンシューマを作成する
package com.guo.direct;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class DirectCustomer_01 {
@RabbitListener(queues = "directQueue1")
public void receive(String content){
System.out.println("DirectCustomer_01:"+content);
}
}
package com.guo.direct;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class DirectCustomer_02 {
@RabbitListener(queues = "directQueue2")
public void receive(String content){
System.out.println("DirectCustomer_02:"+content);
}
}
4) テスト用にオブジェクトとメソッドをテストクラスに追加します。
@Autowired
private DirectProducer directProducer;
@Test
public void testDirectProducer(){
directProducer.send();
}
5.7 トピックテーマモード
1 つのプロデューサー、1 つのエクスチェンジ、2 つのキュー、2 つのコンシューマー
構造図 |
---|
ワイルドカード モードとも呼ばれます (あいまい一致として理解でき、ルーティング モードは完全一致と同等です)。
直接接続スイッチを使用するとシステムを改善できますが、複数条件のルーティングを実装できないという制限がまだあります。
メッセージング システムでは、ルーティング キーに基づいてキューをサブスクライブするだけでなく、運用メッセージに基づいてソースもサブスクライブしたいと考えます。このとき、トピックスイッチを使用できます。
トピック スイッチを使用する場合、ルーティング キーは任意の方法で記述することはできず、ドットで区切られた意味のある単語の形式にする必要があります。例えば「goods.stock.info」など。ルーティング キーは最大 255 バイトです。
* は単語を表します
# 記号は 0 個以上の単語を表します
1) 構成クラスの作成
package com.guo.topic;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TopicConfig {
@Bean
public Queue topicQueue1(){
return new Queue("topicQueue1");
}
@Bean
public Queue topicQueue2(){
return new Queue("topicQueue2");
}
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("topicExchange");
}
@Bean
public Binding bingTopicQueue1(){
return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("wangwu.*");
}
@Bean
public Binding bingTopicQueue2(){
return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("zhaoliu.#");
}
}
2) プロデューサーを作成する
package com.guo.topic;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class TopicProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private TopicExchange topicExchange;
public void send(){
System.out.println("TopicProducer");
rabbitTemplate.convertAndSend(topicExchange.getName(),"wangwu.abc","wangwuContent"); rabbitTemplate.convertAndSend(topicExchange.getName(),"zhaoliu.xyz.qwer","zhaoliuContent");
}
}
3) 2 つのコンシューマを作成する
package com.guo.topic;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class TopicCustomer_01 {
@RabbitListener(queues = "topicQueue1")
public void receive(String content){
System.out.println("TopicCustomer_01:"+content);
}
}
package com.guo.topic;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class TopicCustomer_02 {
@RabbitListener(queues = "topicQueue2")
public void receive(String content){
System.out.println("TopicCustomer_02:"+content);
}
}
4) テスト用にオブジェクトとメソッドをテストクラスに追加します。
@Autowired
private TopicProducer topicProducer;
@Test
public void testTopicProducer(){
topicProducer.send();
}
5.8 手動確認
RabbitMQ での Ack: 主に、メッセージがコンシューマによって消費されたことを確認し、キュー内のメッセージをクリアするようにサーバーに通知します。spring-boot-data-amqp は自動 ACK メカニズムであり、MQ が自動的にサポートしてくれることを意味します。メッセージが送信されます。ACK に移動して、キュー内のメッセージを削除します。これにより、いくつかの問題が発生します。コンシューマーがメッセージの処理に時間がかかる場合、またはメッセージの消費時に例外が発生した場合、問題が発生します。手動 ACKメッセージの繰り返しの消費を避けることができます。
5.8.1 ネイティブモードのテスト
1. シンプル モードを例にとると、コンシューマを変更し、テストのためにプロデューサを起動するだけで済みます。
package com.guo.helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
//消费者
public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("消费者启动...");
//创建连接
ConnectionFactory factory = new ConnectionFactory();
//设置参数
factory.setUsername("guest");
factory.setPassword("guest");
factory.setHost("192.168.25.134");
factory.setPort(5672);//浏览器访问的是:15672
//获取连接
Connection connection = factory.newConnection();
//获取Channel
Channel channel = connection.createChannel();
//回调,创建Consumer
DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
//获取消息
System.out.println(new String(body, "UTF-8"));
//模拟异常
int i = 1/0;
//手动ack
//参数1:deliveryTag:投递过来消息的标签,由MQ打的,从1开始,1,2,3...
//参数2:multiple:是否批量确认之前已经消费过的消息,一般为false
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (Exception e) {
//捕获所有异常
//第三个参数:requeue -> true表示重新放入队列,false -> 放弃该消息
//channel.basicNack(envelope.getDeliveryTag(), false, true);
//抛弃此条消息
channel.basicNack(envelope.getDeliveryTag(), false, false);
e.printStackTrace();
//关闭,否则一直循环当前操作
connection.close();
}
}
};
//获取消息
//1.队列名称(从哪个队列中获取消息)
//2.true表示自动ack(消费完消息之后,自动告诉rabbitmq)
// false表示手动ack,需要自己收到调用方法
//3.回调
channel.basicConsume("helloworld",false,defaultConsumer);
//由于channel会回调DefaultConsumer中的handleDelivery方法,直接关闭会报错,可以在关闭之前调用Thread.sleep();
//或者可以不调用关闭方法(消费方一般不关闭,有消息过来可以及时处理)
//channel.close();
//connection.close();
}
}
5.8.2 SpringBoot でのテスト
1. application.properties に構成を追加します。
#配置手动Ack
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2. 以前にテストしたモードに AckCustomer デモを追加します
package com.guo.simple;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
//消费者:监听队列中的消息,然后获取消息
@Component
@RabbitListener(queues = "simpleQueue")
public class AckCustomer {
//处理消息(获取消息,进一步操作)
@RabbitHandler
public void receive(String message,Channel channel,Message msg){
System.out.println("AckCustomer...");
//获取消息内容
if(message!=null && message.length()>0){
try {
System.out.println("获取消息:"+message);
int i = 1/0;
//手动确认
long deliveryTag = msg.getMessageProperties().getDeliveryTag();
System.out.println("deliveryTag:"+deliveryTag);
channel.basicAck(deliveryTag,false);
} catch (Exception e) {
//捕获所有异常
System.out.println("消息处理...");
try {
//重新放入队列中
//channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,true);
//放弃消息
channel.basicNack(msg.getMessageProperties().getDeliveryTag(),false,false);
e.printStackTrace();
//关闭
channel.getConnection().close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}else{
System.out.println("没有消息");
}
}
}
コメントするか、テスト用の例外を開いてください。
6. トランザクションと確認のメカニズム
6.1 メッセージの信頼性
考える?
1. メッセージが RabbitMQ に到達し、RabbitMQ がダウンした場合、メッセージは失われますか?
Queueの永続化メカニズムを使用できます
2. コンシューマがメッセージを消費しているときに、プログラムが途中で実行されてコンシューマがクラッシュした場合はどうすればよいですか?
手動で確認できます
3. プロデューサがメッセージを送信するときに、ネットワークの問題によりメッセージが RabbitMQ に送信されない場合はどうすればよいですか?
RabbitMQ は、トランザクション操作と確認および返却メカニズムを提供します。
メッセージの配信を確実にするために、RabbitMQ でトランザクションを使用できます。トランザクションはメッセージの 100% 配信を保証できます。トランザクションのロールバックを通じてログを記録し、後で現在のメッセージを定期的に再送信できます。ただし、トランザクションの操作効率が低すぎます。
RabbitMQ は、トランザクションに加えて、トランザクションよりもはるかに効率的な確認確認メカニズムも提供します。
6.2 RabbitMQ トランザクション
RabbitMQ トランザクションは AMQP プロトコルの実装であり、
Channel
モードを設定することで完了します。具体的な操作は次のとおりです。
channel.txSelect(); //开启事务
// ....本地事务操作
channel.txCommit(); //提交事务
channel.txRollback(); //回滚事务
特記事項: RabbitMQ のトランザクション メカニズムは同期操作であるため、RabbitMQ のパフォーマンスが大幅に低下します。
6.3 メカニズムの確認
RabbitMQ のトランザクション パフォーマンスの問題により、送信者確認モードが導入されました。
6.3.1 ツールクラスの作成
package com.guo.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMQUtils {
/**
* 1. 创建连接工厂(ConnectionFactory)
* 2. 创建连接 (Connection)
* 3. 创建通道 (Channel)
Connection conn = connectionFactory.newConnection();
Channel channel = conn.createChannel();
*/
private static ConnectionFactory connectionFactory;
static {
connectionFactory = new ConnectionFactory();
connectionFactory.setPort(5672);
connectionFactory.setUsername("guest");
connectionFactory.setPassword("guest");
connectionFactory.setHost("192.168.25.134");
}
public static Connection getConnection() {
Connection connection = null;
try {
connection = connectionFactory.newConnection();
} catch (IOException ioException) {
ioException.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
return connection;
}
public static void close(Channel channel, Connection connection) {
try {
if(null != channel) {
channel.close();
}
if(null != connection){
connection.close();
}
}catch (Exception ex) {
ex.printStackTrace();
}
}
}
6.3.2 単一メッセージの確認
channel.confirmSelect(); //开启发送方确认模式
1. RabbitMq コンソール ページで、ダイレクト タイプ スイッチを作成し、キューを作成してバインドします。
channel.waitForconfirms(); //単一メッセージの確認の場合、true は成功を示します
package com.guo.confirm;
import com.guo.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class ConfirmTest1 {
public static void main(String[] args) throws Exception {
//获取连接
Connection connection = RabbitMQUtils.getConnection();
//获取Channel
Channel channel = connection.createChannel();
//开启confirm
channel.confirmSelect();
//发送消息(前提:队列已经通过routingKey绑定到该交换机上)
channel.basicPublish("myExchange","my",null,"消息内容".getBytes());
//判断消息到达交换机,true表示到达,若没有交换机则系统会直接报错
if(channel.waitForConfirms()){
System.out.println("消息已到达交换机");
}
//设置延迟关闭,否则可能无法显示相关信息
Thread.sleep(5000);
//关闭
RabbitMQUtils.close(channel,connection);
}
}
6.3.2 バッチメッセージの確認
channel.waitForconfirmsOrDie(); //バッチメッセージ確認、1 つのメッセージが正常に送信されなかった場合、例外がスローされます
package com.guo.confirm;
import com.guo.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
public class ComfirmTest2 {
public static void main(String[] args) throws Exception {
//获取连接
Connection connection = RabbitMQUtils.getConnection();
//获取Channel
Channel channel = connection.createChannel();
//开启confirm
channel.confirmSelect();
//发送消息(前提:队列已经通过routingKey绑定到该交换机上)
for (int i = 0; i < 10; i++) {
//判断
if(i == 5){
//前提是系统中没有名为myExchange2的交换机
channel.basicPublish("myExchange2", "my", null, ("消息内容"+ i).getBytes());
continue;
}
//发送消息
channel.basicPublish("myExchange", "my", null, ("消息内容"+ i).getBytes());
}
//确定批量操作是否成功
//当发送的全部消息,有一个失败的时候,就直接全部失败 抛出异常
channel.waitForConfirmsOrDie();
//设置延迟关闭,否则可能无法显示相关信息
Thread.sleep(5000);
//关闭
RabbitMQUtils.close(channel,connection);
}
}
6.3.3 コールバックメソッドの確認
package com.guo.confirm;
import com.guo.utils.RabbitMQUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmListener;
import com.rabbitmq.client.Connection;
import java.io.IOException;
public class ComfirmTest3 {
public static void main(String[] args) throws Exception {
//获取连接
Connection connection = RabbitMQUtils.getConnection();
//获取Channel
Channel channel = connection.createChannel();
//开启confirm
channel.confirmSelect();
//发送消息(前提:队列已经通过routingKey绑定到该交换机上)
channel.basicPublish("myExchange","my",null,"消息内容".getBytes());
//开启异步回调
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("成功达到交换机");
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("没有到达交换机");
}
});
//设置延迟关闭,否则可能无法显示相关信息
Thread.sleep(5000);
//关闭
RabbitMQUtils.close(channel,connection);
}
}
6.4 復帰機構
確認は、メッセージが交換機に到達することを保証することしかできませんが、交換機によってメッセージが指定されたキューに配布されることを保証することはできません。
さらに、Exchange はメッセージを永続化できませんが、Queue はメッセージを永続化できます。
Return メカニズムを使用して、メッセージが交換局から指定されたキューに送信されたかどうかを監視します。
リターン メカニズムをオンにする。メッセージを送信するときは、必須を true に指定する必要があります。
package com.guo.confirm;
import com.guo.utils.RabbitMQUtils;
import com.rabbitmq.client.*;
import java.io.IOException;
public class ReturnTest {
public static void main(String[] args) throws Exception {
//获取连接
Connection connection = RabbitMQUtils.getConnection();
//获取Channel
Channel channel = connection.createChannel();
//开启confirm
channel.confirmSelect();
//发送消息
//注意:指定mandatory参数为true,设置没有绑定的routingkey
channel.basicPublish("myExchange","my2",true,null,"消息内容".getBytes());
//开启异步回调
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
System.out.println("成功达到交换机");
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
System.out.println("没有到达交换机");
}
});
//没有到达队列的时候触发,设置错误的路由的key的名称进行测试
channel.addReturnListener(new ReturnListener() {
@Override
public void handleReturn(int replyCode, String replyText, String exchange,
String routingKey, AMQP.BasicProperties properties,
byte[] body) throws IOException {
System.out.println("没有到达队列");
//如果消息没有到达队列,我们可以获取到消息的相关信息
System.out.println("exchange:" + exchange);
System.out.println("routingKey:" + routingKey);
System.out.println("body:" + new String(body));
//然后人工干预,考虑编写一个补偿机制,把消息保存到redis或者mysql中,保证消息不丢失
//....
}
});
//设置延迟关闭,否则可能无法显示相关信息
Thread.sleep(5000);
//关闭
RabbitMQUtils.close(channel,connection);
}
}
6.5 SpringBootの実装
1. application.properties に構成を追加します。
spring.rabbitmq.publisher-confirm-typeの対応する値の説明
- NONE: リリース確認モードを無効にします。これはデフォルト値です。
- 相関: コールバック メソッドは、メッセージが交換に正常にパブリッシュされた後にトリガーされます。
- シンプル: 2 つのエフェクト
- CORRELATED 値と同様に、コールバック メソッドがトリガーされます。
- メッセージの発行に成功したら、rabbitTemplate を使用して waitForconfirms または waitForconfirmsOrDie メソッドを呼び出し、ブローカー ノードから送信結果が返されるのを待ちます。返された結果に基づいて、次のステップのロジックを決定します。メソッドが false を返すと、チャネルが閉じられ、次にメッセージを送信できなくなります。
# 配置开启Confirm和Return
spring.rabbitmq.publisher-confirm-type: simple
spring.rabbitmq.publisher-returns: true
2. 構成クラスを作成する
package com.guo.config;
import org.springframework.amqp.core.Message;
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.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
@Configuration
public class PublisherConfirmAndReturnConfig implements
RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnsCallback {
@Resource
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
}
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("已到达交换机");
}
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("未到达队列");
//考虑人工干预,获取消息信息进行保存
String exchange = returnedMessage.getExchange();
String routingKey = returnedMessage.getRoutingKey();
Message message = returnedMessage.getMessage();
System.out.println("exchange:" + exchange);
System.out.println("routingKey:" + routingKey);
System.out.println("message:" + new String(message.getBody()));
}
}
3. プロデューサのメッセージ送信メソッドの routingKey を変更し、テストクラスのテストを開始します。
7. デッドレターキュー
デッドレターキューは特別なキューではなく、普通のキューですが、これをデッドレターキューと呼びます。
デッドレターキューの設計は、あるキューのヘッダ情報に
x-dead-letter-exchange
(デッドレタースイッチ)とx-dead-letter-routing-key
(デッドレタールーティングキー)を設定することになります。デッドレター交換にバインドされたキューに関連付けられています。次に、キューの有効期限または指定したメッセージの有効期限を指定すると、期限切れ後にメッセージは自動的にデッドレター キューに到着します。
7.1 シナリオ
シナリオ 1: 未払いの注文は指定された時間内にキャンセルされます。実装では、注文メッセージをキューに入れ、その有効期限を指定します。有効期限が切れると、メッセージはデッド レター キューに入り、デッド レター キューのコンシューマ側から対応するメッセージを直接取得できます。
シナリオ 2: 特定のメッセージがコンシューマー側で何度も消費されようとしましたが、正常に消費されず、デッド レター キューに入り、手動による介入が可能になります。
7.2 テスト
事前に RabbitMQUtils ツール クラスを作成してテストするだけです
package com.guo.dead;
import com.guo.utils.RabbitMQUtils;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.util.HashMap;
import java.util.Map;
public class DeadQueueTest {
//死信交换机
private static final String dead_letter_exchange = "dead_letter_exchange";
//死信路由键
private static final String dead_letter_routing_key = "dead_letter_routing_key";
//死信队列
private static final String dead_letter_queue = "dead_letter_queue";
private static final String people_exchange = "people_exchange";
private static final String people_routing_key = "people_routing_key";
private static final String people_queue = "people_queue";
public static void main(String[] args) throws Exception{
//获取连接
Connection connection = RabbitMQUtils.getConnection();
Channel channel = connection.createChannel();
// 创建一个死信的交换机
channel.exchangeDeclare(dead_letter_exchange, "direct");
// 创建死信队列
channel.queueDeclare(dead_letter_queue, true, false, false, null);
// 将死信队列绑定到死信交换机,路由键为 "dead_letter_routing_key"
channel.queueBind(dead_letter_queue, dead_letter_exchange, dead_letter_routing_key);
//设置队列参数
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", dead_letter_exchange);
arguments.put("x-dead-letter-routing-key", dead_letter_routing_key);
//创建当前交换机,队列以及路由键
channel.exchangeDeclare(people_exchange, "direct");
//最后一个参数是当前队列的属性
channel.queueDeclare(people_queue, true, false, false, arguments);
channel.queueBind(people_queue, people_exchange, people_routing_key);
//设置消息的过期时间,单位:毫秒
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("15000").build();
//发送消息
channel.basicPublish(people_exchange, people_routing_key, properties, "dead_message".getBytes());
RabbitMQUtils.close(channel, connection);
}
}
8. メッセージの繰り返しの消費を避ける
8.1 冪等性
すべてのメッセージミドルウェアには、このような問題、つまりメッセージが繰り返し消費されるという問題があるため、冪等設計を行う必要があります。消費された。
メッセージを繰り返し消費すると、非べき等操作で問題が発生します。メッセージが繰り返し消費される理由は、コンシューマが RabbitMQ に Ack を与えないためです。
メッセージが繰り返し消費される問題を解決するために、Redis を使用することができ、コンシューマーがメッセージを消費する前に、メッセージ ID が Redis に配置されるようになりました。
id-0(業務執行)
id-1(業務遂行成功)
ack が失敗した場合、RabbitMQ が他のコンシューマにメッセージを渡すときに、setnx が最初に実行されます。キーがすでに存在する場合は、その値が取得されます。0 の場合は、コンシューマがビジネス ロジックを実行していることを意味します。1 の場合は、消費者がビジネス ロジックを実行していることを意味します。消費は完了しており、直接確認されます。
極端な例: 最初のコンシューマがビジネスを実行しているときにデッドロックが発生する setnx に基づいて、キーの生存時間を設定します。
8.2 解決策
解決策 1: 各メッセージに対してグローバルに一意の ID を生成し、その ID とビジネス データを同じトランザクションに入れ、各メッセージの消費後にその ID をテーブルに挿入し、各消費の前に ID が存在するかどうかを確認します。 、対応するロジックが実行され、存在する場合は直接確認されます。
オプション 2 (推奨): Redis + データベース ソリューションを使用して冪等設計を実現します。実装のアイデアは Redis キャッシュ ブレークダウン ソリューションと似ています。データを挿入するときに、一意の ID を同時にデータベースに挿入し、それをredis に有効期限を設定し、その都度redisから判断します。
8.3 スプリングブートでのテスト
8.3.1 依存関係のインポート
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
8.3.2 application.yml の設定
#配置rabbitmq
spring:
rabbitmq:
username: guest
password: guest
host: 192.168.25.131
port: 5672
listener:
simple:
acknowledge-mode: manual #表示手动Ack
#confirm和return配置
publisher-confirm-type: simple
publisher-returns: true
#配置redis
redis:
host: 127.0.0.1
port: 6379
database: 0
lettuce:
pool:
max-active: 8
8.3.3 シンプルモードでのデモはこちら
1.設定クラスの書き込み
package com.guo.config;
import org.ietf.jgss.MessageProp;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author yangl
* @version 1.0
* @date 2022/12/1 16:55
*/
@Configuration
public class SimplestConfig {
@Bean
public Queue simplest(){
return new Queue("simplestQueue");
}
//返回MessagePostProcessor,获取消息id
@Bean
public MessagePostProcessor getMessagePostProcessor(){
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
return message;
}
@Override
public Message postProcessMessage(Message message, Correlation correlation, String exchange, String routingKey) {
//获取消息参数
MessageProperties messageProperties = message.getMessageProperties();
//获取消息id
String correlationDataId = ((CorrelationData) correlation).getId();
System.out.println("配置类中的消息id:" + correlationDataId);
messageProperties.setCorrelationId(correlationDataId);
return message;
}
};
return messagePostProcessor;
}
}
2. プロデューサーを書く
package com.guo.simplest;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 生产者
*/
@Component
public class SimplestProducer {
@Resource
private RabbitTemplate rabbitTemplate;
@Resource
private MessagePostProcessor messagePostProcessor;
public void send(){
System.out.println("SimplestProducer生产者");
//发送消息
//参数一:队列名称(或是路由规则)
//参数二:消息内容
CorrelationData correlationData = new CorrelationData();
String id = correlationData.getId();
System.out.println("生产者中的消息id:" + id);
rabbitTemplate.convertAndSend("","simplestQueue","简单队列",messagePostProcessor,correlationData);
}
}
3. コンシューマを書き込む
package com.guo.simplest;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
*
* 消费者
*/
@Component
public class SimplestCustomer {
@Resource
private StringRedisTemplate stringRedisTemplate;
//监听队列中的消息
@RabbitListener(queues = "simplestQueue")
public void recrive(String content, Channel channel, Message message) throws Exception {
//获取消息id
String correlationId = message.getMessageProperties().getCorrelationId();
System.out.println("消费者中的消息id:" + correlationId);
//存到redis中,使用setIfAbsent方法,防止死锁
Boolean flag = stringRedisTemplate.opsForValue()
.setIfAbsent(correlationId, "0", 600, TimeUnit.SECONDS);
//判断
if(flag){
System.out.println("SimplestCustomer消费者");
System.out.println("接收来自simplestQueue中的消息:" + content);
//业务执行完毕了,使用set方法修改key的值
stringRedisTemplate.opsForValue()
.set(correlationId, "1", 600, TimeUnit.SECONDS);
//手动ack
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
return;
}
//如果key没有设置成功,意味着redis中已经有这个消息的记录了
//判断,1表示已经执行完毕
if("1".equalsIgnoreCase(stringRedisTemplate.opsForValue().get(correlationId))){
System.out.println("消息已被消费过,直接Ack");
//直接手动Ack就可以
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
}
}
}
4. テストタイプをテストするだけです
package com.guo;
import com.guo.simplest.SimplestProducer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Springboot17RabbitmqApplicationTests {
@Test
void contextLoads() {
}
//简单模式
@Autowired
private SimplestProducer simplestProducer;
@Test
public void testSimplestProducer(){
simplestProducer.send();
}
}
テストが成功したら、redis で確認してください。この時点で、ID は生成されており、値は 1 です。次に、SimplestCustomer コンシューマー クラスの最初の手動 Ack メソッドをコメント アウトして、再度実行します。これは、メッセージがただし、rabbitmq キューにはメッセージがまだ残っているため、プロジェクトのスタートアップ クラスを開始し、リッスン キューを自動的に呼び出すコンシューマのメソッドをロードし、値がコンシューマにあると判断するメソッドを実行します。 redis は 1 で、手動で直接 Ack します。