在调用第三方接口或者使用mq时,会出现网络抖动,连接超时等网络异常,所以需要重试。为了使处理更加健壮并且不太容易出现故障,后续的尝试操作,有时候会帮助失败的操作最后执行成功。例如,由于网络故障或数据库更新中的DeadLockLoserException导致Web服务或RMI服务的远程调用可能会在短暂等待后自行解决。 为了自动执行这些操作的重试,Spring Batch具有RetryOperations策略。不过该重试功能从Spring Batch 2.2.0版本中独立出来,变成了Spring Retry模块。
一、使用Spring boot集成的@Retryable
1、引入maven依赖
org.springframework.retry
spring-retry
org.aspectj
aspectjweaver
2、springboot 启动类标注@EnableRetry
3、在需要重试的方法上使用@Retryable
@Retryable(value= {RemoteAccessException.class},maxAttempts = 3,backoff = @Backoff(delay = 5000l,multiplier = 1))
参数说明:
value:抛出指定异常才会重试
include:和value一样,默认为空,当exclude也为空时,默认所以异常
exclude:指定不处理的异常
maxAttempts:最大重试次数,默认3次
backoff:重试等待策略,默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000L;multiplier(指定延迟倍数) 默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
具体可参考原文:https://blog.csdn.net/keets1992/article/details/80255698
2 自定义实现重试机制
场景: 我们通常在服务启动后通过继承CommandLineRunner来实现一些任务异步执行初始化操作,如果重写的run方法中有多个任务,可能某些任务没有启动成功,那么需要重试。如下代码所示有很多任务需要异步执行,这些为了保证成功启动,均需要启用重试机制,本次介绍的重试机制是基于数据库实现的,可以在数据库中查看到启动失败的任务。但该方案与业务数据库耦合度较高,可把中间状态数据替换为基于redis的实现。
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.CommandLineRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* @description: 服务启动时异步执行初始化操作
*/
@Component
@Slf4j
public class InitListener implements CommandLineRunner {
@Autowired
private EventService eventService;
@Autowired
private AutoRegConsumer autoRegConsumer;
@Autowired
private LicenseConsumer licenseConsumer;
@Autowired
private DictDataService dictDataService;
@Autowired
private ResubscribeReminderService resubscribeReminderService;
@Qualifier("redisTemplate")
@Autowired
private RedisTemplate redisTemplate;
@Override
public void run(String... args) throws Exception {
log.info(" start to do something init operate ");
try {
//每次启动清除所有的reconnect method
resubscribeReminderService.deleteAll();
/**
* 将常用的字典数据缓存到redis中
*/
CompletableFuture.supplyAsync(()->{
dictDataService.setCommonDictDatasToRedis();
return null;
});
// 异步方式获取license信息
LicenseJob synclicense = new LicenseJob(DacConfigConsts.LICENSE_SYNC);
LicenseTaskExecutor.submit(synclicense);
//启动license信息变更监听
LicenseJob licenseChange = new LicenseJob(DacConfigConsts.LICENSE_CHANGE);
LicenseTaskExecutor.submit(licenseChange);
CompletableFuture.supplyAsync(()->{
deleteDHkey();
return null;
});
CompletableFuture.supplyAsync(()->{
licenseConsumer.licenseListener();
return null;
});
//监听自动注册事件
CompletableFuture.supplyAsync(()->{
autoRegConsumer.startHandleAutoReg();
return null;
});
log.info(" init end ............ ");
} catch (Exception e) {
log.error(LogUtil.logWithParams("init listener failure", "exception detail msg"), e);
}
}
private void deleteDHkey() {
List
重试机制的自定义实现:
1、配置自定义重试AOP
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @Description:
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.TYPE})
public @interface NeedRetry {
}
import com.alibaba.fastjson.JSON;
import com.***.core.service.***.ResubscribeReminderService;
import com.***.common.context.SpringBeanContext;
import com.***.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* @Description: 将需要重试的方法写入数据库中,定时10分钟重试一次,重试成功就从数据库中移除
*/
@Aspect
@Slf4j
@Component
public class NeedRetryAspect {
@Pointcut("@annotation(NeedRetry)")
public void pointCut() {}
/**
* 对订阅的结果做判断,如果订阅失败就将订阅方法写入数据,待后期重试
* @param joinPoint
* @param result
*/
@AfterReturning(pointcut = "pointCut()",returning = "result")
public void afterReturning(JoinPoint joinPoint,Object result){
Result subscribeResult = (Result)result;
if (!subscribeResult.getCode().equalsIgnoreCase("0")){
Object target = joinPoint.getTarget();
String className = target.getClass().getName();
Signature signature = joinPoint.getSignature();
String method = signature.getName();
String methodParams = null;
String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
Class[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getParameterTypes();
String paramterTypesAsString = null;
String paramterValuesAsString = null;
if (null != parameterNames && parameterNames.length>0){
Map params = new ConcurrentHashMap<>(10);
List paramsValues = new ArrayList<>();
for (int i = 0; i < parameterNames.length; i++) {
String value = joinPoint.getArgs()[i] != null ? JSON.toJSONString(joinPoint.getArgs()[i]) : "null";
if (!StringUtils.isNotEmpty(value)) {
paramsValues.add(value);
params.put(parameterNames[i], value);
}
}
paramterTypesAsString = Arrays.stream(parameterTypes).map(clazz -> clazz.getName().toString()).collect(Collectors.joining(";"));
paramterValuesAsString = String.join(";",paramsValues);
methodParams = JSON.toJSONString(params);
}
ResubscribeReminderService resubscribeReminderService = SpringBeanContext.getBean(ResubscribeReminderService.class);
resubscribeReminderService.save(className,method,methodParams,paramterTypesAsString,paramterValuesAsString);
}
}
}
根据函数返回值是不是0(成功)来触发重试
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.***.***.core.model.entity.ResubscribeReminder;
import com.**.*****.ResubscribeReminderService;
import com.hikvision.fireprotection.common.context.SpringBeanContext;
import com.hikvision.fireprotection.common.log.SystemLogUtil;
import com.hikvision.fireprotection.common.util.StringUtil;
import com.hikvision.fireprotection.common.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.List;
@Component
@Slf4j
public class InitMethodRetryScheduleTasks {
@Autowired
private ResubscribeReminderService resubscribeReminderService;
/**
* 定时任务:重试启动方法中失败的订阅方法
*/
@Scheduled(cron = "0 */10 * * * *")
public void retryInitFailMethod() throws Exception {
List allResubscribeReminder = resubscribeReminderService.findAll();
for (ResubscribeReminder reminder :allResubscribeReminder) {
String className = reminder.getClassName();
String methodName = reminder.getMethodName();
String paramsTypesStr = reminder.getParamsTypes();
String paramsValuesStr = reminder.getParamsValues();
Class [] classTypes = null;
Object [] paramsValues = null;
if(StringUtil.isNotNullAndEmpty(paramsTypesStr)&& StringUtil.isNotNullAndEmpty(paramsValuesStr)){
String[] paramsTypesAsJson = reminder.getParamsTypes().split(";");
String[] paramsValuesAsJson = reminder.getParamsValues().split(";");
classTypes = new Class[paramsTypesAsJson.length];
paramsValues = new Object[paramsValuesAsJson.length];
if (paramsTypesAsJson.length>0){
String tmpClassType = "";
Object tmpParamValue = null;
for (int i=0; i clazz = Class.forName(className);
Method declaredMethod = clazz.getDeclaredMethod(methodName, classTypes);
Object bean = SpringBeanContext.getBean(clazz);
try {
Object invoke = declaredMethod.invoke(bean, paramsValues);
Result result = (Result) invoke;
if (result.getCode().equalsIgnoreCase("0")){
resubscribeReminderService.delete(reminder.getId());
}
} catch (Exception e) {
log.error(SystemLogUtil.logWithParams("retry subscribe failure","class","method","paramsValues","exception detail msg"),className,methodName,JSON.toJSON(paramsValues),e);
}
}
}
}
数据库实现:
数据库表
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
/**
* @Description: 订阅失败后会把相关的数据放到这个里面,会有定时任务10分钟扫描一次这个表,对这个表中的订阅内容重新订阅,订阅成功后会删除这个订阅内容
*/
@Data
@Entity
@Table(name = "tb_resubscribe_reminder")
@NoArgsConstructor
public class ResubscribeReminder {
@Id
@Column(name = "id",nullable = false,unique = true)
private String id;
@Column(name = "class_name",length = 256,nullable = false)
private String className;
@Column(name = "method_name",length = 256,nullable = false)
private String methodName;
@Column(name = "params_content",length = 4096)
private String paramsContent;
@Column(name = "params_types",length = 2056)
private String paramsTypes;
@Column(name = "params_values",length = 2056)
private String paramsValues;
}
数据库接口:
import com.***.core.model.entity.ResubscribeReminder;
import java.util.List;
/**
* @Description:
*/
public interface ResubscribeReminderService {
ResubscribeReminder save(String className, String method, String paramsContent, String paramsTypesAsJson, String paramsValuesAsJson);
void delete(String id);
void deleteAll();
List findAll();
}
获取springBean工具类
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringBeanContext implements ApplicationContextAware {
private static ApplicationContext springContext;
@Override
public void setApplicationContext(ApplicationContext springContext) throws BeansException {
this.springContext = springContext;
}
public static ApplicationContext getSpringContext() {
return springContext;
}
/**
* 根据bean名称获取bean
* @param name
* @return
*/
public static Object getBean(String name){
return getSpringContext().getBean(name);
}
/**
* 根据bean class获取bean
* @param clazz
* @return
*/
public static T getBean(Class clazz){
return getSpringContext().getBean(clazz);
}
/**
* 根据bean名称和bean class获取bean
* @param name
* @param clazz
* @return
*/
public static T getBean(String name, Class clazz){
return getSpringContext().getBean(name, clazz);
}
}
3、使用自定义重试
即在需要重试的方法上标注@NeedRetry
@NeedRetry
DataResult licenseListener(){
DataResult result = new DataResult();
...
...
...
StreamClosedHttpResponse response = HttpClientSSLUtils.doPostStringSecurity(url, context, tokenHeader);
log.info("Method: NmsServiceImpl.addSubscribeToNums response info: " + response.getContent() + "**************************************");
DataResult subscribeResult = JSON.parseObject(response.getContent(), DataResult.class);
//根据http请求返回的是不是0来触发重试, 不是0就会设置进去,定时任务遍历不等于0触发
if (!subscribeResult.getCode().equalsIgnoreCase("0")){
result.setCode(subscribeResult.getCode());
}
return result;
}