springboot プロジェクトを手動で構築する 06-springboot は RabbitMQ とその原理およびアプリケーション シナリオを統合します


序文

RabbitMQ は、 AMQP (Advanved Message Queue Protocol) 高度なメッセージ キュー プロトコルを実装する、erlang 言語によって開発されたメッセージ サービス ミドルウェアです。

ワークフロー - ソウルペインター

作業過程
1. プロデューサー (Producer) とコンシューマー (Consumer) の両方は、メッセージを送受信する前に、RabbitMQ との長い接続 (Connection) を確立する必要があります
。長い接続の場合、メッセージを送受信するために長い接続でチャネルを開きます。
3. プロデューサがメッセージを送信し、メッセージはブローカーの指定された仮想ホスト (サービス) の指定されたスイッチ (送信メッセージは指定されます) に到着します。ルーティング キーとスイッチのキューとのバインド関係に従って、メッセージを対応するキューに送信します。
3、コンシューマーはチャネルを通じてキューをリッスンし、コンシューマーは実際にメッセージを取得できます。列に入ってからの時間

用語集

1.ブローカー(メッセージ ブローカー) メッセージ ブローカー: メッセージ キュー サーバー エンティティ。単純にポスト オフィスとして理解され、メールが送受信されます。
2. JMS (Java Message Service) JAVA メッセージ サービス。JVMメッセージブローカーをベースとした仕様です。ActiveMQおよびHornetMQは JMS 実装です。
3. AMQP (Advanced Message Queuing Protocol)
高度なメッセージ キューイング プロトコルもメッセージ ブローカーの仕様であり、JMS と互換性があります。
RabbitMQ はAMQP の実装です。
ここに画像の説明を挿入
4.メッセージメッセージは、メッセージ ヘッダーとメッセージ本文で構成されます。メッセージ本文は不透明ですが、メッセージ ヘッダーは、routing-key (ルーティング キー)、priority (他のメッセージに対する優先順位)、delivery-mode (メッセージが永続的なストレージを必要とする可能性があることを示す) などの一連のオプションの属性で構成されます。等
5.プロデューサーメッセージのプロデューサーは、メッセージを交換に公開するクライアント アプリケーションでもあります。
6. Exchangeスイッチは、プロデューサによって送信されたメッセージを受信し、これらのメッセージをサーバー内のキューにルーティングするために使用されます。
一般的に使用される Exchange には、ダイレクト、ファンアウト、トピックの 3 つのタイプがあり、Exchange のタイプが異なると、メッセージを転送するための戦略も異なります
キューメッセージ キュー。コンシューマーに送信されるまでメッセージを保存するために使用されます。これはメッセージのコンテナーであり、メッセージの宛先です。メッセージは 1 つ以上のキューに入れることができます。メッセージは常にキュー内にあり、コンシューマーがキューに接続してメッセージを取り出すのを待っています。
8.バインディングバインディング。メッセージ キューとスイッチ間の関連付けに使用されます。バインディングは、ルーティング キーに基づいて交換機とメッセージ キューを接続するルーティング ルールであるため、交換機はバインディングで構成されるルーティング テーブルとして理解できます。
Exchange と Queue のバインディングは、多対多の関係になる場合があります。
9.接続ネットワーク接続 (TCP 接続など)。
10.チャネルチャネル。多重接続における独立した双方向データ フロー チャネル。チャネルは、実際の TCP 接続で確立された仮想接続です。AMQP コマンドはチャネルを通じて送信されます。メッセージのパブリッシュ、キューへのサブスクライブ、メッセージの受信など、これらのアクションはすべてチャネルを通じて完了します。オペレーティング システムが TCP を確立および破棄すると非常にコストがかかるため、TCP 接続を再利用するためにチャネルの概念が導入されました。
11.コンシューマとは、メッセージ キューからメッセージを取得するクライアント アプリケーションを意味します。
12.仮想ホスト: スイッチ、メッセージ キュー、および関連オブジェクトのバッチを表します。仮想ホストは、同じ認証および暗号化環境を共有するサーバーの別個のドメインです。各 vhost は基本的に、独自のキュー、スイッチ、バインディング、および許可メカニズムを備えた RabbitMQ サーバーのミニバージョンです。vhost は AMQP の概念の基礎であり、接続時に指定する必要があります。RabbitMQ のデフォルトの vhost は / です。

