SpringBoot中的7种重试机制

使用重试机制保证SpringBoot应用的稳定性 没有人能保证自己的系统不出错,同样,在调用第三方系统时,也不能保证能够100%的成功。往往会因为程序逻辑、网络中断、边界值等各种各样的问题导致服务失败。在不同的业务领域对于服务的错误率有着不同的要求,一些金融领域的系统一般要求服务的错误率为0.01%。那么为了保证很低的错误率,则需要通过一些专门的机制来完成。而最常见的方式就是在出现错误时通过重试来解决。 阅读详情
AI摘要

随着互联网项目复杂度提升,服务间的调用成为常态,但远程服务的不稳定可能导致调用失败。本文介绍了四种重试机制的实现:1)手动重试,侵入性强;2)静态代理,减少侵入性;3)JDK动态代理,统一重试逻辑;4)CGLib动态代理,处理未实现接口的类。此外,还探讨了SpringAOP和Spring的重试注解,以及GuavaRetry库的使用,提供了更多灵活性和控制选项。

随着互联网的不断发展,项目中的业务功能变得越来越复杂。在实际开发过程中,我们经常会调用一些第三方接口或者公司内部的其他项目提供的服务来支撑业务需求。然而,远程服务的健壮性和网络稳定性往往是不可控的因素。尽管在测试阶段可能没有出现异常情况,但一旦系统上线,可能会遇到由于内部错误或者网络波动而导致调用接口出错或返回系统异常的情况。为了应对这些不稳定因素,我们必须考虑引入重试机制。

重试机制是一种有效的策略,它可以提高系统的健壮性,并且减少因网络波动导致依赖服务临时不可用所带来的影响,从而使系统能够更加稳定地运行。

1、手动重试

手动重试:使用 while 循环语句进行重试:

@Service
public class OrderServiceImpl implements OrderService {
	public void addOrder() {
	    int times = 1;
	    while (times <= 5) {
	        try {
	            // 故意抛异常
	            int i = 3 / 0;
	            // addOrder
	        } catch (Exception e) {
	            System.out.println("重试" + times + "次");
	            Thread.sleep(2000);
	            times++;
	            if (times > 5) {
	                throw new RuntimeException("不再重试!");
	            }
	        }
	    }
	}
}

代码运行结果:
在这里插入图片描述
上述代码看上去可以解决重试问题,但实际上存在一些弊端:

1、由于没有重试间隔,很可能远程调用的服务还没有从网络异常中恢复,所以有可能接下来的几次调用都会失败。
2、代码侵入式太高,调用方代码不够优雅。
3、项目中远程调用的服务可能有很多,每个都去添加重试会出现大量的重复代码。

2、静态代理

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。

@Service
public class OrderServiceProxyImpl implements OrderService {
    
    @Autowired
    private OrderServiceImpl orderService;

    @Override
    public void addOrder() {
        int times = 1;
        while (times <= 5) {
            try {
                // 故意抛异常
                int i = 3 / 0;
                orderService.addOrder();
            } catch (Exception e) {
                System.out.println("重试" + times + "次");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
                times++;
                if (times > 5) {
                    throw new RuntimeException("不再重试!");
                }
            }
        }
        
    }
}

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了。

代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。如果每个类都写这么一长串类似的代码,显然,不优雅!

3、JDK动态代理

基于上述弊端,这时候,动态代理就闪亮登场了。只需要写一个代理处理类就 ok 了。

