一:简介
使用场景:调用第三方接口时如果调用失败需要隔几秒再去尝试下一次调用,直到调用N次还失败就停止调用。最常用的场景就是支付成功异步通知第三方支付成功。
1. 为什么要调用多次?
如果调用1次就成功了就停止调用,如果失败可能由于网络原因没有请求到服务器需要再次尝试,第二次很可能就会调用成功了。
2. 为什么要间隔几秒再尝试下次调用?
如果是因为网络原因没有请求到服务器如果再立刻调用,很可能此时网络还是没有好,可能等几秒后网络就恢复了,此时再去调用就好了。
实现效果类似于支付宝中的回调延迟重试:
二:代码示例
功能示例:每隔2秒、4秒、8秒、16秒去重试调用接口,总共调用4次。
-
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>
-
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
-
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; } } }
-
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;
}
}
-
声明队列和交换机
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); } }
-
消费消息
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); } }
-
模拟回调
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)); } }
三:参考文章
来源:CSDN
作者:vbirdbest
链接:https://blog.csdn.net/vbirdbest/article/details/104654376