スイッチタイプ

ここに画像の説明を挿入


1. インストール

1.1 RabbitMQ公式サイトのインストール

1.2 Dockerのインストールと起動

docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.11-management

# 开机自启
docker update rabbitmq --restart=always

● 5672 (AMQP ポート)
● 15672 (Web 管理バックグラウンド ポート)

ローカル インストールにはhttp://127.0.0.1:15672/からアクセスできます。デフォルトのユーザー名とパスワードは guest です。

2. 食べ物のチュートリアル

2.1. 依存関係のインポート

<!--RabbitMQ-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2.2 構成の追加

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root #用户名 默认guest
    password: root #密码 默认guest
    virtual-host: springboot-test #虚拟主机 默认/

2.3 コードの実装

2.3.1 ダイレクトタイプ

直接接続されたスイッチは、メッセージに含まれるルーティング キーに従って、メッセージを対応するキューに配信します。

直結型初期化設定

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;

/**
 * 1、直连交换机配置
 */
@Configuration
public class DirectRabbitConfig {
    
    

    public static final String DIRECT_QUEUE = "===DirectQueue===";
    public static final String DIRECT_EXCHANGE = "===DirectExchange===";
    public static final String DIRECT_ROUTING = "===DirectRouting===";

    /**
     *      durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
     *      exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
     *      autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
     *      return new Queue("TestDirectQueue",true,true,false);
     */
    @Bean
    public Queue directQueue() {
    
    
        return new Queue(DIRECT_QUEUE,false);
    }

    @Bean
    DirectExchange directExchange() {
    
    
        return new DirectExchange(DIRECT_EXCHANGE,false,false);
    }

    @Bean
    Binding binding() {
    
    
        return BindingBuilder.bind(directQueue())
                .to(directExchange()).with(DIRECT_ROUTING);
    }
}

この構成では主にキュー、スイッチ、バインディングを Spring で管理するため、キュー、スイッチの宣言、バインディング関係の確立を忘れずに行ってください。指定されたスイッチによってメッセージが送信された後、スイッチはルーティング キーに従って、一致するキューにメッセージを送信できます。

消費者

import com.chendi.springboot_rabbitmq.config.DirectRabbitConfig;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;

@Slf4j
@Component
public class DirectReceiver {
    
    

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE)
    public void receiver(String dataMsg) {
    
    
        log.info("接收者A dataMsg:{} ",dataMsg);
    }

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE)
    public void receiver(String dataMsg) {
    
    
        log.info("接收者B dataMsg:{} ",dataMsg);
    }
}

プロデューサー

@RestController
@RequiredArgsConstructor
public class RabbitMQTestController {
    
    

    final RabbitTemplate rabbitTemplate;

    @GetMapping("/sendDirectMessage")
    public String sendDirectMessage() {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            String messageData = "Hello World!" + i;
            //可自定义消息体类型
            rabbitTemplate.convertAndSend(DirectRabbitConfig.DIRECT_EXCHANGE, DirectRabbitConfig.DIRECT_ROUTING, messageData);
        }
        return "发送完成";
    }
}

ディスカバリの実行: デフォルトでは、RabbitMQ ラウンドロビン配布は各メッセージを次のコンシューマに順番に送信します。
1. メッセージが消費されたという保証はありません2.
メッセージを迅速に処理するサービスは、メッセージを処理するのが遅いサービスと同じくらい多くのメッセージを取得します (公平な配布、可能な人の作業量が増加します)。

2.3.2 メッセージ手動確認機構の導入

設定ファイル

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual # 设置消费端手动 ack
        #表示消费者端每次从队列拉取多少个消息进行消费,直到手动确认消费完毕后,才会继续拉取下一条
        prefetch: 1 # 预加载消息数量--QOS

消費者の反応

