Springboot + RabbitMQ实现消息延迟重试

*爱你&永不变心* 提交于 2020-03-04 18:39:14

一:简介

使用场景:调用第三方接口时如果调用失败需要隔几秒再去尝试下一次调用,直到调用N次还失败就停止调用。最常用的场景就是支付成功异步通知第三方支付成功。

1. 为什么要调用多次?

如果调用1次就成功了就停止调用,如果失败可能由于网络原因没有请求到服务器需要再次尝试,第二次很可能就会调用成功了。

2. 为什么要间隔几秒再尝试下次调用?

如果是因为网络原因没有请求到服务器如果再立刻调用,很可能此时网络还是没有好,可能等几秒后网络就恢复了,此时再去调用就好了。

实现效果类似于支付宝中的回调延迟重试:

在这里插入图片描述

二:代码示例

功能示例:每隔2秒、4秒、8秒、16秒去重试调用接口,总共调用4次。

在这里插入图片描述

  1. pom.xml

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
      <groupId>org.apache.httpcomponents</groupId>
      <artifactId>httpclient</artifactId>
      <version>4.5.6</version>
    </dependency>
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.62</version>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    
  2. application.yml

    spring:
      rabbitmq:
        host: localhost
        port: 5672
        username: guest
        password: guest
    
    ## http component
    http:
      maxTotal: 100 #最大连接数
      defaultMaxPerRoute: 20 # 并发数
      connectTimeout: 1000  #创建连接的最长时间
      connectionRequestTimeout: 500 #从连接池中获取到连接的最长时间
      socketTimeout: 10000  #数据传输的最长时间
      validateAfterInactivity: 1000
    
  3. HttpClient

    import org.apache.http.client.config.RequestConfig;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClientBuilder;
    import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class HttpClientConfig {
    
        @Value("${http.maxTotal}")
        private Integer maxTotal;
    
        @Value("${http.defaultMaxPerRoute}")
        private Integer defaultMaxPerRoute;
    
        @Value("${http.connectTimeout}")
        private Integer connectTimeout;
    
        @Value("${http.connectionRequestTimeout}")
        private Integer connectionRequestTimeout;
    
        @Value("${http.socketTimeout}")
        private Integer socketTimeout;
    
        @Value("${http.validateAfterInactivity}")
        private Integer validateAfterInactivity;
    
    
        @Bean
        public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager(){
            PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
            connectionManager.setMaxTotal(maxTotal);
            connectionManager.setDefaultMaxPerRoute(defaultMaxPerRoute);
            connectionManager.setValidateAfterInactivity(validateAfterInactivity);
            return connectionManager;
        }
    
        @Bean
        public HttpClientBuilder httpClientBuilder(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager){
            HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
            httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager);
    
            return httpClientBuilder;
        }
    
        @Bean
        public CloseableHttpClient closeableHttpClient(HttpClientBuilder httpClientBuilder){
            return httpClientBuilder.build();
        }
    
    
        @Bean
        public RequestConfig.Builder builder(){
            RequestConfig.Builder builder = RequestConfig.custom();
            return builder.setConnectTimeout(connectTimeout)
                    .setConnectionRequestTimeout(connectionRequestTimeout)
                    .setSocketTimeout(socketTimeout);
        }
    
        @Bean
        public RequestConfig requestConfig(RequestConfig.Builder builder){
            return builder.build();
        }
    }
    
    
    import com.alibaba.fastjson.JSONObject;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.http.HttpHeaders;
    import org.apache.http.HttpStatus;
    import org.apache.http.client.config.RequestConfig;
    import org.apache.http.client.methods.CloseableHttpResponse;
    import org.apache.http.client.methods.HttpGet;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.client.utils.URIBuilder;
    import org.apache.http.entity.ContentType;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.util.EntityUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.Map;
    
    
    @Slf4j
    @Component
    public class HttpClient {
    
        /** 默认字符集 */
        public static final String DEFAULT_CHARSET = "UTF-8";
    
        @Autowired
        private CloseableHttpClient closeableHttpClient;
    
        @Autowired
        private RequestConfig config;
    
    
    
        public <T> T doPost(String url, Map<String, Object> requestParameter, Class<T> clazz) throws Exception {
            HttpResponse httpResponse = this.doPost(url, requestParameter);
            if (clazz == String.class) {
                return (T) httpResponse.getBody();
            }
            T response = JSONObject.parseObject(httpResponse.getBody(), clazz);
            return response;
        }
    
        public HttpResponse doPost(String url, Map<String, Object> requestParameter) throws Exception {
            HttpPost httpPost = new HttpPost(url);
            httpPost.setConfig(config);
            httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
    
            if (requestParameter != null) {
                String requestBody = JSONObject.toJSONString(requestParameter);
                StringEntity postEntity = new StringEntity(requestBody, "UTF-8");
                httpPost.setEntity(postEntity);
            }
    
            CloseableHttpResponse response = this.closeableHttpClient.execute(httpPost);
            // 对请求的响应进行简单的包装成自定义的类型
            return new HttpResponse(response.getStatusLine().getStatusCode(), EntityUtils.toString(
                    response.getEntity(), DEFAULT_CHARSET));
        }
    
    
    
        /**
         * 封装请求的响应码和响应的内容
         */
        public class HttpResponse {
            /** http status */
            private Integer code;
            /** http response content */
            private String body;
    
            public HttpResponse() { }
    
            public HttpResponse(Integer code, String body) {
                this.code = code;
                this.body = body;
            }
    
            public Integer getCode() {
                return code;
            }
    
            public void setCode(Integer code) {
                this.code = code;
            }
    
            public String getBody() {
                return body;
            }
    
            public void setBody(String body) {
                this.body = body;
            }
        }
    }
    
  4. MessagePostProcessor

