수동으로 springboot 프로젝트 빌드 06-springboot는 RabbitMQ와 해당 원칙 및 애플리케이션 시나리오를 통합합니다.


머리말

RabbitMQ는 AMQP (Advanved Message Queue Protocol) 고급 메시지 큐 프로토콜을 구현하는 erlang 언어로 개발된 메시지 서비스 미들웨어 입니다.

워크플로 - 소울 페인터

작업 과정
1. 생산자(Producer)와 소비자(Consumer) 모두 RabbitMQ와 긴 연결(Connection)을 설정해야 메시지를 보내고 받을 수 있습니다. 2. 클라이언트
(생산자, 소비자)와 서버(RabbitMQ)는 긴 연결, 긴 연결에서 채널을 열어 메시지 송수신
3. 생산자가 메시지를 보내고 메시지는 브로커의 지정된 가상 호스트(서비스 구성됩니다) 라우팅 키와 스위치에 따라 대기열과의 바인딩 관계는 해당 대기열
3에 메시지를 보내고 소비자는 채널을 통해 대기열을 듣고 메시지는 실제로 소비자가 얻을 수 있습니다 대기열에 들어간 후 시간

용어 사전

1. 브로커 (메시지 브로커) 메시지 브로커: 간단히 우체국으로 이해되는 메시지 큐 서버 엔티티로 메일을 주고받는 역할을 합니다.
2. JMS (Java Message Service) JAVA 메시지 서비스. JVM 메시지 브로커 기반의 사양입니다. ActiveMQHornetMQ 는 JMS 구현입니다.
3. AMQP (Advanced Message Queuing Protocol)
고급 메시지 대기열 프로토콜도 메시지 브로커 사양이며, JMS
RabbitMQ 와 호환되는 AMQP 구현입니다.
여기에 이미지 설명 삽입
4. 메시지 메시지는 메시지 헤더와 메시지 본문으로 구성됩니다. 메시지 본문은 불투명한 반면 메시지 헤더는 라우팅 키(라우팅 키), 우선 순위(다른 메시지에 상대적인 우선 순위), 배달 모드(메시지에 영구 저장소가 필요할 수 있음을 나타냄), 등.
5. 생산자 메시지 생산자는 메시지를 교환기에 게시하는 클라이언트 애플리케이션이기도 합니다.
6. Exchange 스위치는 생산자가 보낸 메시지를 수신하고 이러한 메시지를 서버의 대기열로 라우팅하는 데 사용됩니다.
일반적으로 사용되는 세 가지 유형의 Exchange가 있습니다. 직접, 팬아웃, 주제 및 서로 다른 유형의 Exchange는 메시지 전달을 위한 서로 다른 전략을 사용합니다
. 7.소비자에게 보낼 때까지 메시지를 저장하는 데 사용되는 대기열 메시지 대기열. 메시지의 컨테이너이자 메시지의 대상입니다. 하나 이상의 큐에 메시지를 넣을 수 있습니다. 메시지는 항상 대기열에 있으며 소비자가 메시지를 가져오기 위해 대기열에 연결하기를 기다립니다.
8. 메시지 대기열과 스위치 간의 연결에 사용되는 바인딩 바인딩. 바인딩은 라우팅 키를 기반으로 교환기와 메시지 대기열을 연결하는 라우팅 규칙이므로 교환기는 바인딩으로 구성된 라우팅 테이블로 이해할 수 있습니다.
Exchange와 Queue의 바인딩은 다대다 관계일 수 있습니다.
9. TCP 연결과 같은 연결 네트워크 연결.
10. 다중 연결에서 독립적인 양방향 데이터 흐름 채널인 채널 채널. 채널은 실제 TCP 연결에서 설정된 가상 연결입니다.AMQP 명령은 채널을 통해 전송됩니다.메시지 게시, 대기열 구독 또는 메시지 수신 여부에 관계없이 이러한 작업은 모두 채널을 통해 완료됩니다. 운영 체제가 TCP를 설정하고 파괴하는 데 비용이 많이 들기 때문에 TCP 연결을 재사용하기 위해 채널 개념이 도입되었습니다.
11. 소비자는 메시지 대기열에서 메시지를 가져오는 클라이언트 응용 프로그램을 의미합니다.
12. 스위치, 메시지 대기열 및 관련 개체의 배치를 나타내는 가상 호스트 . 가상 호스트는 동일한 인증 및 암호화 환경을 공유하는 별도의 서버 도메인입니다. 각 가상 호스트는 기본적으로 자체 큐, 스위치, 바인딩 및 권한 메커니즘이 있는 RabbitMQ 서버의 미니 버전입니다. 가상 호스트는 AMQP 개념의 기본이며 연결 시 지정해야 합니다.RabbitMQ의 기본 가상 호스트는 /입니다.