@Slf4j
@Component
public class DirectReceiver {
    
    

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE)
    public void receiver(String dataMsg, Channel channel, Message message) throws IOException, InterruptedException {
    
    
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        Thread.sleep(1000);
        log.info("接收者A deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
        channel.basicAck(deliveryTag,true);
    }

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE)
    public void receiver2(String dataMsg, Channel channel, Message message) throws IOException {
    
    
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        log.info("接收者B deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
        channel.basicAck(deliveryTag,true);
    }
}

受信メソッド (
1.channel.basicAck は成功の確認を示します。この受信メソッドを使用した後、メッセージは Rabbitmq ブローカーによって削除されます。2.channel.basicNack は
失敗の確認を示します。通常、このメソッドは消費メッセージ ビジネスが異常な場合に使用されます。 3. channel.basicRejectがメッセージを拒否するかどうかを判断できます
。basicNack との違いは、バッチ操作が実行できないことです。その他の使用法は同様です。

2.3.2 ブロードキャスト (ファンアウト) タイプ

ファン タイプのスイッチ。このスイッチにはルーティング キーの概念がありません。このスイッチは、メッセージを受信した後、それにバインドされているすべてのキューに直接転送します。

ブロードキャストタイプの設定クラス

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;
/**
 * 2、广播、扇出交换机
 */
@Configuration
public class FanoutRabbitConfig {
    
    
    public final static String FANOUT_EXCHANGE = "fanoutExchange";
    public static final String FANOUT_QUEUE_A = "fanoutQueueA";
    public static final String FANOUT_QUEUE_B = "fanoutQueueB";
    public static final String FANOUT_QUEUE_C = "fanoutQueueC";
    /**
     *  创建三个队列
     *  将三个队列都绑定在交换机 fanoutExchange 上
     *  因为是扇型交换机, 路由键无需配置,配置也不起作用
     */
    @Bean
    public Queue queueA() {
    
    
        return new Queue(FANOUT_QUEUE_A);
    }
    @Bean
    public Queue queueB() {
    
    
        return new Queue(FANOUT_QUEUE_B);
    }
    @Bean
    public Queue queueC() {
    
    
        return new Queue(FANOUT_QUEUE_C);
    }
    @Bean
    FanoutExchange fanoutExchange() {
    
    
        return new FanoutExchange(FANOUT_EXCHANGE);
    }
    @Bean
    Binding bindingExchangeA() {
    
    
        return BindingBuilder.bind(queueA()).to(fanoutExchange());
    }
    @Bean
    Binding bindingExchangeB() {
    
    
        return BindingBuilder.bind(queueB()).to(fanoutExchange());
    }
    @Bean
    Binding bindingExchangeC() {
    
    
        return BindingBuilder.bind(queueC()).to(fanoutExchange());
    }
}

消費者

import com.chendi.springboot_rabbitmq.config.FanoutRabbitConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

//如果开启了消息手动确认机制,一定要记得应答消息噢
//不然消息会一直堆积在mq里
@Slf4j
@Component
public class FanoutReceiver {
    
    
    @RabbitListener(queues = FanoutRabbitConfig.FANOUT_QUEUE_A)
    public void fanout_A(String message) {
    
    
        log.info("fanout_A  {}" , message);
    }

    @RabbitListener(queues = FanoutRabbitConfig.FANOUT_QUEUE_B)
    public void fanout_B(String message) {
    
    
        log.info("fanout_B  {}" , message);
    }

    @RabbitListener(queues = FanoutRabbitConfig.FANOUT_QUEUE_C)
    public void fanout_C(String message) {
    
    
        log.info("fanout_C  {}" , message);
    }
}

テストプロデューサーコントローラープラス

@GetMapping("/sendFanoutMessage")
public String sendFanoutMessage() {
    
    
    String messageData = "这是一条广播消息";
    rabbitTemplate.convertAndSend(FanoutRabbitConfig.FANOUT_EXCHANGE, "", messageData);
    return "发送完成";
}

2.3.3 トピックの種類

トピック交換の特徴は、ルーティング キーとバインディング キーの間にルールがあることです。

「*」 (アスタリスク) は単語を示すために使用されます (必須)。
「#」 (ポンド記号) は任意の数 (0 個以上) の単語を示すのに使用されます。
トピック スイッチがルーティング キーにバインドされていない場合、トピック スイッチはルーティング キーにバインドされていません。はダイレクトスイッチで、「#」記号で括ると扇形のスイッチになります。

テーマモード設定クラス

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;
/**
 * 主题交换机
 * 转发规则:
 * #:匹配一个或者多个词
 * *:匹配一个或者0个词
 * 比如 有msg.# 、msg.* 匹配规则
 * msg.# 会匹配 msg.email、msg.email.b、msg.email.a
 * msg.* 只会匹配 msg.email 和 msg ,
 */
@Configuration
public class TopicRabbitConfig {
    
    
    //绑定键
    public final static String MSG_EMAIL = "msg.email";
    public final static String MSG_EMAIL_A = "msg.email.a";
    public final static String MSG_SMS = "msg.sms";
    public final static String TOPIC_EXCHANGE = "topicExchange";
    @Bean
    public Queue firstQueue() {
    
    
        return new Queue(TopicRabbitConfig.MSG_EMAIL);
    }
    @Bean
    public Queue secondQueue() {
    
    
        return new Queue(TopicRabbitConfig.MSG_EMAIL_A);
    }
    @Bean
    public Queue thirdQueue() {
    
    
        return new Queue(TopicRabbitConfig.MSG_SMS);
    }
    @Bean
    TopicExchange exchange() {
    
    
        return new TopicExchange(TOPIC_EXCHANGE);
    }
    @Bean
    Binding bindingExchangeMessage() {
    
    
        return BindingBuilder.bind(firstQueue()).to(exchange()).with(MSG_EMAIL);
    }
    @Bean
    Binding bindingExchangeMessage2() {
    
    
        return BindingBuilder.bind(secondQueue()).to(exchange()).with("msg.#");
    }
    @Bean
    Binding bindingExchangeMessage3() {
    
    
        return BindingBuilder.bind(thirdQueue()).to(exchange()).with("msg.*");
    }
}

消費者

import com.chendi.springboot_rabbitmq.config.TopicRabbitConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class TopicReceiver {
    
    
    @RabbitListener(queues = TopicRabbitConfig.MSG_EMAIL)
    public void topic_man(String message) {
    
    
        log.info("队列{} 收到消息:{}" ,TopicRabbitConfig.MSG_EMAIL, message);
    }
    @RabbitListener(queues = TopicRabbitConfig.MSG_SMS)
    public void topic_woman(String message) {
    
    
        log.info("队列{} 收到消息:{}" ,TopicRabbitConfig.MSG_SMS, message);
    }
    @RabbitListener(queues = TopicRabbitConfig.MSG_EMAIL_A)
    public void xxx(String message) {
    
    
        log.info("队列{} 收到消息:{}" ,TopicRabbitConfig.MSG_EMAIL_A, message);
    }
}

テストプロデューサーコントローラープラス

@GetMapping("/sendTopicMessage")
public String sendTopicMessage() {
    
    
    rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, TopicRabbitConfig.MSG_EMAIL, "Hello Topic!所有队列都可以收到这条信息");
    rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, TopicRabbitConfig.MSG_EMAIL_A, "只有 msg.email.a可以收到这条信息");
    rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, TopicRabbitConfig.MSG_SMS, "msg.email.a 和 msg.sms可以收到这条信息");
    return "发送完成";
}

