@Service public class OrderServiceImpl implements OrderService { public void addOrder() { int times = 1; while (times <= 5) { try { // 堆代码 duidaima.com // 故意抛异常 int i = 3 / 0; // addOrder } catch (Exception e) { System.out.println("重试" + times + "次"); Thread.sleep(2000); times++; if (times > 5) { throw new RuntimeException("不再重试!"); } } } } }运行上述代码:
@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("不再重试!"); } } } } }这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了。
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 = 1; while (times <= 5) { try { // 故意抛异常 int i = 3 / 0; return method.invoke(subject, args); } catch (Exception e) { System.out.println("重试【" + times + "】次"); try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); } times++; if (times > 5) { throw new RuntimeException("不再重试!"); } } } return null; } public static Object getProxy(Object realSubject) { InvocationHandler handler = new RetryInvocationHandler(realSubject); return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler); } }测试:
@RestController @RequestMapping("/order") public class OrderController { @Qualifier("orderServiceImpl") @Autowired private OrderService orderService; @GetMapping("/addOrder") public String addOrder() { OrderService orderServiceProxy = (OrderService)RetryInvocationHandler.getProxy(orderService); orderServiceProxy.addOrder(); return "addOrder"; } }动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了
@Component public class CGLibRetryProxyHandler implements MethodInterceptor { private Object target; @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { int times = 1; while (times <= 5) { try { // 故意抛异常 int i = 3 / 0; return method.invoke(target, objects); } catch (Exception e) { System.out.println("重试【" + times + "】次"); try { Thread.sleep(2000); } catch (InterruptedException ex) { ex.printStackTrace(); } times++; if (times > 5) { throw new RuntimeException("不再重试!"); } } } 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; } }测试:
@GetMapping("/addOrder") public String addOrder() { OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService); orderServiceProxy.addOrder(); return "addOrder"; }这样就很棒了,完美的解决了 JDK 动态代理带来的缺陷。优雅指数上涨了不少。但这个方案仍旧存在一个问题,那就是需要对原来的逻辑进行侵入式修改,在每个被代理实例被调用的地方都需要进行调整,这样仍然会对原有代码带来较多修改
<dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>自定义注解:
@Documented @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MyRetryable { // 最大重试次数 int retryTimes() default 3; // 重试间隔 int retryInterval() default 1; } @Slf4j @Aspect @Component public class RetryAspect { @Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)") private void retryMethodCall(){} @Around("retryMethodCall()") public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException { // 获取重试次数和重试间隔 MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.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 * 1000L); } throw new RuntimeException("重试次数耗尽", error); } }给需要重试的方法添加注解 @MyRetryable:
@Service public class OrderServiceImpl implements OrderService { @Override @MyRetryable(retryTimes = 5, retryInterval = 2) public void addOrder() { int i = 3 / 0; // addOrder } }这样即不用编写重复代码,实现上也比较优雅了:一个注解就实现重试。
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> </dependency>开启重试功能:在启动类或者配置类上添加 @EnableRetry 注解。
@Slf4j @Service public class OrderServiceImpl implements OrderService { @Override @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2)) public void addOrder() { System.out.println("重试..."); int i = 3 / 0; // addOrder } @Recover public void recover(RuntimeException e) { log.error("达到最大重试次数", e); } }该方法调用后会进行重试,最大重试次数为 3,第一次重试间隔为 2s,之后以 2 倍大小进行递增,第二次重试间隔为 4 s,第三次为 8s 。
@Retryable @Backoff @Recover查看 @Retryable 注解源码:指定异常重试、次数
public @interface Retryable { // 设置重试拦截器的 bean 名称 String interceptor() default ""; // 只对特定类型的异常进行重试。默认:所有异常 Class<? extends Throwable>[] value() default {}; // 包含或者排除哪些异常进行重试 Class<? extends Throwable>[] include() default {}; Class<? extends Throwable>[] exclude() default {}; // l设置该重试的唯一标志,用于统计输出 String label() default ""; boolean stateful() default false; // 最大重试次数,默认为 3 次 int maxAttempts() default 3; String maxAttemptsExpression() default ""; // 设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数 Backoff backoff() default @Backoff; // 异常表达式,在抛出异常后执行,以判断后续是否进行重试 String exceptionExpression() default ""; String[] listeners() default {}; }@Backoff 注解: 指定重试回退策略(如果因为网络波动导致调用失败,立即重试可能还是会失败,最优选择是等待一小会儿再重试。决定等待多久之后再重试的方法。通俗的说,就是每次重试是立即重试还是等待一段时间后重试)。
@Retryable public String hello() { long current = count.incrementAndGet(); System.out.println("第" + current +"次被调用"); if (current % 3 != 0) { log.warn("调用失败"); return "error"; } return "success"; }因此就算在方法上添加 @Retryable,也无法实现失败重试。除了使用注解外,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; } ); }此时唯一的好处是可以设置多种重试策略:
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
<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实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制:
retryIfResult():对不符合预期的返回结果进行重试
withBlockStrategy():设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情