最近公司在搞活动,需要依赖一个第三方接口,测试阶段并没有什么异常状况,但上线后发现依赖的接口有时候会因为内部错误而返回系统异常,虽然概率不大,但总因为这个而报警总是不好的,何况死信队列的消息还需要麻烦运维进行重新投递,所以加上重试机制势在必行。
重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。让你原本就稳如狗的系统更是稳上加稳。
为了方便说明,先假设我们想要进行重试的方法如下:
@Slf4j @Component public class HelloService { private static AtomicLong helloTimes = new AtomicLong(); public String hello(){ long times = helloTimes.incrementAndGet(); if (times % 4 != 0){ log.warn("发生异常,time:{}", LocalTime.now() ); throw new HelloRetryException("发生Hello异常"); } return "hello"; } }调用处:
@Slf4j @Service public class HelloRetryService implements IHelloService{ @Autowired private HelloService helloService; public String hello(){ return helloService.hello(); } }也就是说,这个接口每调4次才会成功一次。
手动重试先来用最简单的方法,直接在调用的时候进重试:
// 手动重试 public String hello(){ int maxRetryTimes = 4; String s = ""; for (int retry = 1; retry <= maxRetryTimes; retry++) { try { s = helloService.hello(); log.info("helloService返回:{}", s); return s; } catch (HelloRetryException e) { log.info("helloService.hello() 调用失败,准备重试"); } } throw new HelloRetryException("重试次数耗尽"); }输出如下:
发生异常,time:10:17:21.079413300 helloService.hello() 调用失败,准备重试 发生异常,time:10:17:21.085861800 helloService.hello() 调用失败,准备重试 发生异常,time:10:17:21.085861800 helloService.hello() 调用失败,准备重试 helloService返回:hello service.helloRetry():hello程序在极短的时间内进行了4次重试,然后成功返回。
这样虽然看起来可以解决问题,但实践上,由于没有重试间隔,很可能当时依赖的服务尚未从网络异常中恢复过来,所以极有可能接下来的几次调用都是失败的。
而且,这样需要对代码进行大量的侵入式修改,显然,不优雅。
代理模式上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。
所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?
其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。
@Slf4j public class HelloRetryProxyService implements IHelloService{ @Autowired private HelloRetryService helloRetryService; @Override public String hello() { int maxRetryTimes = 4; String s = ""; for (int retry = 1; retry <= maxRetryTimes; retry++) { try { s = helloRetryService.hello(); log.info("helloRetryService 返回:{}", s); return s; } catch (HelloRetryException e) { log.info("helloRetryService.hello() 调用失败,准备重试"); } } throw new HelloRetryException("重试次数耗尽"); } }这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了,分工明确。比如,现在想要在重试之间加上一个延迟,只需要做一点点修改即可:
@Override public String hello() { int maxRetryTimes = 4; String s = ""; for (int retry = 1; retry <= maxRetryTimes; retry++) { try { s = helloRetryService.hello(); log.info("helloRetryService 返回:{}", s); return s; } catch (HelloRetryException e) { log.info("helloRetryService.hello() 调用失败,准备重试"); } // 延时一秒 try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } throw new HelloRetryException("重试次数耗尽"); }代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。如果每个类都写这么一长串类似的代码,显然,不优雅!
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("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); } }