メッセージの手動確認メカニズムを有効にした場合は、忘れずにメッセージに応答する必要があります。


以上で統合は完了です。

3. 実際の応用シナリオ

3.1 メッセージの順序を制御する方法

1. メッセージの順序を保証できるコンシューマが 1 つだけであるが、効率が低い場合。
2. プロデューサはメッセージをキューに順番に送信しますが、複数のコンシューマがキューをリッスンすると、配布のためにポーリングが行われるため、結果的に混乱が生じます。コンシューマーが 1 つのキューのみをリッスンし、プロデューサーが配信戦略をカスタマイズするように変更されています。1、2、および 3 は A キューに入れられ、4、5、および 6 は B キューに入れられます (シーケンシャルメッセージは全体です)、キューに入れられます。

3.2 メッセージが繰り返し消費されないようにする (冪等性)

コンシューマが消費した後、通常の状況では、メッセージが消費されたことを証明するためにレシートがメッセージ キューに送信されます。ただし、この時点では、コンシューマ ネットワークの送信障害またはダウンタイムが発生すると、メッセージ キューはメッセージ消費の受信を受信せず、メッセージを他のコンシューマに再配布するため、メッセージが複数回消費されます。
・・・・・・・・・・・・
解決策: (具体的な問題の具体的な分析)
1. redis でセットを維持し、プロデューサーがメッセージを送信する前に、グローバルに一意の ID を追加し、コンシューマーが消費する前に、redis で確認します。消費されているかどうかを確認し、消費されていない場合は実行を続行します。

