spring全家桶

java中常见重试(retry)方案 (含AOP)

2020-01-17  本文已影响0人  suxin1932
别忘记 异步程序中的重试方案设计

2.重试的几种解决方案

对于重试是有场景限制的,不是什么场景都适合重试,
比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。

比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。
几种重试实现.png

2.1 原生代码侵入性实现重试

package com.zy.eureka.retry.v1;

public interface IEmployeeService {
    String getName(Long id);
}
package com.zy.eureka.retry.v1;

import com.zy.eureka.retry.RpcService;
import org.springframework.stereotype.Service;

@Service
public class EmployeeServiceImplRetryV1 implements IEmployeeService {
    private static final int RETRY_TIMES = 3;
    @Override
    public String getName(Long id) {
        int times = 0;
        while (times < RETRY_TIMES) {
            try {
                return RpcService.getInstance().getName(id);
            } catch (Exception e) {
                times++;
                System.out.println("times ------------> " + times);
                if (times >= RETRY_TIMES) {
                    throw new RuntimeException(e);
                }
            }
        }
        return null;
    }
}

2.2 jdk动态代理实现

当业务中需要重试的方法越来越多时, 则需要抽取, 可采用动态代理
package com.zy.eureka.retry.v2;

public interface ITeacherService {
    void teach(String subjectName);
}
package com.zy.eureka.retry.v2;

import com.zy.eureka.retry.RpcService;

public class TeacherServiceImplRetryV2 implements ITeacherService {
    @Override
    public void teach(String subjectName) {
        RpcService.getInstance().teach(subjectName);
    }
}
package com.zy.eureka.retry.v2;

import lombok.AllArgsConstructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * 基于 jdk 动态代理实现重试功能, 适用于有接口的业务
 */
@AllArgsConstructor
public class JdkProxy implements InvocationHandler {
    private final Object target;

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;
        while (times < 3) {
            try {
                return method.invoke(target, args);
            } catch (Throwable e) {
                times ++;
                System.out.println("times >>>>>>>>> " + times);
                if (times >= 3) {
                    throw new RuntimeException(e);
                }
            }
        }
        return null;
    }

    /**
     * 获取动态代理对象
     * @param realObj 真实对象
     * @return
     */
    public static Object getProxy(Object realObj) {
        InvocationHandler handler = new JdkProxy(realObj);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realObj.getClass().getInterfaces(), handler);
    }
}

测试加参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

jdk proxy test.png jdk proxy 生成的代理类.png

2.3 cglib动态代理实现

如果被代理的方法没有实现接口, 则必须要采用 cglib 动态代理了
package com.zy.eureka.retry.v3;

import com.zy.eureka.retry.RpcService;

public class ProgrammerServiceImpl {
    public void program(String language) {
        RpcService.getInstance().program(language);
    }
}
package com.zy.eureka.retry.v3;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;

public class CglibProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        int times = 0;
        while (times < 3) {
            try {
                //通过代理子类调用父类的方法
                return methodProxy.invokeSuper(object, args);
            } catch (Throwable e) {
                times++;
                System.out.println("times >>>>>>>>> " + times);
                if (times >= 3) {
                    throw new RuntimeException(e);
                }
            }
        }
        return null;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> tClass) {
        return (T) Enhancer.create(tClass, this);
    }
}

测试

测试时, 可在测试类(如下文的MyServiceImplTest)中, 加入静态代码块即可
static{
    System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "F:\\demos\\spring-cloud-dubbo\\spring-cloud-dubbo-eureka");
}
cglib生成的类分析.png
// 需要说明的是:
本例中用的是 net.sf.cglib 包下的依赖, 会生成 3 个文件.
若是采用 Spring 来间接实现, 则只有生成 1 个代理 文件.

// 调用过程:
代理对象调用 this.program 方法 -> 
调用拦截器 -> 
methodProxy.invokeSuper -> 
CGLIB$program$0 -> 
被代理对象program 方法

而我们在自定义的 com.zy.eureka.retry.v3.CglibProxy#intercept 中调用了
net.sf.cglib.proxy.MethodProxy#invokeSuper 方法.

