随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。
在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为内部错误或者网络波动而出错或返回系统异常,因此我们必须考虑加上重试机制
重试机制 可以提高系统的健壮性,并且减少因网络波动依赖服务临时不可用带来的影响,让系统能更稳定的运行。
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("不再重试!");
- }
- }
- }
- }
- }
复制代码 运行上述代码:

上述代码看上去可以解决重试问题,但实际上存在一些弊端:
- 由于没有重试间隔,很可能远程调用的服务还没有从网络异常中恢复,所以有可能接下来的几次调用都会失败
- 代码侵入式太高,调用方代码不够优雅
- 项目中远程调用的服务可能有很多,每个都去添加重试会出现大量的重复代码
推荐一个开源免费的 Spring Boot 实战项目:
https://github.com/javastacks/spring-boot-best-practice
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 了- 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动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了
4. CGLib 动态代理
既然已经说到了 JDK 动态代理,那就不得不提 CGLib 动态代理了。使用 JDK 动态代理对被代理的类有要求,不是所有的类都能被代理,而 CGLib 动态代理则刚好解决了这个问题- @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 动态代理带来的缺陷。优雅指数上涨了不少。
但这个方案仍旧存在一个问题,那就是需要对原来的逻辑进行侵入式修改,在每个被代理实例被调用的地方都需要进行调整,这样仍然会对原有代码带来较多修改
5. 手动 Aop
考虑到以后可能会有很多的方法也需要重试功能,咱们可以将重试这个共性功能通过 AOP 来实现,使用 AOP 来为目标调用设置切面,即可在目标方法调用前后添加一些重试的逻辑- <dependency>
- <groupId>org.aspectj</groupId>
- <artifactId>aspectjweaver</artifactId>
- </dependency>
复制代码 自定义注解:
[code]@Documented@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface MyRetryable { // 最大重试次数 int retryTimes() default 3; // 重试间隔 int retryInterval() default 1;}@Slf4j@Aspect@Componentpublic 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 |