記事ディレクトリ
- MQ の一般的な問題のいくつか
環境準備:MQデプロイ【docker環境】
- ダウンロードミラー
docker pull rabbitmq:3.8-management
- MQ をインストールする
docker run \ -e RABBITMQ_DEFAULT_USER=itcast \ -e RABBITMQ_DEFAULT_PASS=123321 \ -v mq-plugins:/plugins \ --name mq1 \ --hostname mq1 \ -p 15672:15672 \ -p 5672:5672 \ -d \ rabbitmq:3.8-management
- テスト
[root@kongyue ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7eea857604d3 rabbitmq:3.8-management "docker-entrypoint.s…" 17 seconds ago Up 16 seconds 4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, :::5672->5672/tcp, 15671/tcp, 15691-15692/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp, :::15672->15672/tcp mq1
メッセージの信頼性
- メッセージの送信からコンシューマーによる受信まで、複数のプロセスを管理します。これらの各手順はメッセージの損失につながる可能性があります。損失の一般的な理由は次のとおりです。
- これらの問題に対して、RabbitMQ はそれぞれ次の解決策を提供します。
- 製作者確認の仕組み
- mq 永続性
- 消費者確認の仕組み
- 失敗時の再試行メカニズム
プロデューサーメッセージの確認応答
RabbitMQ は、MQ に送信されたメッセージの損失を回避するための発行者確認メカニズムを提供します。このメカニズムでは、各メッセージに一意の ID を割り当てる必要があります。メッセージが MQ に送信されると、メッセージが正常に処理されたかどうかを示す結果が送信者に返されます。結果を返す方法は 2 つあります。
- 発行者確認、送信者確認
- メッセージはスイッチに正常に配信され、ack を返します。
- メッセージは取引所に配信されませんでした。応答なし
- メッセージ送信中に例外が発生し、受信が受信されませんでした
- 発行者返送、差出人受領書
- メッセージは交換局に配信されますが、キューにはルーティングされません。ACK とルーティング失敗の理由を返します。
- メッセージは交換局に配信されますが、キューにはルーティングされません。ACK とルーティング失敗の理由を返します。
- 注: 確認メカニズムがメッセージを送信するとき、異なるメッセージを区別し、ACK の競合を避けるために、各メッセージにグローバルに一意の ID を設定する必要があります。
プロジェクト構成
- まず、プロデューサー サービスの application.yml ファイルを変更し、次の内容を追加します。
spring: rabbitmq: publisher-confirm-type: correlated #生产者确认类型 publisher-returns: true #开启publish-return功能 template: mandatory: true #定义消息路由失败时的策略
publish-confirm-type
: Publisher-confirm を有効にします。ここでは 2 つのタイプがサポートされています。simple
: タイムアウトになるまで確認結果を同期的に待ちます。correlated
: 非同期コールバック、ContinueCallback を定義します。MQ が結果を返すときにこの confirmCallback がコールバックされます。
publish-returns
:publish-return 関数を有効にします。これもコールバック メカニズムに基づいていますが、ReturnCallback を定義します。template.mandatory
: メッセージのルーティングが失敗した場合のポリシーを定義します。true、ReturnCallback を呼び出す、false: メッセージを直接破棄する
Return コールバックと confirmCallback を定義する
- メッセージは交換に正常に送信されましたが、キューにルーティングされなかった場合は、ReturnCallback を呼び出します。
- 各 RabbitTemplate は ReturnCallback を 1 つだけ設定できるため、プロジェクトのロード時に設定する必要があります。
- プロデューサー サービスを変更して追加します。
import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Configuration; @Slf4j @Configuration public class CommonConfig implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // 获取RabbitTemplate RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); // 设置ReturnCallback rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { // 投递失败,记录日志 log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}", replyCode, replyText, exchange, routingKey, message.toString()); // 如果有业务需要,可以重发消息 }); } }
- 成功または失敗を確認する各業務処理のロジックは必ずしも同じではないため、メッセージ送信時に confirmCallback を指定できます。
- 単体テストメソッドを定義します。
public void testSendMessage2SimpleQueue() throws InterruptedException { // 1.消息体 String message = "hello, spring amqp!"; // 2.全局唯一的消息ID,需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 3.添加callback correlationData.getFuture().addCallback( result -> { if(result.isAck()){ // 3.1.ack,消息成功 log.debug("消息发送成功, ID:{}", correlationData.getId()); }else{ // 3.2.nack,消息失败 log.error("消息发送失败, ID:{}, 原因{}",correlationData.getId(), result.getReason()); } }, ex -> log.error("消息发送异常, ID:{}, 原因{}",correlationData.getId(),ex.getMessage()) ); // 4.发送消息 rabbitTemplate.convertAndSend("task.direct", "task", message, correlationData); // 休眠一会儿,等待ack回执 Thread.sleep(2000); }
メッセージの永続性
- MQ はデフォルトでメッセージをメモリに保存します。メッセージを RabbitMQ に安全に保存するには、メッセージ永続化メカニズムを有効にして、MQ にキャッシュされたメッセージが失われないようにする必要があります。
- スイッチの永続性
- キューの永続性
- メッセージの永続性
スイッチの永続性
- SpringAMQP では、スイッチの永続性をコードで指定できます。
@Bean public DirectExchange simpleExchange(){ // 三个参数:交换机名称、是否持久化、当没有queue与其绑定时是否自动删除 return new DirectExchange("simple.direct", true, false); }
- デフォルトでは、Spring AMQP によって宣言されたスイッチは永続的です。
- RabbitMQ コンソールでは、永続スイッチに次のマークが表示されます
D
。
キューの永続性
- SpringAMQP では、スイッチの永続性をコードで指定できます。
@Bean public Queue simpleQueue(){ // 使用QueueBuilder构建队列,durable就是持久化的 return QueueBuilder.durable("simple.queue").build(); }
- デフォルトでは、Spring AMQP によって宣言されたキューは永続的です。
- RabbitMQ コンソールでは、永続キューが
D
次のようにマークされることを確認します。
メッセージの永続性
SpringAMQP を使用してメッセージを送信する場合、メッセージのプロパティ (MessageProperties) を設定し、配信モードを指定できます。
- 1: 非永続的
- 2:持久化
@Test
public void testDuarbleMessage() {
//创建消息
Message message = MessageBuilder.withBody("hello, ttl queue".getBytes(StandardCharsets.UTF_8))
.setDeliveryMode(MessageDeliveryMode.PERSISTENT)
.build();
//消息ID,需要封装到需要封装到CorrelationData中
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//发送消息
rabbitTemplate.convertAndSend("simple.queue",message,correlationData);
log.debug("发送消息成功!");
}
- デフォルトでは、Spring AMQP によって送信されるメッセージは、指定しなくても永続的です。
消費者メッセージの確認
- RabbitMQ は、メッセージを読み取った後に書き込むメカニズムであり、メッセージがコンシューマーによって消費された後、すぐに削除されることを確認します。RabbitMQ はコンシューマ確認メカニズムをサポートしています。つまり、コンシューマはメッセージの処理後に確認応答を MQ に送信でき、MQ は確認確認の受信後にメッセージを削除します。
次のシナリオを想像してください。
- 1) RabbitMQ はコンシューマにメッセージを配信します
- 2) コンシューマーはメッセージを受信した後、RabbitMQ に ACK を返します。
- 3) RabbitMQ 削除メッセージ
- 4) コンシューマがダウンし、メッセージが処理されない
ため、メッセージは失われます。したがって、コンシューマが ACK を返すタイミングは非常に重要です。
SpringAMQP では、次の 3 つの確認モードを構成できます。
- 手動: 手動 ACK。ビジネス コードの終了後に API を呼び出して ACK を送信する必要があります。
- auto: 自動 ack、リスナー コードは Spring によって監視され、例外があるかどうかが確認されます。例外がなければ ack を返し、例外がスローされた場合は nack を返します。
- none: close ack、MQ はコンシューマーがメッセージを取得した後に正常に処理すると想定するため、メッセージは配信後すぐに削除されます。
このことから次のことがわかります。
- なしモードでは、メッセージ配信の信頼性が低く、失われる可能性があります
- auto モードはトランザクション メカニズムに似ており、例外が発生すると nack を返し、メッセージは mq にロールバックされ、例外がなければ ack を返します。
- 手動: ビジネスの状況に応じて、どの時点で ACK が
正常であるかを判断します。私たちは全員、デフォルトの自動を使用します。
なしモードのデモ
- 確認メカニズムを変更します。
spring: rabbitmq: listener: simple: acknowledge-mode: auto # 关闭ack
- 異常な位置でポイントをブレークし、メッセージを再送信します。プログラムがブレークポイントでスタックすると、メッセージのステータスが unack (未確定ステータス) であることがわかります。
- 例外がスローされた後、Spring は自動的に nack を返すため、メッセージは Ready 状態に戻り、RabbitMQ によって削除されません。
消費失敗リトライ機構
- コンシューマに例外が発生すると、メッセージは引き続きキューに再キューイング (キューに再入力) され、その後コンシューマに再送信されます。その後、再び例外が発生し、再びキューに再登録され、無限ループが発生し、mq のメッセージ処理が発生します。急上昇し、不必要な圧力をもたらします。
ローカルでの再試行
- Spring の再試行メカニズムを使用して、コンシューマが異常な場合に mq キューに無制限に再キューするのではなく、ローカルな再試行を使用します。
- コンシューマ サービスの application.yml ファイルを変更し、コンテンツを追加します。
spring: rabbitmq: listener: simple: retry: enabled: true # 开启消费者失败重试 initial-interval: 1000 # 初识的失败等待时长为1秒 multiplier: 1 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval max-attempts: 3 # 最大重试次数 stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
- コンシューマ サービスを再起動し、テストを繰り返します。
- SpringAMQP は例外 AmqpRejectAndDontRequeueException をスローします
- RabbitMQ コンソールを確認すると、メッセージが削除されていることがわかります。これは、最後の SpringAMQP が ack を返し、mq がメッセージを削除していることを示しています。
結論は:
- ローカル再試行が有効な場合、メッセージ処理中に例外がスローされた場合、その例外はキューに要求されませんが、コンシューマーによってローカルで再試行されます。
- 最大再試行回数に達すると、Spring は ack を返し、メッセージは破棄されます。
失敗戦略
再試行モードをオンにすると、再試行回数が制限されます。メッセージがまだ失敗する場合は、それを処理するために MessageRecoverer インターフェイスが必要です。これには、次の 3 つの異なる実装が含まれています。
- RejectAndDontRequeueRecoverer: 再試行が完了した後、メッセージを直接拒否して破棄します。これがデフォルトです
- ImmediateRequeueMessageRecoverer: 再試行が完了すると、nack が返され、メッセージが再度キューに入れられます。
- RepublishMessageRecoverer: 再試行が完了した後、指定された交換に失敗メッセージを配信します。
-
コンシューマ サービスで失敗したメッセージを処理するための交換とキューを定義する
@Bean public DirectExchange errorMessageExchange(){ return new DirectExchange("error.direct"); } @Bean public Queue errorQueue(){ return new Queue("error.queue", true); } @Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error"); }
-
RepublishMessageRecoverer を定義し、キューとスイッチを関連付ける
@Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); }
- 完全なコード:
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.amqp.rabbit.core.RabbitTemplate; import org.springframework.amqp.rabbit.retry.MessageRecoverer; import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer; import org.springframework.context.annotation.Bean; @Configuration public class ErrorMessageConfig { @Bean public DirectExchange errorMessageExchange(){ return new DirectExchange("error.direct"); } @Bean public Queue errorQueue(){ return new Queue("error.queue", true); } @Bean public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){ return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error"); } @Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){ return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error"); } }
- テストを再開します。
- プロジェクトを再起動し、以前のシミュレートされた例外を使用してテストすると、エラー キュー内の特定の例外情報を確認できます。
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueue(String msg) {
System.out.println("消费者接收到simple.queue的消息:【" + msg + "】");
//模拟异常
System.out.println(1/0);
log.debug("消息处理完毕!!!");
}
要約する
RabbitMQ メッセージの信頼性を確保するにはどうすればよいですか?
- プロデューサー確認メカニズムを有効にして、プロデューサーのメッセージがキューに到達できることを確認します。
- 永続化機能を有効にして、メッセージが消費される前にキュー内で失われないようにします。
- コンシューマ確認メカニズムを自動としてオンにすると、スプリングはメッセージが正常に処理されたことを確認した後に確認を完了します。
- コンシューマ障害の再試行メカニズムを有効にし、MessageRecoverer を設定します。複数回の再試行が失敗すると、メッセージは異常なスイッチに配信され、手動処理に引き渡されます。
デッドレター交換
デッドレタースイッチについて知る
キュー内のメッセージが次のいずれかの条件を満たす場合、デッドレターになる可能性があります。
- コンシューマは、basic.reject または Basic.nack を使用して消費の失敗を宣言し、メッセージの requeue パラメータが false に設定されます。
- メッセージは期限切れのメッセージです。タイムアウト後は誰も消費しません
- 配信するキューメッセージがいっぱいのため配信できません
デッド レターを含むキューがdead-letter-exchange
属性で構成され、スイッチが指定されている場合、キュー内のデッド レターはこのスイッチに配信され、このスイッチはデッド レター交換(Dead Letter Exchange、DLX を確認) と呼ばれます。
キューがデッドレターをデッドレター交換局に配信するとき、次の 2 つの情報を知っている必要があります。
- デッドレタースイッチ名
- デッドレター交換およびデッドレターキューにバインドされた RoutingKey
この方法によってのみ、配信されたメッセージがデッド レター交換に到達し、デッド レター キューに正しくルーティングされることが保証されます。
デッドレタースイッチ受信デッドレターデモ
- デッドレター スイッチを simple.queue に追加し、キューをデッドレター スイッチにバインドします。このようにして、メッセージはデッド レターになった後も破棄されず、最終的にデッド レター エクスチェンジに配信され、デッド レター エクスチェンジにバインドされたキューにルーティングされます。
- コンシューマ サービスで、デッド レター スイッチとデッド レター キューのセットを定義します。
// 声明普通的 simple.queue队列,并且为其指定死信交换机:dl.direct
@Bean
public Queue simpleQueue2(){
return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
.deadLetterExchange("dl.direct") // 指定死信交换机
.build();
}
// 声明死信交换机 dl.direct
@Bean
public DirectExchange dlExchange(){
return new DirectExchange("dl.direct", true, false);
}
// 声明存储死信的队列 dl.queue
@Bean
public Queue dlQueue(){
return new Queue("dl.queue", true);
}
// 将死信队列 与 死信交换机绑定
@Bean
public Binding dlBinding(){
return BindingBuilder.bind(dlQueue()).to(dlExchange()).with("simple");
}
TTL(生存時間)
キュー内のメッセージがタイムアウト後に消費されない場合、そのメッセージはデッドレターになります。タイムアウトには 2 つのケースがあります。
- メッセージが配置されているキューにはタイムアウトが設定されています
- メッセージ自体がタイムアウトを設定します
- コンシューマサービスのSpringRabbitListenerで、新しいコンシューマを定義し、デッドレタースイッチとデッドレターキューを宣言します
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "dl.ttl.queue", durable = "true"), exchange = @Exchange(name = "dl.ttl.direct"), key = "ttl" )) public void listenDlQueue(String msg){ log.info("接收到 dl.ttl.queue的延迟消息:{}", msg); }
- キューを宣言し、TTL を指定してスイッチを宣言し、TTL をスイッチにバインドします。
@Bean public Queue ttlQueue(){ return QueueBuilder.durable("ttl.queue") // 指定队列名称,并持久化 .ttl(10000) // 设置队列的超时时间,10秒 .deadLetterExchange("dl.ttl.direct") // 指定死信交换机 .build(); } @Bean public DirectExchange ttlExchange(){ return new DirectExchange("ttl.direct"); } @Bean public Binding ttlBinding(){ return BindingBuilder.bind(ttlQueue()).to(ttlExchange()).with("ttl"); }
- メッセージを送信しますが、TTL は指定しません
@Test public void testTTLQueue() { // 创建消息 String message = "hello, ttl queue"; // 消息ID,需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 发送消息 rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData); // 记录日志 log.debug("发送消息成功"); }
- 送信メッセージのログ:
- 受信したメッセージのログを表示します。
- キューの TTL 値が 10 秒であるためです。メッセージの送信と受信の時間差がちょうど 10 秒であることがわかります。
- メッセージ送信時にTTLを設定する
@Test public void testTTLMsg() { // 创建消息 Message message = MessageBuilder .withBody("hello, ttl message".getBytes(StandardCharsets.UTF_8)) .setExpiration("5000") .build(); // 消息ID,需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 发送消息 rabbitTemplate.convertAndSend("ttl.direct", "ttl", message, correlationData); log.debug("发送消息成功"); }
- 送信されたメッセージのログを表示します。
15:12:16:804 DEBUG 21600 --- [ main] cn.itcast.mq.spring.SpringAmqpTest : 发送消息成功
- メッセージログの受信:
15:12:21:810 INFO 14572 --- [ntContainer#0-1] c.i.mq.listener.SpringRabbitListener : 接收到 dl.ttl.queue的延迟消息:hello, ttl message
- キューとメッセージの両方に TTL が設定されている場合、期限が切れるとどちらか一方がデッドレターになります。
遅延キュー
- 遅延キュー (遅延キュー) モード: TTL とデッドレター スイッチを組み合わせて使用し、メッセージの送信後にコンシューマーがメッセージの受信を遅らせる効果を実現します。
- 遅延キューの使用例には次のものがあります。
- SMS送信の遅延
- ユーザーが注文し、15分以内に支払いがない場合、注文は自動的にキャンセルされます
- 仕事の会議をスケジュールし、20 分後にすべての参加者に自動的に通知します
遅延キューに対する需要が多いため、RabbitMQ は遅延キュー効果をネイティブにサポートする DelayExchange プラグインを正式にリリースしました。RabbitMQのプラグイン一覧ページを参照
DelayExchangeプラグインをインストールする
- DelayExchange プラグインと RabbitMQ の間のバージョン互換性に注意してください。DelayExchange3.8.9 プラグインは RabbitMQ バージョン 3.8.5 以降に対応します。
- プラグインをアップロードする
- Dockerのインストールを前提としているため、最初にRabbitMQプラグインディレクトリに対応するデータ量を確認する必要があります。
docker volume inspect mq-plugins
- 次に、プラグインをこのディレクトリにアップロードします。
- プラグインをインストールする
- インストールを実行するために MQ コンテナーの内部に入り、コマンドを実行します (コンテナー名に注意してください)
docker exec -it mq1 bash
- コンテナに入ったら、次のコマンドを実行してプラグインを有効にします。
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
- 結果は次のとおりです。
[root@kongyue ~]# docker exec -it mq1 bash root@mq1:/# rabbitmq-plugins enable rabbitmq_delayed_message_exchange Enabling plugins on node rabbit@mq1: rabbitmq_delayed_message_exchange The following plugins have been configured: rabbitmq_delayed_message_exchange rabbitmq_management rabbitmq_management_agent rabbitmq_prometheus rabbitmq_web_dispatch Applying plugin configuration to rabbit@mq1... The following plugins have been enabled: rabbitmq_delayed_message_exchange started 1 plugins. root@mq1:/# exit exit
プラグインを使用する
DelayExchange プラグインの原理は、公式のネイティブ Exchange の機能をアップグレードすることです。
-
DelayExchangeで受信したメッセージをメモリに一時的に保存します(公式Exchangeではメッセージを保存できません)
-
DelayExchange のタイミング、メッセージはタイムアウト後にキューに配信されます
-
RabbitMQ 管理プラットフォームで DelayExchange を宣言する
-
メッセージ送信時にメッセージ遅延時間を指定する必要があります
遅延交換の原理
DelayExchange では、交換が遅延していると宣言される必要があります。lateExchange にメッセージを送信するときのフローは次のとおりです。
- メッセージを受信する
- メッセージに x-delay 属性があるかどうかを判断する
- x-lay 属性がある場合は、それが遅延メッセージであることを意味し、ハード ディスクに永続化され、x-lay 値が遅延時間として読み取られます。
- ルーティングが見つからないという結果をメッセージ送信者に返します
- x 遅延時間が経過した後、指定されたキューにメッセージを再配信します。
DelayExchangeを使用する
-
プラグインの使用も非常に簡単です。スイッチを宣言します。スイッチのタイプは任意で、遅延属性を true に設定し、それをバインドするキューを宣言するだけです。
- アノテーション方法 (推奨):
@RabbitListener(bindings = @QueueBinding( value = @Queue(name = "delay.queue",durable = "true"), exchange = @Exchange(name = "delay.direct",delayed = "true"), key = "delay" )) public void listenDelayedQueue(String msg) { log.info("接收到delay.queue的延迟消息:{}"+msg); }
- @Bean の方法に基づいて:
-
メッセージを送ります
- メッセージを送信するときは、遅延時間を指定するために x-delay 属性を必ず指定してください。
@Test public void testDelayedMsg() { // 创建消息 Message message = MessageBuilder .withBody("hello, delay message".getBytes(StandardCharsets.UTF_8)) .setHeader("x-delay",10000) .build(); // 消息ID,需要封装到CorrelationData中 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); // 发送消息 rabbitTemplate.convertAndSend("delay.direct", "delay", message, correlationData); log.debug("发送消息成功"); }
- スイッチは情報をハードディスクに保存し、ルーティングが見つからないという結果をメッセージ送信者に返すため、次のエラー メッセージが表示されます。
15:24:47:054 DEBUG 6124 --- [ main] cn.itcast.mq.spring.SpringAmqpTest : 发送消息成功 15:24:47:056 INFO 6124 --- [nectionFactory1] cn.itcast.mq.config.CommonConfig : 消息发送失败,应答码312,原因NO_ROUTE,交换机delay.direct,路由键delay, 消息(Body:'[B@18f05d55(byte[18])' MessageProperties [headers={ spring_returned_message_correlation=aac9ebb8-f8eb-4c39-a64b-c1bf35b0c639}, contentType=application/octet-stream, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, receivedDelay=10000, deliveryTag=0])
-
エラーの問題を解決する
import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Configuration; @Slf4j @Configuration public class CommonConfig implements ApplicationContextAware { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { // 获取RabbitTemplate RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class); // 设置ReturnCallback rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> { //判断是否有延迟消息 if(message.getMessageProperties().getReceivedDelay()>0) { //判断是一个延迟消息,忽视错误 return; } // 投递失败,记录日志 log.info("消息发送失败,应答码{},原因{},交换机{},路由键{},消息{}", replyCode, replyText, exchange, routingKey, message.toString()); // 如果有业务需要,可以重发消息 }); } }
怠惰なキュー
メッセージの蓄積の問題
-
プロデューサがメッセージを送信する速度がコンシューマがメッセージを処理できる速度を超えると、キューにメッセージが上限まで格納されるまで、キュー内のメッセージが蓄積されます。後から送信されたメッセージはデッドレターとなり破棄される可能性がありますが、これがメッセージの蓄積の問題です。
-
メッセージの蓄積を解決するには 3 つの方法があります。
- 消費者を追加して消費速度を向上させる
- コンシューマでスレッド プールを開き、メッセージ処理を高速化します。
- キューのボリュームを拡張し、スタッキング制限を増やします
怠惰なキュー
RabbitMQ のバージョン 3.6.0 から、Lazy Queues、つまり遅延キューの概念が追加されました。遅延キューの特徴は次のとおりです。
- メッセージを受信したら、メモリではなくディスクに直接保存します
- コンシューマは、メッセージを消費する場合にのみディスクから読み取り、メモリにロードします。
- 数百万のメッセージストレージをサポート
コマンドラインに基づいて遅延キューを設定する[理解]
キューを遅延キューとして設定するには、キューを宣言するときに x-queue-mode 属性を遅延として指定するだけです。コマンドラインを使用して、実行中のキューを遅延キューに変更できます。
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
コマンドの解釈:
rabbitmqctl
: RabbitMQ 用のコマンドライン ツールset_policy
: 戦略を追加しますLazy
: カスタマイズ可能なポリシー名"^lazy-queue$"
: キューの名前を正規表現で照合します。'{"queue-mode":"lazy"}'
: キューモードを遅延モードに設定します--apply-to queues
: ポリシーの対象はすべてのキューです
@Beanに基づいて遅延キューを宣言する
@Bean
public Queue lazyQueue() {
return QueueBuilder.durable("lazy.queue")
.lazy()
.build();
}
@RabbitListener に基づいて LazyQueue を宣言する
@RabbitListener(queuesToDeclare = cQueue(
name = "lazy.queue",
arguments = @Argument(name = "x-queue-mode" ,value = "lazy")
))
public void listenLazyQueue(string msg){
log.info("接收到lazy.queue的消息:{",msg);
}
練習
- 新しい LazyConfig クラスを作成する
import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class LazyConfig { @Bean public Queue lazyQueue() { return QueueBuilder.durable("lazy.queue") .lazy() .build(); } @Bean public Queue normalQueue() { return QueueBuilder.durable("normal.queue").build(); } }
-
ここではスイッチは明示的に指定されていないため、Spring AMQP のデフォルト スイッチがデフォルトで使用されます。
名前 タイプ 特徴 メッセージレート メッセージレートアウト (AMQP のデフォルト) 直接 D 0.00/秒 0.00/秒 -
デフォルトの交換は、キュー名と同じルーティング キーを使用して各キューに暗黙的にバインドされます。デフォルトの交換に明示的にバインドしたり、デフォルトの交換からバインドを解除したりすることはできません。削除することもできません。
@Test
public void testLazyQueue() {
for (int i = 0; i < 100_000; i++) {
Message message = MessageBuilder.withBody("hello lazyQueue".getBytes(StandardCharsets.UTF_8))
.build();
rabbitTemplate.convertAndSend("lazy.queue",message);
}
}
MQクラスター
クラスターの分類
RabbitMQ は Erlang 言語に基づいて書かれており、Erlang はクラスター モードを当然サポートする並行性指向の言語です。RabbitMQ クラスターには 2 つのモードがあります。
-
通常クラスター: クラスターの各ノードにキューを分散する分散クラスターであり、クラスター全体の同時実行能力が向上します。通常モードのクラスターはデータ同期を実行せず、各 MQ には独自のキューとデータ情報があります (スイッチなどの他のメタデータ情報は同期されます)。たとえば、mq1 と mq2 という 2 つの MQ があるとします。メッセージが mq1 にあり、mq2 に接続されている場合、mq2 は mq1 に移動してメッセージをプルし、それを返します。mq1 がダウンすると、メッセージは失われます。
-
ミラークラスタ: 通常のクラスタをベースにマスタ・スレーブバックアップ機能を追加し、クラスタのデータ可用性を向上させたマスタ・スレーブクラスタです。通常モードとは異なり、各mqのミラーノード間でキューが同期されるため、どのミラーノードに接続してもメッセージを取得できます。また、ノードがダウンしてもデータは失われません。ただし、このアプローチではデータ同期のための帯域幅の消費が増加します。
ミラークラスタはマスター/スレーブをサポートしていますが、マスター/スレーブの同期は強力な一貫性ではなく、場合によってはデータ損失のリスクがある可能性があります。したがって、RabbitMQ バージョン 3.8 以降では、ミラー クラスターを置き換えるアービトレーション キューという新しい機能が導入され、最下層は Raft プロトコルを使用してマスターとスレーブ間のデータの一貫性を確保します。
通常のクラスター
クラスターの構造と特徴
通常のクラスター、つまりクラシック クラスターには次の特徴があります。
- スイッチとキューのメタデータを含むデータの一部は、クラスター内の各ノード間で共有されます。キュー内のメッセージは含まれません。
- クラスター内のノードにアクセスするときに、キューがノード上にない場合は、データが存在するノードから現在のノードにキューが渡されて返されます。
- キューが配置されているノードがダウンすると、キュー内のメッセージが失われます。
共通のクラスター展開
- 3 ノードの MQ クラスターのデプロイを計画する
CPU名 | コンソールポート | amqp通信ポート |
---|---|---|
mqNode1 | 8081 —> 15672 | 8071 —> 5672 |
mqNode2 | 8082 —> 15672 | 8072 —> 5672 |
mqNode3 | 8083 —> 15672 | 8073 —> 5672 |
- クラスター内のノードのデフォルトのラベルは次のとおりです。
rabbit@[hostname]
したがって、上記の 3 つのノードの名前は次のようになります。- うさぎ@mqNode1
- うさぎ@mqNode2
- うさぎ@mqNode3
クッキーを取得する
-
RabbitMQ の最下層は Erlang に依存しており、Erlang 仮想マシンはデフォルトでクラスター モードをサポートする分散指向言語です。クラスター モードの各 RabbitMQ ノードは、Cookie を使用して相互通信が許可されているかどうかを判断します。
-
2 つのノードが通信できるようにするには、 Erlang Cookieと呼ばれる同じ共有シークレットを持っている必要があります。Cookie は、最大 255 文字の英数字の文字列です。
-
すべてのクラスター ノードには同じ Cookieが必要です。インスタンス間で相互に通信するためにも必要です。
-
まず、以前に起動した mq コンテナ内の cookie 値をクラスター cookie として取得します。次のコマンドを実行します。
docker exec -it mq cat /var/lib/rabbitmq/.erlang.cookie
-
Cookieの値は次のとおりです。
FXZMCVGLBIXZCDEMMVZQ
-
次に、現在の mq コンテナを停止して削除し、クラスターを再構築します。
docker rm -f mq
クラスター構成の準備
echo FXZMCVGLBIXZCDEMMVZQ> .erlang.cookie
- /tmp/rabbitMqCluster ディレクトリに新しい構成ファイル Rabbitmq.conf を作成します。
cd /tmp/rabbitMqCluster touch rabbitmq.conf
- ファイルの内容は次のとおりです。
loopback_users.guest = false listeners.tcp.default = 5672 cluster_formation.peer_discovery_backend = rabbit_peer_discovery_classic_config cluster_formation.classic_config.nodes.1 = rabbit@mqNode1 cluster_formation.classic_config.nodes.2 = rabbit@mqNode2 cluster_formation.classic_config.nodes.3 = rabbit@mqNode3
- Cookieを記録するために別のファイルを作成する
cd /tmp/rabbitMqCluster # 创建cookie文件 touch .erlang.cookie # 写入cookie 请使用自己生成的cookie echo "FXZMCVGLBIXZCDEMMVZQ" > .erlang.cookie # 修改cookie文件的权限 chmod 600 .erlang.cookie
- 3 つのディレクトリ mqNode1、mqNode2、mqNode3 を準備します。
cd /tmp/rabbitMqCluster # 创建目录 mkdir mqNode1 mqNode2 mqNode3
- 次に、rabbitmq.confをコピーします
[root@kongyue rabbitMqCluster]# mkdir mqNode1 mqNode2 mqNode3 [root@kongyue rabbitMqCluster]# cp .erlang.cookie mqNode1 [root@kongyue rabbitMqCluster]# cp .erlang.cookie mqNode2 [root@kongyue rabbitMqCluster]# cp .erlang.cookie mqNode3 [root@kongyue rabbitMqCluster]# cp rabbitmq.conf mqNode1 [root@kongyue rabbitMqCluster]# cp rabbitmq.conf mqNode2 [root@kongyue rabbitMqCluster]# cp rabbitmq.conf mqNode3
クラスターを開始する
- ネットワークを作成します。
docker network create mq-net
- コマンドを実行する
docker run -d --net mq-net \ -v ${ PWD}/mqNode1/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \ -v ${ PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \ -v mq-plugins:/plugins \ -e RABBITMQ_DEFAULT_USER=itcast \ -e RABBITMQ_DEFAULT_PASS=123321 \ --name mqNode1 \ --hostname mqNode1 \ -p 8071:5672 \ -p 8081:15672 \ rabbitmq:3.8-management
docker run -d --net mq-net \ -v ${ PWD}/mqNode2/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \ -v ${ PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \ -v mq-plugins:/plugins \ -e RABBITMQ_DEFAULT_USER=itcast \ -e RABBITMQ_DEFAULT_PASS=123321 \ --name mqNode2 \ --hostname mqNode2 \ -p 8072:5672 \ -p 8082:15672 \ rabbitmq:3.8-management
docker run -d --net mq-net \ -v ${ PWD}/mqNode3/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf \ -v ${ PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \ -v mq-plugins:/plugins \ -e RABBITMQ_DEFAULT_USER=itcast \ -e RABBITMQ_DEFAULT_PASS=123321 \ --name mqNode3 \ --hostname mqNode3 \ -p 8073:5672 \ -p 8083:15672 \ rabbitmq:3.8-management
テスト
- 任意のノードにログインし、ノードを表示します
- mq1 ノードにキューを追加します。
- これは、mq2 コンソールと mq3 コンソールの両方でも表示されます。
データ共有テスト
- コンソールを使用してこのキューにメッセージを送信します
ミラーモード
クラスターの構造と特徴
ミラー クラスター: 本質はマスター/スレーブ モードであり、次の特性があります。
- スイッチ、キュー、およびキュー内のメッセージは、各 mq のミラー ノード間で同期的にバックアップされます。
- キューを作成するノードはキューのプライマリ ノードと呼ばれ、バックアップ先の他のノードはキューのミラーノードと呼ばれます。
- キューのマスター ノードが別のキューのミラー ノードになる場合があります
- すべての操作はマスター ノードによって完了し、ミラー ノードに同期されます。
- マスターがダウンすると、ミラー ノードは新しいマスターに置き換えられます。
要約:
- ミラーキュー構造は1つのマスターと複数のスレーブ(スレーブはミラーイメージ)です。
- すべての操作はマスター ノードによって完了し、ミラー ノードに同期されます。
- マスターがダウンすると、ミラー ノードが新しいマスターとして置き換えられます (マスターとスレーブの同期が完了する前にマスターがダウンすると、データ損失が発生する可能性があります)
- すべての操作がマスター ノードによって完了されるため、ロード バランシング機能はありません (ただし、キューが異なるとマスター ノードも異なる可能性があり、これを使用してスループットを向上させることができます)
ミラーモードの構成
- ミラー モード構成には 3 つのモードがあります。
ハモード | ハパラメータ | 効果 |
---|---|---|
正確なモード | キューカウントのコピー数 | クラスター内のキュー レプリカの数 (プライマリ サーバーとミラー サーバーの合計)。count が 1 の場合、単一のコピー、つまりキュー マスター ノードを意味します。カウント値 2 は、2 つのコピー (1 つのキュー マスターと 1 つのキュー ミラー) を意味します。言い換えると、カウント = ミラーの数 + 1 となります。クラスター内のノードが count よりも少ない場合、キューはすべてのノードにミラーリングされます。クラスターの合計が count+1 より大きい場合、ミラーを含むノードに障害が発生すると、別のノードに新しいミラーが作成されます。 |
全て | (なし) | キューはクラスター内のすべてのノードにわたってミラーリングされます。キューは新しく参加したノードにミラーリングされます。すべてのノードにミラーリングすると、ネットワーク I/O、ディスク I/O、ディスク領域の使用量など、すべてのクラスター ノードにさらなる負荷がかかります。正確に使用し、レプリカの数を (N / 2 +1) に設定することをお勧めします。 |
ノード | ノード名 | キューを作成するノードを指定します。指定したノードが存在しない場合、例外が発生します。指定したノードがクラスター内に存在するものの、一時的に使用できない場合は、現在のクライアントが接続しているノードにノードが作成されます。 |
- 構成構文を説明するために、rabbitmqctl コマンドを例として取り上げます。
- まさにモード
rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
rabbitmqctl set_policy
: 文言を修正ha-two
: ポリシー名、カスタム"^two\."
: キューの正規表現と一致します。命名規則に準拠したキューが有効になります。ここには、でtwo.
始まるキュー名が入ります。'{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
: ポリシーの内容"ha-mode":"exactly"
: 戦略モード、ここはまさにモード、コピー数を指定します"ha-params":2
: ポリシーパラメータ。ここでは 2 です。つまり、レプリカの数は 2、マスター 1 つ、ミラー 1 つです。"ha-sync-mode":"automatic"
: 同期戦略。デフォルトは手動です。つまり、新しく追加されたミラー ノードは古いメッセージを同期しません。自動に設定されている場合、新しく追加されたミラー ノードはマスター ノード内のすべてのメッセージを同期するため、追加のネットワーク オーバーヘッドが発生します。
- オールモード
rabbitmqctl set_policy ha-all "^all\." '{"ha-mode":"all"}'
ha-all
: ポリシー名、カスタム"^all\."
:でall.
始まるすべてのキュー名に一致します。'{"ha-mode":"all"}'
: ポリシーの内容"ha-mode":"all"
: ストラテジー モード、ここではすべてのモードです。つまり、すべてのノードがミラー ノードと呼ばれます。
- ノードモード
rabbitmqctl set_policy ha-nodes "^nodes\." '{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
rabbitmqctl set_policy
: 文言を修正ha-nodes
: ポリシー名、カスタム"^nodes\."
: キューの正規表現と一致します。命名規則に準拠したキューが有効になります。ここには、でnodes.
始まるキュー名が入ります。'{"ha-mode":"nodes","ha-params":["rabbit@nodeA", "rabbit@nodeB"]}'
: ポリシーの内容"ha-mode":"nodes"
: 戦略モード、ここはノードモードです"ha-params":["rabbit@mq1", "rabbit@mq2"]
: ポリシーパラメータ。ここではコピーが配置されているノードの名前を指定します。
正確にテストモード
- Exact モードでミラーリングを使用します。クラスター ノードの数が 3 であるため、ミラーリングの数は 2 に設定され、次のコマンドを実行します。
docker exec -it mqNode1 rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
[root@kongyue rabbitMqCluster]# docker exec -it mqNode1 rabbitmqctl set_policy ha-two "^two\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
Setting policy "ha-two" for pattern "^two\." to "{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}" with priority "0" for vhost "/" ...
- 新しいキューを作成します。
- 任意の MQ コンソールでキューを確認します。
テストデータの共有
- 给two.queue发送一条消息:
- 次に、mq1、mq2、mq3 のいずれかのコンソールでメッセージを確認します。
高可用性をテストする
- two.queue のマスター ノード mq1 をダウンさせます。
docker stop mqNode1
- クラスターのステータスを表示します。
- キューのステータスを表示します。
調停キュー
クラスターの特性
アービトレーション キュー: アービトレーション キューは、バージョン 3.8 以降で利用できる新しい機能であり、ミラー キューを置き換えるために使用され、次の特徴があります。
- ミラーキューと同様に、マスター/スレーブモードであり、マスター/スレーブデータ同期をサポートします。
- 非常に使いやすく、複雑な設定は必要ありません
- Raft プロトコルに基づくマスター/スレーブ同期、強力な一貫性
デプロイメント: クォーラム キューの追加
- 注: 各 RabbitMQ コンテナに入り、遅延 DelayExchange プラグインを開きます
- コンソールにキューを追加するには、必ずキュー タイプをクォーラム タイプとして選択してください。
- 任意のコンソールでキューを表示します。
- クォーラム キューの +2 ワード。これは、このキューには 2 つのミラー ノードがあることを意味します。
- クォーラム キューのデフォルトのミラー番号は 5 であるためです。クラスターに 7 つのノードがある場合、ミラーの数は 5 でなければなりませんが、現在クラスターには 3 つのノードしかないため、ミラーの数は 3 です。
- 各ノードのキューの詳細を表示する
クォーラムキューを作成する Java コード
@Bean
public Queue quorumQueue() {
return QueueBuilder
.durable("quorum.queue") // 持久化
.quorum() // 仲裁队列
.build();
}
SpringAMQP が MQ クラスターに接続する
spring:
rabbitmq:
addresses: 192.168.188.112:8071, 192.168.188.112:8072, 192.168.188.112:8073
username: itcast
password: 123321
virtual-host: /
- プロジェクトを再起動し、キューを確認します。
クラスターの拡大
クラスターに参加する
- 新しい MQ コンテナを開始します。
docker run -d --net mq-net \ -v ${PWD}/.erlang.cookie:/var/lib/rabbitmq/.erlang.cookie \ -e RABBITMQ_DEFAULT_USER=itcast \ -e RABBITMQ_DEFAULT_PASS=123321 \ --name mqNode4 \ --hostname mqNode4 \ -p 8074:15672 \ -p 8084:15672 \ rabbitmq:3.8-management
- コンテナコンソールに入ります。
docker exec -it mqNode4 bash
- MQプロセスを停止します
rabbitmqctl stop_app
- RabbitMQ でデータをリセットします。
rabbitmqctl reset
- mqNode1 に参加します:
rabbitmqctl join_cluster rabbit@mqNode1
- mqプロセスを再度開始します
rabbitmqctl start_app
[root@kongyue rabbitMqCluster]# docker exec -it mqNode4 bash
root@mqNode4:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@mqNode4 ...
root@mqNode4:/# rabbitmqctl reset
Resetting node rabbit@mqNode4 ...
root@mqNode4:/# rabbitmqctl join_cluster rabbit@mqNode1
Clustering node rabbit@mqNode4 with rabbit@mqNode1
root@mqNode4:/# rabbitmqctl start_app
Starting node rabbit@mqNode4 ...
- 結果:
クォーラムキューのコピーを追加
- まず、quorum.queue キューの現在のコピーを確認し、mqNode1 コンテナに入り、次のコマンドを実行します。
[root@kongyue rabbitMqCluster]# docker exec -it mqNode1 bash
root@mqNode1:/# rabbitmq-queues quorum_status "quorum.queue"
Status of quorum queue quorum.queue on node rabbit@mqNode1 ...
┌────────────────┬────────────┬───────────┬──────────────┬────────────────┬──────┬─────────────────┐
│ Node Name │ Raft State │ Log Index │ Commit Index │ Snapshot Index │ Term │ Machine Version │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode3 │ follower │ 2 │ 2 │ undefined │ 1 │ 1 │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode2 │ leader │ 2 │ 2 │ undefined │ 1 │ 1 │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode1 │ follower │ 2 │ 2 │ undefined │ 1 │ 1 │
└────────────────┴────────────┴───────────┴──────────────┴────────────────┴──────┴─────────────────┘
- mqNode4 に参加します。
root@mqNode1:/# rabbitmq-queues add_member "quorum.queue" "rabbit@mqNode4"
Adding a replica for queue quorum.queue on node rabbit@mqNode4...
root@mqNode1:/# rabbitmq-queues quorum_status "quorum.queue"
Status of quorum queue quorum.queue on node rabbit@mqNode1 ...
┌────────────────┬────────────┬───────────┬──────────────┬────────────────┬──────┬─────────────────┐
│ Node Name │ Raft State │ Log Index │ Commit Index │ Snapshot Index │ Term │ Machine Version │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode4 │ follower │ 3 │ 3 │ undefined │ 1 │ 1 │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode3 │ follower │ 3 │ 3 │ undefined │ 1 │ 1 │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode2 │ leader │ 3 │ 3 │ undefined │ 1 │ 1 │
├────────────────┼────────────┼───────────┼──────────────┼────────────────┼──────┼─────────────────┤
│ rabbit@mqNode1 │ follower │ 3 │ 3 │ undefined │ 1 │ 1 │
└────────────────┴────────────┴───────────┴──────────────┴────────────────┴──────┴─────────────────┘
- コンソールを見ると、quorum.queue のミラー数も +2 から +3 に変更されています。