스위치 유형

여기에 이미지 설명 삽입


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(웹 관리 백그라운드 포트)

로컬 설치는 다음을 통해 액세스할 수 있습니다. 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);
    }
}

이 구성은 주로 대기열, 스위치 및 바인딩을 스프링으로 관리하며 대기열, 스위치를 선언하고 바인딩 관계를 설정해야 합니다. 지정된 스위치에서 메시지를 보낸 후 스위치는 라우팅 키에 따라 일치하는 큐에 메시지를 보낼 수 있습니다.

소비자

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 "发送完成";
    }
}

Run Discovery: 기본적으로 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. 하나의 소비자만이 메시지의 순서를 보장할 수 있지만 효율성이 낮은 경우.
2. Producer는 Queue에 메시지를 순차적으로 보내지만 여러 Consumer가 Queue를 수신하면 배포를 위해 폴링하여 장애가 발생합니다. 소비자는 하나의 대기열만 듣고 생산자는 전달 전략을 맞춤화하여 A 대기열에 1, 2, 3을 넣고 B 대기열에 4, 5, 6을 넣도록 수정했습니다(순차적 메시지는 전체) 큐에 넣습니다.

3.2 메시지가 반복적으로 사용되지 않는지 확인(멱등성)

소비자가 소비한 후 정상적인 상황에서는 메시지가 소비되었음을 증명하기 위해 영수증이 메시지 대기열로 전송됩니다. 그러나 이 시점에서 소비자 네트워크 전송 실패 또는 중단 시간, 메시지 큐는 메시지 소비 영수증을 받지 못하고 메시지를 다른 소비자에게 재분배하여 메시지가 여러 번 소비됩니다.
········
Solution : (구체적인 문제에 대한 구체적인 분석)
1. redis에 set을 유지하고, Producer가 메시지를 보내기 전에 global unique id를 추가하고, Consumer가 소비하기 전에 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 메시지의 신뢰성 보장

메시지 전송 프로세스 Producer 가 보낸 메시지가 Consumer에게
메시지 전송 프로세스
정확하게 도착하면 두 부분으로 나뉘는 것을 볼 수 있습니다 . 스위치가 큐에 메시지를 전달하는 데 실패하면 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의 두 그룹을 초기화합니다. 그룹 A의 초기화 매개변수 x-dead-letter-exchange 및 x-dead-letter-routing-key는 그룹 B의 스위치 및 라우팅 키를 가리킵니다. 이는 A에서 삭제되거나 만료된 데이터를 지정된 스위치의 지정된 라우팅 키 대기열에 넣을 수 있음을 의미합니다.
- 이와 같이 주문이 5분 이상 미결제로 설정되어 있으면
발신자는 메시지를 보낼 때 만료 시간을 5 * 60 * 1000으로 지정하고
시간이 지나면 해당 메시지는 Queue 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. 트래픽 피크 쉐이빙 (스파이크 활동, 일반적으로 과도한 트래픽으로 인해 애플리케이션이 중단되고 메시지 큐 매개변수를 설정합니다. 길이가 최대값을 초과하면 사용자 요청이 직접 폐기되거나 오류 페이지로 이동합니다.)

Supongo que te gusta

Origin blog.csdn.net/weixin_45549188/article/details/129175289
Recomendado
Clasificación