//生产者
public void sendMessageIde() {
    
    
    MessageProperties properties = new MessageProperties();
    properties.setMessageId(UUID.randomUUID().toString());
    Message message = new Message("消息".getBytes(), properties);
    rabbitTemplate.convertAndSend("exchange", "", message);
}

//消费者
@RabbitListener(queues = "queue")
@RabbitHandler
public void processIde(Message message, Channel channel) throws IOException {
    
    
    if (stringRedisTemplate.opsForValue().setIfAbsent(message.getMessageProperties().getMessageId(),"1")){
    
    
        // 业务操作...
        System.out.println("消费消息:"+ new String(message.getBody(), "UTF-8"));
        // 手动确认   
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
}

3.3 メッセージの信頼性を保証する

メッセージ送信プロセスプロデューサによって送信されたメッセージがコンシューマに正確に到着すると、
メッセージ送信プロセス
2 つの部分に分割されることがわかります。スイッチはメッセージをキューに配信できなかった場合、returnCallbackをコールバックします。

設定ファイル

spring:
  rabbitmq:
    publisher-returns: true # 开启消息抵达队列的确认  
    # 低版本 publisher-confirms: true
    publisher-confirm-type: correlated # 开启发送端确认

構成クラス

/**
 * 常用的三个配置如下
 * 1---设置手动应答(acknowledge-mode: manual)
 * 2---设置生产者消息发送的确认回调机制 (  #这个配置是保证提供者确保消息推送到交换机中,不管成不成功,都会回调
 *     publisher-confirm-type: correlated
 *     #保证交换机能把消息推送到队列中
 *     publisher-returns: true
 *      template:
 *       #以下是rabbitmqTemplate配置
 *       mandatory: true)
 *  3---设置重试
 */
@Slf4j
@Configuration
public class RabbitConfig {
    
    
    @Autowired
    private ConnectionFactory rabbitConnectionFactory;

    // 存在此名字的bean 自带的容器工厂会不加载(yml下rabbitmq下的template的配置),如果想自定义来区分开 需要改变bean 的名称
    @Bean
    public RabbitTemplate rabbitTemplate(){
    
    
        RabbitTemplate rabbitTemplate=new RabbitTemplate(rabbitConnectionFactory);
        //默认是用jdk序列化
        //数据转换为json存入消息队列,方便可视化界面查看消息数据
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数
        rabbitTemplate.setMandatory(true);
        //此处设置重试template后,会再生产者发送消息的时候,调用该template中的调用链
        rabbitTemplate.setRetryTemplate(rabbitRetryTemplate());
        rabbitTemplate.setConfirmCallback(
                (correlationData, ack, cause) -> {
    
    
                    if(!ack){
    
    
                        System.out.println("ConfirmCallback     "+"相关数据:"+  correlationData);
                        System.out.println("ConfirmCallback     "+"确认情况:"+ ack);
                        System.out.println("ConfirmCallback     "+"原因:"+ cause);
                    }
                });
        rabbitTemplate.setReturnsCallback((ReturnedMessage returned) -> {
    
    
            System.out.println("ReturnsCallback:     "+"消息:"+ returned.getMessage());
            System.out.println("ReturnsCallback:     "+"回应码:"+ returned.getReplyCode());
            System.out.println("ReturnsCallback:     "+"回应消息:"+ returned.getReplyText());
            System.out.println("ReturnsCallback:     "+"交换机:"+ returned.getExchange());
            System.out.println("ReturnsCallback:     "+"路由键:"+ returned.getRoutingKey());
        });
        return rabbitTemplate;
    }

    //重试的Template
    @Bean
    public RetryTemplate rabbitRetryTemplate() {
    
    
        RetryTemplate retryTemplate = new RetryTemplate();
        // 设置监听  调用重试处理过程
        retryTemplate.registerListener(new RetryListener() {
    
    
            @Override
            public <T, E extends Throwable> boolean open(RetryContext retryContext, RetryCallback<T, E> retryCallback) {
    
    
                // 执行之前调用 (返回false时会终止执行)
                //log.info("执行之前调用 (返回false时会终止执行)");
                return true;
            }
            @Override
            public <T, E extends Throwable> void close(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
    
    
                // 方法结束的时候调用
                if(retryContext.getRetryCount() > 0){
    
    
                    log.info("最后一次调用");
                }
            }
            @Override
            public <T, E extends Throwable> void onError(RetryContext retryContext, RetryCallback<T, E> retryCallback, Throwable throwable) {
    
    
                // 方法异常时会调用
                log.info("第{}次调用", retryContext.getRetryCount());
            }
        });
        return retryTemplate;
    }
}

送信者テスト

import com.chendi.springboot_rabbitmq.config.DirectRabbitConfig;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;

@RestController
public class SendCallbackMessageController {
    
    

    @Autowired
    RabbitTemplate rabbitTemplate;  //使用RabbitTemplate,这提供了接收/发送等等方法

    @ResponseBody
    @GetMapping("/sendMessageToExchangeFail")
    public Object sendMessageToExchangeFail() {
    
    
        String messageData = "这条消息不会到达交换机";
        rabbitTemplate.convertAndSend("不存在的交换机", "", messageData, new CorrelationData(UUID.randomUUID().toString()));
        return messageData;
    }
    @ResponseBody
    @GetMapping("/sendMessageToQueueFail")
    public Object sendMessageToQueueFail() {
    
    
        String messageData = "这条消息不会到达队列";
        rabbitTemplate.convertAndSend(DirectRabbitConfig.DIRECT_EXCHANGE, "不存在的路由键", messageData, new CorrelationData(UUID.randomUUID().toString()));
        return messageData;
    }
}

リクエスト結果:
ここに画像の説明を挿入

3.4 デッドレターキューにより注文タイムアウトが解決され、支払いが失敗する

シナリオ: 顧客が製品を購入するとき、
支払いを完了するために注文 = "在庫を差し引く =" を生成する操作があります。
在庫に商品が 1 つだけ残っているとき、ユーザー A は注文を出しましたが、支払いをしていませんでした。 、これにより、ユーザー B は注文時に在庫を判断することになります。不十分な場合は注文の生成に失敗します。
この時点で、残業代未払いの問題を解決する必要があります。

プロセス:
通常のキューとスイッチ A および B の 2 つのグループを初期化します。グループ A の初期化パラメータ x-dead-letter-exchange および x-dead-letter-routing-key は、グループ B のスイッチおよびルーティング キーを指します。これは、A 内の削除または期限切れのデータを、指定されたスイッチの指定されたルーティング キーのキューに入れることができることを意味します。
- このように、注文が 5 分以上未払いになるように設定されている場合
、送信者はメッセージを送信するときに有効期限を 5 * 60 * 1000 として指定します。
時間が経過すると、メッセージはキュー B に配信されます (デッドレターキュー)、キューBは注文IDに応じて支払いを行うかどうかを判断し、在庫の追加などの対応する操作を実行します。

デッドレターキュー構成クラス

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;

import java.util.HashMap;
import java.util.Map;

/**
 * 解决订单超时未支付的问题
 *
 * 创建两个队列
 * 1、队列A(正常的队列只是设置了某些参数):设置队列中的超时未消费信息指定丢到对应的队列B
 * 2、队列B(也是一个正常的队列),只是把超时的信息丢给它所以称呼为死信队列
 */

@Configuration
public class DeadLetterExchangeConfig {
    
    

    /**
     * x-message-tti(Time-To-Live)发送到队列的消息在丟弃之前可以存活多长时间(毫秒)
     * x-max-length限制队列最大长度(新增后挤出最早的),单位个数
     * x-expires队列没有访问超时时,自动删除(包含没有消费的消息),单位毫秒
     * x-max-length-bytes限制队列最大容量
     * x-dead-letter-exchange死信交换机,将删除/过期的数据,放入指定交换机
     * x-dead-letter-routing-key死信路由,将删除/过期的数据,放入指定routingKey
     * x-max-priority队列优先级
     * x-queue-mode对列模式,默认lazy(将数据放入磁盘,消费时放入内存)
     * x-queue-master-locator镜像队列
     */
    @Bean
    public Queue orderQueue(){
    
    
        Map<String, Object> args = new HashMap<>(2);
        // 绑定我们的死信交换机
        args.put("x-dead-letter-exchange", "orderDeadExChange");
        // 绑定我们的路由key
        args.put("x-dead-letter-routing-key", "orderDeadRoutingKey");
        return new Queue("orderQueue", true, false, false, args);
    }
    @Bean
    public Queue orderDeadQueue(){
    
    
        return new Queue("orderDeadQueue");
    }
    @Bean
    public DirectExchange orderExchange(){
    
    
        return new DirectExchange("orderExchange");
    }
    @Bean
    public DirectExchange orderDeadExchange(){
    
    
        return new DirectExchange("orderDeadExChange");
    }
    //绑定正常队列到交换机
    @Bean
    public Binding orderBindingExchange(Queue orderQueue, DirectExchange orderExchange) {
    
    
        return BindingBuilder.bind(orderQueue).to(orderExchange).with("orderRoutingKey");
    }
    //绑定死信队列到死信交换机
    @Bean
    public Binding deadBindingExchange(Queue orderDeadQueue,  DirectExchange orderDeadExchange) {
    
    
        return BindingBuilder.bind(orderDeadQueue).to(orderDeadExchange).with("orderDeadRoutingKey");
    }
}

消費者

import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
 * 死信队列的消费者
 */
@Slf4j
@Component
public class DeadLetterReceiver {
    
    

    @RabbitListener(queues = "orderDeadQueue")
    public void orderDeadQueueReceiver(String dataMsg, Channel channel, Message message) {
    
    
        try{
    
    
            long deliveryTag = message.getMessageProperties().getDeliveryTag();
            log.info("死信队列接收者A收到消息,根据订单id查询订单是否支付,未支付解锁库存 deliveryTag:{} dataMsg:{} ",deliveryTag ,dataMsg);
            channel.basicAck(deliveryTag,false);
        } catch (Exception e){
    
    
            log.info("如果报错了,执行补偿机制");
        }
    }
}

プロデューサー

@GetMapping("/createOrder")
public String createOrder() {
    
    
    rabbitTemplate.convertAndSend("orderExchange", "orderRoutingKey", "我是订单json", message -> {
    
    
        //设置过期时间10s
        message.getMessageProperties().setExpiration("10000");
        return message;
    });
    return "发送完成";
}

要約する

MQ のアプリケーション シナリオ:

  1. 非同期処理(登録、メール送信、ショートメッセージ送信)
  2. アプリケーションの切り離し(ユーザーが注文した後、注文システムは在庫を差し引くように在庫システムに通知する必要があります。在庫システムに障害が発生した場合でも、メッセージ キューにより、メッセージ損失を引き起こすことなくメッセージを確実に配信できます。)
  3. トラフィック ピーク シェービング(スパイク アクティビティ。通常は過剰なトラフィックが原因で、アプリケーションがハングアップし、メッセージ キュー パラメーターを設定します。長さが最大値を超える場合、ユーザー リクエストは直接破棄されるか、エラー ページにジャンプします)

おすすめ

転載: blog.csdn.net/weixin_45549188/article/details/129175289