import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;

/**
 * 设置消息过期时间
 */
public class ExpirationMessagePostProcessor implements MessagePostProcessor{

    private final Long ttl;

    public ExpirationMessagePostProcessor(Long ttl) {
        this.ttl = ttl;
    }

    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        // 设置失效时间
        message.getMessageProperties().setExpiration(ttl.toString());
        return message;
    }
}
  1. 声明队列和交换机

    package com.example.rabbitmq.retry;
    
    import org.springframework.amqp.core.*;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 延迟队列
     */
    @Configuration
    public class DelayQueueConfig {
    
        /** 缓冲队列名称:过期时间针对于每个消息 */
        public final static String DELAY_QUEUE_PER_MESSAGE_TTL_NAME = "delay_queue_per_message_ttl";
    
        /** 死亡交换机:过期消息将通过该死亡交换机放入到实际消费的队列中  */
        public final static String DELAY_EXCHANGE_NAME = "delay_exchange";
    
        /** 死亡交换机对应的路由键,通过该路由键路由到实际消费的队列 */
        public final static String DELAY_PROCESS_QUEUE_NAME = "delay_process_queue";
    
        /** 路由到 delay_queue_per_message_ttl(统一失效时间的队列)的exchange(用于队列延迟重试) */
        public final static String PER_MESSAGE_TTL_EXCHANGE_NAME = "per_message_ttl_exchange";
    
    
        /**
         * delay_queue_per_message_ttl
         * 每个消息都可以控制自己的失效时间
         * x-dead-letter-exchange声明了队列里的死信转发到的DLX名称
         * x-dead-letter-routing-key声明了这些死信在转发时携带的routing-key名称
         */
        @Bean
        Queue delayQueuePerMessageTTL() {
            return QueueBuilder.durable(DELAY_QUEUE_PER_MESSAGE_TTL_NAME)
                    .withArgument("x-dead-letter-exchange", DELAY_EXCHANGE_NAME)
                    .withArgument("x-dead-letter-routing-key", DELAY_PROCESS_QUEUE_NAME)
                    .build();
        }
    
        /**
         * 死亡交换机 DLX
         * @return
         */
        @Bean
        DirectExchange delayExchange() {
            return new DirectExchange(DELAY_EXCHANGE_NAME);
        }
    
        /**
         * 实际消费队列:过期之后会进入到该队列中来
         * @return
         */
        @Bean
        Queue delayProcessQueue() {
            return QueueBuilder.durable(DELAY_PROCESS_QUEUE_NAME).build();
        }
    
    
        /**
         * 将死亡交换机delayExchange和实际消费队列delay_process_queue绑定在一起,并携带路由键delay_process_queue
         * @param delayProcessQueue
         * @param delayExchange
         * @return
         */
        @Bean
        Binding dlxBinding(Queue delayProcessQueue, DirectExchange delayExchange) {
            return BindingBuilder.bind(delayProcessQueue)
                    .to(delayExchange)
                    .with(DELAY_PROCESS_QUEUE_NAME);
        }
    
    
        /**
         * 重试交换机:消费失败后通过该交换机转发到队列中
         * @return
         */
        @Bean
        DirectExchange perMessageTtlExchange() {
            return new DirectExchange(PER_MESSAGE_TTL_EXCHANGE_NAME);
        }
    
    
        /**
         * 重试交换机和缓冲队列绑定
         * @param delayQueuePerMessageTTL 缓冲队列
         * @param perMessageTtlExchange 重试交换机
         * @return
         */
        @Bean
        Binding messageTtlBinding(Queue delayQueuePerMessageTTL, DirectExchange perMessageTtlExchange) {
            return BindingBuilder.bind(delayQueuePerMessageTTL)
                    .to(perMessageTtlExchange)
                    .with(DELAY_QUEUE_PER_MESSAGE_TTL_NAME);
        }
    }
    
  2. 消费消息

    import com.alibaba.fastjson.JSONObject;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.amqp.core.AmqpTemplate;
    import org.springframework.amqp.rabbit.annotation.RabbitListener;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * 支付回调重试
     * 2秒、4秒、8秒后分别重试,算上0秒这次总共重试4次
     */
    @Slf4j
    @Component
    public class CallbackConsumer {
    
        @Autowired
        private AmqpTemplate rabbitTemplate;
    
        @Autowired
        private HttpClient httpClient;
    
        /** 最大重试次数 */
        private Integer maxRetryTime = 4;
    
    
        @RabbitListener(queues = DelayQueueConfig.DELAY_PROCESS_QUEUE_NAME)
        public void process(String msg) {
            log.info("----------date = {}, msg ={}", new Date(), msg);
            Map<String, Object> callbackRequestMap = JSONObject.parseObject(msg, Map.class);
            // 重试次数
            Integer retryTime = (Integer)callbackRequestMap.get("retryTime");
            String notifyUrl = (String)callbackRequestMap.get("notifyUrl");
    
            try {
                if (maxRetryTime <= 4) {
                    log.info("################Callback retry time = {}, date = {}################", retryTime, new Date());
                    String response = httpClient.doPost(notifyUrl, callbackRequestMap, String.class);
                    if (Objects.equals("SUCCESS", response)) {
                        log.info("ok");
                    } else {
                        // 下一次重试
                        log.error("error request={} response={}", callbackRequestMap, response);
                        if (retryTime == maxRetryTime) {
                            lastTimeRetryResult(response, null);
                            return;
                        }
                        retryCallback(callbackRequestMap);
                    }
                }
            } catch (Exception e) {
                if (maxRetryTime < maxRetryTime) {
                    retryCallback(callbackRequestMap);
                } else {
                    lastTimeRetryResult(null, e);
                }
            }
        }
    
    
        /**
         * 尝试下一次回调
         * @param callbackRequestMap 请求参数
         */
        private void retryCallback(Map<String, Object> callbackRequestMap) {
            // 下次重试,重试次数+1
            Integer retryTime = (Integer)callbackRequestMap.get("retryTime");
            callbackRequestMap.put("retryTime", retryTime + 1);
    
            // 下一次延迟是上一次延迟的2倍时间
            long expiration = retryTime * 2000;
            String callbackMessage = JSONObject.toJSONString(callbackRequestMap);
            rabbitTemplate.convertAndSend(DelayQueueConfig.DELAY_QUEUE_PER_MESSAGE_TTL_NAME,
                    (Object) callbackMessage,
                    new ExpirationMessagePostProcessor(expiration));
        }
    
        /**
         * 记录最后一次重试的结果
         * @param response
         * @param e
         */
        private void lastTimeRetryResult(String response, Exception e) {
            log.error("last time retry, response={}", response);
        }
    }
    
  3. 模拟回调

    package com.example.rabbitmq.retry;
    
    import com.alibaba.fastjson.JSONObject;
    import org.springframework.amqp.core.AmqpTemplate;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;
    
    @RestController
    @RequestMapping("/mock")
    public class MockController {
    
        @Autowired
        private AmqpTemplate rabbitTemplate;
    
        @RequestMapping("/callback")
        public void callback(String notifyUrl) {
            Map<String, Object> requestMap = new HashMap<>();
            requestMap.put("retryTime", 1);
            requestMap.put("notifyUrl", notifyUrl);
            requestMap.put("orderCode", UUID.randomUUID().toString());
            Object callbackMessage = JSONObject.toJSONString(requestMap);
    
    
            rabbitTemplate.convertAndSend(DelayQueueConfig.DELAY_QUEUE_PER_MESSAGE_TTL_NAME, callbackMessage, new ExpirationMessagePostProcessor(0L));
        }
    }
    

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

三:参考文章

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!