// net.sf.cglib.proxy.MethodProxy
// 其静态内部类: net.sf.cglib.proxy.MethodProxy.FastClassInfo
private static class FastClassInfo {
    FastClass f1; // 被代理类FastClass
    FastClass f2; // 代理类FastClass
    int i1; // 被代理类的方法签名(index)
    int i2; // 代理类的方法签名

    private FastClassInfo() {
    }
}
上面代码调用过程就是获取到代理类对应的FastClass,并执行了代理方法。
FastClass并不是跟代理类一块生成的,而是在第一次执行MethodProxy invoke/invokeSuper时生成的并放在了缓存中。

// FastClass机制
Cglib动态代理执行代理方法效率之所以比JDK的高是因为Cglib采用了FastClass机制,
它的原理简单来说就是:
为代理类和被代理类各生成一个Class,这个Class会为代理类或被代理类的方法分配一个index(int类型)。
这个index当做一个入参,FastClass就可以直接定位要调用的方法直接进行调用,
这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。

总结一下JDK动态代理和Gglib动态代理的区别

1.JDK动态代理是实现了被代理对象的接口,Cglib是继承了被代理对象。
2.JDK和Cglib都是在运行期生成字节码,JDK是直接写Class字节码,
Cglib使用ASM框架写Class字节码,Cglib代理实现更复杂,生成代理类比JDK效率低。
3.JDK调用代理方法,是通过反射机制调用,
Cglib是通过FastClass机制直接调用方法,Cglib执行效率更高。

2.4 利用AOP自定义注解简化开发

package com.zy.eureka.retry.v4;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RetryAnno {
    /**
     * 重试次数
     *
     * @return
     */
    int times() default 0;

    /**
     * 两次重试之间的间隔时间, 单位: ms
     * @return
     */
    long internal() default 100L;
}
package com.zy.eureka.retry.v4;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Component
@Aspect
@Order(Ordered.LOWEST_PRECEDENCE - 3)
public class RetryAspect {

    @Around(value = "@annotation(retryAnno)")
    public Object process(ProceedingJoinPoint point, RetryAnno retryAnno) throws Throwable {
        RetryAnno retry = Objects.nonNull(retryAnno) ? retryAnno : point.getTarget().getClass().getAnnotation(RetryAnno.class);
        if (Objects.isNull(retry)) {
            return point.proceed();
        }
        int times = retry.times();
        if (times <= 1) {
            return point.proceed();
        }
        int i = 0;
        while (i < times) {
            try {
                return point.proceed();
            } catch (Throwable e) {
                i++;
                System.out.println(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()) + " retry times is ======>>> " + i);
                if (i >= times) {
                    throw new RuntimeException(e);
                }
                TimeUnit.MILLISECONDS.sleep(retry.internal());
            }
        }
        return null;
    }
}
package com.zy.eureka.retry.v4;

import com.zy.eureka.retry.RpcService;
import org.springframework.stereotype.Service;

@Service
public class CommentServiceImpl {
    @RetryAnno(times = 3, internal = 2000)
    public void comment(String foodName) {
        RpcService.getInstance().comment(foodName);
    }
}

2.5 基于 spring-retry 实现

pom.xml

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <!-- 如果是 spring-boot项目, 这里版本号可省略, 会自动适配版本号 -->
   <!-- <version>1.2.5.RELEASE</version>-->
</dependency>
package com.zy.eureka.retry.v5;

import com.zy.eureka.retry.RpcService;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class NearbyServiceImpl {

    /**
     * 重试
     * @param address
     * @return
     */
    @Retryable(maxAttempts = 3, value = NullPointerException.class, backoff = @Backoff(delay = 2000L, multiplier = 2))
    public boolean nearby(String address) {
        System.out.println("根据这里调了几次, 可以看到重试了多少次...||||||||||||| >>>>>>>>>>> ||||||||||||||");
        return RpcService.getInstance().nearby(address);
    }

    /**
     * 降级机制: 如果最终仍然失败, 将会调用这里的方法
     * @param e
     * @return
     */
    @Recover
    public boolean degreeNearby(NullPointerException e) {
        return false;
    }
}