@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
 
    private final Object subject;
 
    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        return null;
    }
 
    /**
     * 获取动态代理
     *
     * @param realSubject 代理对象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }
 
}

测试:

 @Test
public void helloDynamicProxy() {
    IHelloService realService = new HelloService();
    IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);
 
    String hello = proxyService.hello();
    log.info("hello:{}", hello);
}


输出如下:
hello times:1
发生异常,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
发生异常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
发生异常,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello

在重试了4次之后输出了Hello,符合预期。

动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。

不过不要高兴的太早,这里因为被代理的HelloService是一个简单的类,没有依赖其它类,所以直接创建是没有问题的,但如果被代理的类依赖了其它被Spring容器管理的类,则这种方式会抛出异常,因为没有把被依赖的实例注入到创建的代理实例中。

这种情况下,就比较复杂了,需要从Spring容器中获取已经装配好的,需要被代理的实例,然后为其创建代理类实例,并交给Spring容器来管理,这样就不用每次都重新创建新的代理类实例了。

新建一个工具类,用来获取代理实例:

@Component
public class RetryProxyHandler {
 
    @Autowired
    private ConfigurableApplicationContext context;
 
    public Object getProxy(Class clazz) {
        // 1. 从Bean中获取对象
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
        Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
        Set<Map.Entry<String, Object>> entries = beans.entrySet();
        if (entries.size() <= 0){
            throw new ProxyBeanNotFoundException();
        }
        // 如果有多个候选bean, 判断其中是否有代理bean
        Object bean = null;
        if (entries.size() > 1){
            for (Map.Entry<String, Object> entry : entries) {
                if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){
                    bean = entry.getValue();
                }
            };
            if (bean != null){
                return bean;
            }
            throw new ProxyBeanNotSingleException();
        }
 
        Object source = beans.entrySet().iterator().next().getValue();
        Object source = beans.entrySet().iterator().next().getValue();
 
        // 2. 判断该对象的代理对象是否存在
        String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;
        Boolean exist = beanFactory.containsBean(proxyBeanName);
        if (exist) {
            bean = beanFactory.getBean(proxyBeanName);
            return bean;
        }
 
        // 3. 不存在则生成代理对象
        bean = RetryInvocationHandler.getProxy(source);
 
        // 4. 将bean注入spring容器
        beanFactory.registerSingleton(proxyBeanName, bean);
        return bean;
    }
}

使用的是JDK动态代理:

@Slf4j
public class RetryInvocationHandler implements InvocationHandler {
 
    private final Object subject;
 
    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }
 
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("retry times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        return null;
    }
 
    /**
     * 获取动态代理
     *
     * @param realSubject 代理对象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }
 
}

至此,主要代码就完成了,修改一下HelloService类,增加一个依赖:

@Slf4j
@Component
public class HelloService implements IHelloService{
 
    private static AtomicLong helloTimes = new AtomicLong();
 
    @Autowired
    private NameService nameService;
 
    public String hello(){
        long times = helloTimes.incrementAndGet();
        log.info("hello times:{}", times);
        if (times % 4 != 0){
            log.warn("发生异常,time:{}", LocalTime.now() );
            throw new HelloRetryException("发生Hello异常");
        }
        return "hello " + nameService.getName();
    }
}

NameService其实很简单,创建的目的仅在于测试依赖注入的Bean能否正常运行:

@Service
public class NameService {
 
    public String getName(){
        return "Frank";
    }
}

测试:

@Test
public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);
}
hello times:1
发生异常,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
发生异常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
发生异常,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank

完美,这样就不用担心依赖注入的问题了,因为从Spring容器中拿到的Bean对象都是已经注入配置好的。当然,这里仅考虑了单例Bean的情况,可以考虑的更加完善一点,判断一下容器中Bean的类型是Singleton还是Prototype,如果是Singleton则像上面这样进行操作,如果是Prototype则每次都新建代理类对象。

另外,这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了。

4、CGLib动态代理

既然已经说到了JDK动态代理,那就不得不提CGLib动态代理了。使用JDK动态代理对被代理的类有要求,不是所有的类都能被代理,而CGLib动态代理则刚好解决了这个问题。

创建一个CGLib动态代理类:

@Slf4j
public class CGLibRetryProxyHandler implements MethodInterceptor {
    private Object target;//需要代理的目标对象
 
    //重写拦截方法
    @Override
    public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable {
        int times = 0;
 
        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(target, arr);
            } catch (Exception e) {
                times++;
                log.info("cglib retry :{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }
 
            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    //定义获取代理对象方法
    public Object getCglibProxy(Object objectTarget){
        this.target = objectTarget;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(objectTarget.getClass());
        enhancer.setCallback(this);
        Object result = enhancer.create();
        return result;
    }
}

想要换用CGLib动态代理,替换一下这两行代码即可:

// 3. 不存在则生成代理对象
//        bean = RetryInvocationHandler.getProxy(source);
CGLibRetryProxyHandler proxyHandler = new CGLibRetryProxyHandler();
bean = proxyHandler.getCglibProxy(source);

测试:

@Test
public void helloCGLibProxy() {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);
 
    hello = proxy.hello();
    log.info("hello:{}", hello);
}


hello times:1
发生异常,time:15:06:00.799679100
cglib retry :1,time:15:06:00.800175400
hello times:2
发生异常,time:15:06:01.800848600
cglib retry :2,time:15:06:01.801343100
hello times:3
发生异常,time:15:06:02.802180
cglib retry :3,time:15:06:02.802180
hello times:4
hello:hello Frank
hello times:5
发生异常,time:15:06:03.803933800
cglib retry :1,time:15:06:03.803933800
hello times:6
发生异常,time:15:06:04.804945400
cglib retry :2,time:15:06:04.805442
hello times:7
发生异常,time:15:06:05.806886500
cglib retry :3,time:15:06:05.807881300
hello times:8
hello:hello Frank

这样就很棒了,完美的解决了JDK动态代理带来的缺陷。优雅指数上涨了不少。

但这个方案仍旧存在一个问题,那就是需要对原来的逻辑进行侵入式修改,在每个被代理实例被调用的地方都需要进行调整,这样仍然会对原有代码带来较多修改。

5、手动AOP

想要无侵入式的修改原有逻辑?想要一个注解就实现重试?用Spring AOP不就能完美实现吗?使用AOP来为目标调用设置切面,即可在目标方法调用前后添加一些额外的逻辑。

自定义注解:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {
    
    // 最大重试次数
    int retryTimes() default 3;
    // 重试间隔
    int retryInterval() default 1;

}

然后在需要重试的方法上加上注解:

@Retryable(retryTimes = 4, retryInterval = 2)
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello " + nameService.getName();
}

接着,进行最后一步,编写AOP切面:

@Slf4j
@Aspect
@Component
public class RetryAspect {
 
    @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
    private void retryMethodCall(){}
 
    @Around("retryMethodCall()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        // 获取重试次数和重试间隔
        Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class);
        int maxRetryTimes = retry.retryTimes();
        int retryInterval = retry.retryInterval();
 
        Throwable error = new RuntimeException();
        for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
            try {
                Object result = joinPoint.proceed();
                return result;
            } catch (Throwable throwable) {
                error = throwable;
                log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);
            }
            Thread.sleep(retryInterval * 1000);
        }
        throw new RetryExhaustedException("重试次数耗尽", error);
    }
}

测试:

@Autowired
private HelloService helloService;
 
@Test
public void helloAOP(){
    String hello = helloService.hello();
    log.info("hello:{}", hello);
}

hello times:1
发生异常,time:16:49:30.224649800
调用发生异常,开始重试,retryTimes:1
hello times:2
发生异常,time:16:49:32.225230800
调用发生异常,开始重试,retryTimes:2
hello times:3
发生异常,time:16:49:34.225968900
调用发生异常,开始重试,retryTimes:3
hello times:4
hello:hello Frank

这样即不用编写重复代码,实现上也比较优雅了:一个注解就实现重试。

6、Spring 的重试注解

实际上Spring中就有比较完善的重试机制,比上面的切面更加好用,还不需要自己动手重新造轮子。

那让我们先来看看这个轮子究竟好不好使。
先引入重试所需的jar包:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

然后在启动类或者配置类上添加@EnableRetry注解,接下来在需要重试的方法上添加@Retryable注解(嗯?好像跟我自定义的注解一样?)

@Retryable
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello " + nameService.getName();
}

默认情况下,会重试三次,重试间隔为1秒。当然我们也可以自定义重试次数和间隔。这样就跟我前面实现的功能是一毛一样的了。

但Spring里的重试机制还支持很多很有用的特性;
比如说,可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。默认为空,会对所有异常都重试。

@Retryable{value = {HelloRetryException.class}}
public String hello(){
    ...
}

该方法调用将会在抛出HelloRetryException异常后进行重试,最大重试次数为5,第一次重试间隔为1s,之后以2倍大小进行递增,第二次重试间隔为2s,第三次为4s,第四次为8s。

重试机制还支持使用@Recover注解来进行善后工作,当重试达到指定次数之后,将会调用该方法,可以在该方法中进行日志记录等操作。

这里值得注意的是,想要@Recover注解生效的话,需要跟被@Retryable标记的方法在同一个类中,且被@Retryable标记的方法不能有返回值,否则不会生效。

并且如果使用了@Recover注解的话,重试次数达到最大次数后,如果在@Recover标记的方法中无异常抛出,是不会抛出原异常的。

@Recover
public boolean recover(Exception e) {
    log.error("达到最大重试次数",e);
    return false;
}

除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:

@Test
public void normalSpringRetry() {
    // 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
    Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
    exceptionMap.put(HelloRetryException.class, true);
 
    // 构建重试模板实例
    RetryTemplate retryTemplate = new RetryTemplate();
 
    // 设置重试回退操作策略,主要设置重试间隔时间
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    long fixedPeriodTime = 1000L;
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);
 
    // 设置重试策略,主要设置重试次数
    int maxRetryTimes = 3;
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);
 
    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);
 
    Boolean execute = retryTemplate.execute(
        //RetryCallback
        retryContext -> {
            String hello = helloService.hello();
            log.info("调用的结果:{}", hello);
            return true;
        },
        // RecoverCallBack
        retryContext -> {
            //RecoveryCallback
            log.info("已达到最大重试次数");
            return false;
        }
    );
}

此时唯一的好处是可以设置多种重试策略:

NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
 
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
 
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
 
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
 
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
 
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
 
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

可以看出,Spring中的重试机制还是相当完善的,比上面自己写的AOP切面功能更加强大。

这里还需要再提醒的一点是,由于Spring Retry用到了Aspect增强,所以就会有使用Aspect不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效。

但也还是存在一定的不足,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验。

7. guava-retry

和 Spring Retry 相比,Guava Retry 具有更强的灵活性,并且能够根据 返回值 来判断是否需要重试。

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

@Override
public String guavaRetry(Integer num) {
    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
            //无论出现什么异常,都进行重试
            .retryIfException()
            //返回结果为 error时,进行重试
            .retryIfResult(result -> Objects.equals(result, "error"))
            //重试等待策略:等待 2s 后再进行重试
            .withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))
            //重试停止策略:重试达到 3 次
            .withStopStrategy(StopStrategies.stopAfterAttempt(3))
            .withRetryListener(new RetryListener() {
                @Override
                public <V> void onRetry(Attempt<V> attempt) {
                    System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次调用");
                }
            })
            .build();
    try {
        retryer.call(() -> testGuavaRetry(num));
    } catch (Exception e) {
        e.printStackTrace();
    }
    return "test";
}

先创建一个Retryer实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制:

retryIfException():对所有异常进行重试
retryIfRuntimeException():设置对指定异常进行重试
retryIfExceptionOfType():对所有 RuntimeException 进行重试
retryIfResult():对不符合预期的返回结果进行重试
还有五个以 withXxx 开头的方法,用来对重试策略/等待策略/阻塞策略/单次任务执行时间限制/自定义监听器进行设置,以实现更加强大的异常处理:

withRetryListener():设置重试监听器,用来执行额外的处理工作
withWaitStrategy():重试等待策略
withStopStrategy():停止重试策略
withAttemptTimeLimiter:设置任务单次执行的时间限制,如果超时则抛出异常
withBlockStrategy():设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情。

总结

从手动重试,到使用 Spring AOP 自己动手实现,再到站在巨人肩上使用特别优秀的开源实现 Spring Retry 和 Google guava-retrying,经过对各种重试实现方式的介绍,可以看到以上几种方式基本上已经满足大部分场景的需要:

如果是基于 Spring 的项目,使用 Spring Retry 的注解方式已经可以解决大部分问题
如果项目没有使用 Spring 相关框架,则适合使用 Google guava-retrying:自成体系,使用起来更加灵活强大。

深入OnlyOffice回调机制:SpringBoot项目中如何优雅处理文档的保存与冲突? 本文深入探讨了OnlyOffice回调机制在SpringBoot项目中的应用,重点解析了如何优雅处理文档保存与冲突问题。通过详细的代码示例和实战策略,帮助开发者实现高并发编辑场景下的数据一致性,提升团队协作效率。文章还涵盖了强制保存、性能优化和异常处理等关键话题,为二次开发提供全面指导。 阅读详情

相关推荐

Springboot 使用 Guava 的重试Retry ,轻便灵活

前言 重试,我承认是冷饭重炒了。 之前写过一些关于重试相关的实践介绍文章: Spring的Retry《Springboot 整合Retry 实现重试机制》: https://blog.youkuaiyun.com/qq_35387940/article/details/99676114使用DelayQueue 实现了更加动态灵活的 延迟重试: 《Springboot 指定重发的次数和延迟时间,定时异步执行 重发任务》:https://blog.youkuaiyun.com/qq_35387940/article/det

默默不代表沉默 4172

Springboot_重试机制

SpringBoot重试机制

weixin_44582036的博客 696

Spring Boot 一个注解解决重试机制

Spring Boot 一个注解解决重试机制

没伞的孩子努力奔跑 1077

SpringBoot:重试机制@Retryable简介及实践

在实际工作中,重处理是一个非常常见的场景,比如:这些错误可能是因为网络波动造成的,等待过后重处理就能成功。通常来说,会用,循环之类的语法来进行重处理,但是这样的做法缺乏统一性,并且不是很方便,要多写很多代码。然而却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能。spring系列的是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在中,所有配置都是基于简单注释的。 2、启用@Retryable 3、在方法上添加@Retryable 来简单解释一下注解中几个参数的含义

GIS摆渡人 1727

【SpringBoot 4.x 第33节】夯爆了,异步任务、定时任务与重试机制!

🏆 本文收录于 《滚雪球学 Spring Boot 4.x》 专栏。本专栏面向 有一定 Java 基础,但尚未系统学习 Spring Boot 的读者,采用“滚雪球式学习法”:先跑通、再理解、再重构、再上线,带你从第一个 Hello API 开始,逐步完成一个可部署、可监控、可扩展的后端项目。

**My Coding Family** 420

【SpringBoot 4.x 第77节】乐观锁与悲观锁实战:库存扣减与并发更新!

🏆 本文收录于 《滚雪球学 Spring Boot 4.x》 专栏。本专栏面向 有一定 Java 基础,但尚未系统学习 Spring Boot 的读者,采用“滚雪球式学习法”:先跑通、再理解、再重构、再上线,带你从第一个 Hello API 开始,逐步完成一个可部署、可监控、可扩展的后端项目。

**My Coding Family** 228

正确开启zuul的重试机制

问题: 在zuul中想要开启客户端负载均衡的重试机制,网上有很多文章介绍,但是我尝试一下他们提供的配置,发现不起作用。后来通过仔细查找网上别人的文章,最终成功开启重试,记录下来,希望可以帮到别人少做弯路。 环境: Spring Cloud: Hoxton.SR7 Spring boot: 2.3.3.RELEASE 解决方法: 在pom.xml文件中引入 spring-retry包。这是zuul重试机制必须依赖的一个包,但很多文章没有提及。 <dependency> ..

weixin_43981241的博客 1019

金蝶云星空核心数据表解析与高效关联查询指南

本文深入解析金蝶云星空核心数据表结构,提供高效关联查询的实战指南。文章详细介绍了基础资料、业务单据和财务模块的关键表及其关联逻辑,重点阐述了如何利用关联表(_LK表)进行跨单据溯源,并分享了编写高性能SQL的实用技巧与避坑建议,帮助开发者、实施顾问和数据分析师快速掌握数据查询方法,提升工作效率。

weixin_29254673的博客 526

SpringBoot4.0新特性-Resilience之失败重试

从Spring7.0版本开始,Spring Framework核心模块已经集成了常用Resilience相关功能,主要包括了对方法调用的失败重试支持以及并发控制支持。SpringBoot 4.0默认的Spring的版本就是Spring 7.0, 因此SpringBoot 4.0自然也就具有了这些特性。本文先来学习下失败重试,下一篇再来学习并发控制。

若鱼的专栏 257

STM32的DMA模块及Protues仿真实验

(一)DMA简介      DMA(Direct Memory Access,直接存储器访问), 用于在外设与存储器之间以及存储器与存储器之间提供高速数据传输。可以在无需任何 CPU 操作的情况下通过 DMA 快速移动数据。这样节省的 CPU 资源可 供其它操作使用。      STM32F407共有两个 DMA 控制器,总共有 16 个数据流(每个控制器 8 个),每一个 DMA 控制器都用于管理 一个或多

weixin_45930204的博客 2850

Windows环境下DataEase一站式安装指南(含WSL2+Docker配置)

本文详细介绍了在Windows环境下通过WSL2和Docker快速部署DataEase的完整教程。从WSL2配置、Docker环境搭建到DataEase离线安装,逐步指导用户完成一站式部署,特别适合需要快速搭建数据分析平台的开发者。教程包含常见问题解决方案和性能优化技巧,帮助用户高效使用这款国产开源数据可视化工具。

weixin_42526166的博客 398

SpringBoot 4.0失败重试机制解析与实践

在分布式系统架构中,服务容错是保障系统稳定性的关键技术。失败重试机制通过自动重新执行失败操作,有效应对网络抖动、服务短暂不可用等常见问题。SpringBoot 4.0集成了Spring Framework 7.0的Resilience模块,提供声明式的@Retryable注解实现,基于AOP动态代理技术拦截异常并执行重试策略。该机制特别适用于微服务间的远程调用场景,如支付网关对接、数据同步等关键业务。通过配置重试次数、退避算法和异常过滤规则,开发者可以平衡系统可靠性与响应延迟。结合幂等设计、断路器模式等最佳

weixin_33824385的博客 339

水星路由器+群晖NAS+外网访问

水星路由器+群晖NAS+外网访问 有了公网IP,家里是水星路由器的 怎么设置外网访问 1.先电脑登录群晖的http://【你的群晖名】.quickconnect.to/ 2.【控制面板】进入设置 3.选择synology依次设置 ,【主机名称】就是你的名称。注意:左下角第7步,一定勾选。 4.下面进入192.168.1.1水星路由器设置。【IP与MAC绑定】点击NAS对应的【绑定】注意,点这里之前Nas一定设置了固定内网ip(后面补充 )。 5.【高级用户】-虚【拟服务器】。常用服务器不用选,其他的对应

wjxue201的博客 2600

GLM-4.7-Flash与SpringBoot集成实战:构建智能问答系统

本文介绍了如何在星图GPU平台上自动化部署【ollama】GLM-4.7-Flash镜像,并构建智能问答系统。通过SpringBoot集成,该镜像能快速响应自然语言查询,适用于企业客服自动化、知识库问答等场景,显著提升服务效率与用户体验。

weixin_30674431的博客 835

wps js宏获取工作簿、工作表、单元格的值、链接转图片、单元格区域数据转二维数组、保存工作簿的常用Excel操作

wps js宏入门看这里,我写了常用的js操作Excel的方法:获取工作簿对象、获取工作表对象、获取单元格的值、链接转图片、生成条形码图片、单元格区域数据转二维数组方法、保存工作簿、弹出输入框、YesNo选择框,助你从VBA转战JS。

huaermeier的博客 3万+

【数据分析】基于多组学数据整合的微生物与宿主互作网络构建及可视化分析

综上所述,本教程通过详细的步骤和方法,展示了如何整合微生物群落数据、宿主基因表达数据和代谢组数据,构建微生物与宿主互作网络,并进行可视化分析。这种方法为研究微生物与宿主之间的复杂关系提供了一种有力的工具,有助于推动相关疾病的研究和治疗。然而,在实际应用中,需要注意数据处理和分析过程中的各种问题,并结合实验验证来提高研究结果的可靠性和科学性。未来,随着多组学技术的不断发展和完善,以及数据分析方法的创新,相信我们能够更深入地理解微生物与宿主之间的互作机制,为疾病的预防、诊断和治疗提供更有力的支持。

专注生信领域 370

Java开发者指南:SpringBoot集成EasyAnimateV5-7b-zh-InP视频生成API

本文介绍了如何在星图GPU平台上自动化部署EasyAnimateV5-7b-zh-InP/7B参数量图生视频模型,并将其集成到SpringBoot项目中。该模型能够根据输入图片快速生成高质量视频,可广泛应用于电商商品展示、社交媒体内容创作等自动化视频制作场景,显著提升内容生产效率。

weixin_42360733的博客 133

YOLOv11改进策略【模型轻量化】| 使用 MoblieOne 模块,引入结构重参数化,提高模型检测效率

本文记录的是基于MobileOne的YOLOv11骨干网络改进方法研究。MobileOne的基础模块通过重参数化,。在将其替换YOLOv11的骨干网络后,实现模型轻量化。

Limiiiing的博客 1426

Qwen2.5-VL-7B-Instruct在SpringBoot微服务中的集成应用

本文介绍了如何在星图GPU平台自动化部署【ollama】Qwen2.5-VL-7B-Instruct镜像,并将其集成到SpringBoot微服务中。该多模态模型能够理解图像和文本指令,典型应用于电商商品图片的自动化审核与内容分析,显著提升处理效率和智能化水平。

weixin_36151775的博客 306

AI游戏制作07——波次计时器,敌人按波次生成

AI游戏制作07——波次计时器,敌人按波次生成

qq_40572023的博客 48

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

将DeepSeek-R1-Distill-Qwen-7B集成到SpringBoot项目的完整方案

本文介绍了在星图GPU平台上自动化部署【ollama】DeepSeek-R1-Distill-Qwen-7B镜像,并将其集成到SpringBoot项目中的完整方案。该方案通过Ollama提供标准HTTP接口,使Java应用能够便捷调用大模型能力,典型应用场景包括构建智能客服系统,实现自动化的客户咨询与问答服务。

weixin_31800911的博客 92

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

RocketMQ从实战到源码:SpringBoot整合RocketMQ

本文介绍了SpringBoot与RocketMQ的整合实践,主要内容包括: 环境准备:详细说明了JDK、SpringBoot、RocketMQ及starter的版本兼容性要求,推荐使用稳定版本组合 依赖配置:引入rocketmq-spring-boot-starter依赖,简化整合过程 配置文件:提供了完整的RocketMQ配置示例,包含NameServer地址、生产者/消费者参数等 代码实现: 消息实体类定义 生产者实现三种消息发送方式(同步、异步、单向) 消费者通过注解自动监听消息 测试验证:构建测试类

qq_43843951的博客 582

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

Hunyuan-MT 7B与SpringBoot集成:企业级多语言API开发实战

本文介绍了如何在星图GPU平台上自动化部署Hunyuan-MT 7B全能翻译镜像,构建企业级多语言API服务。该方案支持33种语言互译,可集成至SpringBoot系统,实现电商商品描述、客服咨询等场景的实时智能翻译,提升企业国际化服务效率。

weixin_29301059的博客 160

Qwen3-ASR-1.7B与SpringBoot集成:企业级语音处理服务开发

本文介绍了如何在星图GPU平台上自动化部署Qwen3-ASR-1.7B镜像,快速构建企业级语音识别服务。该方案支持将语音实时转换为文字,可广泛应用于智能客服、在线教育等场景,显著提升语音处理效率与准确性。

weixin_42601547的博客 269

11Spring Boot 弹性并发:虚拟线程、结构化并发与声明式重试

Spring Boot弹性并发演进:虚拟线程+结构化并发+声明式重试 Spring Boot 4.0通过整合Java 21虚拟线程、结构化并发和声明式重试机制,构建了新一代弹性并发体系。虚拟线程解决了传统线程的内存瓶颈问题,使单个JVM可承载百万级并发;结构化并发(JEP 453)提供了优雅的并发任务管理能力;而声明式弹性注解(@Retryable等)则简化了容错策略实现。三者协同工作,使开发者能够轻松构建高吞吐、低延迟且具备弹性的分布式系统,特别适合金融支付等高并发场景。文章通过实际案例和架构图,展示了如

模界 | AI工程化实践者 440

RabbitMQ之消息生产消费

前面学习了RabbitMQ的使用场景和基础概念,现在我们来写一个RabbitMQ的使用案例,来正式学习一下。 这个入门案例分为两个部分,消息生产者和消息消费者。模拟用户注册场景,生产者将用户手机号发送到MQ,消费者监听MQ队列,获取用户手机号发送短信。 使用RabbitMQ客户端消息confirm和redis,确保消息正确发送到MQ,不会产生丢失; 结合消息过期(TTL)和死信队列(DLX)实现消...

qq_36300264的博客 4734
上一篇: Linux学习笔记(1)
Java杂货店
博客等级 码龄5年 9粉丝 3原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值