Spring boot 重试机制用法与实现

在调用第三方接口或者使用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 dhKey = Lists.newArrayList();
        Class constsClass = DacConfigConsts.class;
        Field[] declaredFields = constsClass.getDeclaredFields();
        try {
            for (Field field : declaredFields) {
                String key = String.valueOf(field.get(constsClass));
                if (key.contains("dh_cache")) {
                    dhKey.add(key);
                }
            }
            redisTemplate.delete(dhKey);
        } catch (Exception e) {
            log.error("InitListener deleteDHkey error");
        }


    }


}

重试机制的自定义实现:

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;
}

 

你可能感兴趣的:(java,Java,语言)