--------------------------------2.1--2.5的公共部分--------------------------------

package com.zy.eureka.retry;

import java.util.Objects;

public class RpcService {
    private static final RpcService RPC_SERVICE = new RpcService();

    public static RpcService getInstance() {
        return RPC_SERVICE;
    }

    /**
     * 配合测试 v1 版本的重试: 原始的代码侵入性高的
     *
     * @param id
     * @return
     */
    public String getName(Long id) {
        throw new RuntimeException(String.format("failed to get employee's %s name...", id));
        // return String.format("%s->%s", id, UUID.randomUUID().toString());
    }

    /**
     * 配合测试 v2 版本的重试: 基于 jdk 动态代理
     *
     * @param subjectName
     */
    public void teach(String subjectName) {
        throw new RuntimeException(String.format("failed to teach subject: %s ...", subjectName));
    }

    /**
     * 配合测试 v3版本的重试: 基于 cglib 动态代理
     *
     * @param language
     */
    public void program(String language) {
        throw new RuntimeException(String.format("%s programmer failed to code ...", language));
    }

    /**
     * 配合测试 v4 版本的重试: 基于 AOP 实现, 简化开发
     *
     * @param foodName
     */
    public void comment(String foodName) {
        throw new RuntimeException(String.format("failed to comment %s ...", foodName));
    }

    /**
     * 配合测试 v5 版本的重试: 基于 spring-retry 框架实现
     *
     * @param address
     * @return
     */
    public boolean nearby(String address) {
        if (Objects.isNull(address)) {
            throw new NullPointerException(String.format("failed to judge empty address is nearby, ...", address));
        }
        throw new RuntimeException(String.format("failed to judge address %s is nearby ...", address));
    }
}

测试类

package com.zy.eureka.limit;

import com.zy.eureka.retry.v1.IEmployeeService;
import com.zy.eureka.retry.v2.ITeacherService;
import com.zy.eureka.retry.v2.JdkProxy;
import com.zy.eureka.retry.v2.TeacherServiceImplRetryV2;
import com.zy.eureka.retry.v3.CglibProxy;
import com.zy.eureka.retry.v3.ProgrammerServiceImpl;
import com.zy.eureka.retry.v4.CommentServiceImpl;
import com.zy.eureka.retry.v5.NearbyServiceImpl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
@EnableRetry
public class MyServiceImplTest {
    @Autowired
    private IEmployeeService employeeService;
    @Autowired
    private CommentServiceImpl commentService;
    @Autowired
    private NearbyServiceImpl nearbyService;
    /**
     * 测试 v1 版本的重试: 原始的代码侵入性高的
     * @return
     */
    @Test
    public void fn01() {
        System.out.println(employeeService.getName(1L));
    }
    /**
     * 测试 v2 版本的重试: 基于 jdk 动态代理
     */
    @Test
    public void fn02() {
        ITeacherService proxyTeacherService = (ITeacherService) JdkProxy.getProxy(new TeacherServiceImplRetryV2());
        proxyTeacherService.teach("english");
    }
    /**
     * 测试 v3版本的重试: 基于 cglib 动态代理
     */
    @Test
    public void fn03() {
        ProgrammerServiceImpl programmerService = new CglibProxy().getProxy(ProgrammerServiceImpl.class);
        programmerService.program("php");
    }
    /**
     * 测试 v4 版本的重试: 基于 AOP 实现, 简化开发
     */
    @Test
    public void fn04() {
        commentService.comment("banana");
    }
    /**
     * 测试 v5 版本的重试: 基于 spring-retry 框架实现
     */
    @Test
    public void fn05() {
        System.out.println("-----------------------------");
        System.out.println(nearbyService.nearby(null));
        System.out.println("-----------------------------");
    }
}

参考资源
https://houbb.github.io/2018/08/08/retry (全面的总结)
https://blog.csdn.net/u011116672/article/details/77823867
https://blog.csdn.net/liuxiao723846/article/details/78866879
https://www.cnblogs.com/monkey0307/p/8328821.html (cglib动态代理实现原理)

上一篇 下一篇

猜你喜欢

热点阅读