一步一步实现优雅重试

超、凢脫俗 2023-05-21 07:23 108阅读 0赞

重试的作用

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

远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1、timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。对于开发过网络应用程序的程序员来说,重试并不陌生,由于网络的拥堵和波动,此刻不能访问服务的请求,也许过一小段时间就可以正常访问了。

对于RPC调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。

重试的常见实现方式

例如写实现一个推送用户姓名到控制台的功能。这个服务如果调用失败了,需要加上重试逻辑。

简单粗暴

互联网注重效率,最简单的重试写法如下:

  1. public interface UserService {
  2. /** * 发送用户姓名到控制台 * @param userName 用户姓名 * @return 发送是否成功 */
  3. boolean sendUserName2Console(String userName);
  4. }
  5. public class UserServiceImpl implements UserService {
  6. public static final int RETRY_MAX_TIMES = 5;
  7. /** * 这里用System.out模拟RPC服务,如果传入字符串"abc"就返回false(失败),否则就成功 * @param userName * @return */
  8. @Override
  9. public boolean sendUserName2Console(String userName) {
  10. if ("abc".equals(userName)) {
  11. System.out.println("Please Retry!");
  12. return false;
  13. }
  14. System.out.println("Hello, " + userName);
  15. return true;
  16. }
  17. /** * @param userName * @return */
  18. public boolean sendUserName2ConsoleWithRetry(String userName) {
  19. int invokeCount = 0;
  20. while(invokeCount < RETRY_MAX_TIMES) {
  21. if (this.sendUserName2Console(userName)) {
  22. return true;
  23. }
  24. invokeCount++;
  25. }
  26. return false;
  27. }
  28. }

缺点:直接把重试写在服务实现方法体中,不易于维护。

代理模式

使用代理模式去优化上面的写法,为其他对象提供一种代理以控制对这个对象的访问。代理对象可以在客户端和目标对象之间起到中介作用。

  1. public class UserServiceProxyImpl implements UserService {
  2. public static final int RETRY_MAX_TIMES = 5;
  3. private UserService userService;
  4. public UserServiceProxyImpl(UserService userService) {
  5. this.userService = userService;
  6. }
  7. /** * 代理模式 * @param userName * @return */
  8. @Override
  9. public boolean sendUserName2Console(String userName) {
  10. int invokeCount = 0;
  11. while(invokeCount < RETRY_MAX_TIMES) {
  12. if (userService.sendUserName2Console(userName)) {
  13. return true;
  14. }
  15. invokeCount++;
  16. }
  17. return false;
  18. }
  19. }

缺点:如果UserService接口中还有其他方法要加入重试机制或者还有另一个服务也要求有重试机制,就得继续写代理方法或再写一个新的代理类,代理类越写越多

动态代理

  1. public class JdkDynamicProxy implements InvocationHandler {
  2. private final Object target;
  3. public static final int RETRY_MAX_TIMES = 5;
  4. public JdkDynamicProxy(Object target) {
  5. this.target = target;
  6. }
  7. /** * 动态代理模式,被代理类的方法调用都从invoke()方法进入,在这里加上重试逻辑 * @return */
  8. @Override
  9. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  10. int invokeCount = 0;
  11. Object result = null;
  12. System.out.println("JdkDynamic proxy invoke");
  13. while(invokeCount < RETRY_MAX_TIMES) {
  14. result = method.invoke(target, args);
  15. if (result != null && Boolean.TRUE.equals(result)) {
  16. break;
  17. }
  18. invokeCount++;
  19. }
  20. return result;
  21. }
  22. @SuppressWarnings(value = "unchecked")
  23. public <T> T getProxy() {
  24. return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(),
  25. target.getClass().getInterfaces(), this);
  26. }

测试代码

  1. @Test
  2. public void test() {
  3. //版本三 测试
  4. System.out.println("--------版本三:重试逻辑写在动态代理类-----");
  5. UserService jdkProxy = (UserService)new JdkDynamicProxy(userService).getProxy();
  6. jdkProxy.sendUserName2Console("abc");
  7. }

测试结果
在这里插入图片描述

缺点:有的类没有实现接口,怎么代理?(其实就是JDK动态代理的缺点)

动态代理增强

Cglib是一个强大的、高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口,它广泛地被许多AOP及数据访问框架使用来生成动态代理对象,为他们提供方法的拦截。
在这里插入图片描述
对此图总结一下:

  • 最底层的是字节码Bytecode,字节码是Java为了保证“一次编译、到处运行”而产生的一种虚拟指令格式
  • 位于字节码之上的是ASM,这是一种直接操作字节码的框架,应用ASM需要对Java字节码、Class结构比较熟悉
  • 位于ASM之上的是CGLIB、Groovy、BeanShell,后两种并不是Java体系中的内容而是脚本语言,它们通过ASM框架生成字节码变相执行Java代码,这说明在JVM中执行程序并不一定非要写Java代码,只要你能生成Java字节码,JVM并不关心字节码的来源,当然通过Java代码生成的JVM字节码是通过编译器直接生成的,算是最“正统”的JVM字节码
  • 位于CGLIB、Groovy、BeanShell之上的就是Hibernate、Spring AOP这些框架了
  • 最上层的是Applications,即具体应用,一般都是一个Web项目或者本地跑一个程序

使用Cglib实现的动态代理代码如下

  1. import java.lang.reflect.Method;
  2. import org.springframework.cglib.proxy.Enhancer;
  3. import org.springframework.cglib.proxy.MethodInterceptor;
  4. import org.springframework.cglib.proxy.MethodProxy;
  5. public class CglibDynamicProxy implements MethodInterceptor {
  6. public static final int RETRY_MAX_TIMES = 5;
  7. /** * CGlib增强动态代理模式 * @return */
  8. @Override
  9. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  10. int invokeCount = 0;
  11. Object result = null;
  12. System.out.println("CglibDynamic proxy invoke");
  13. while(invokeCount < RETRY_MAX_TIMES) {
  14. result = methodProxy.invokeSuper(o, objects);
  15. if (result != null && Boolean.TRUE.equals(result)) {
  16. break;
  17. }
  18. invokeCount++;
  19. }
  20. return result;
  21. }
  22. @SuppressWarnings(value = "unchecked")
  23. public <T> T getProxy(Class clazz) {
  24. Enhancer enhancer = new Enhancer();
  25. enhancer.setSuperclass(clazz);
  26. enhancer.setCallback(this);
  27. return (T)enhancer.create();
  28. }
  29. }

测试代码

  1. @Test
  2. public void test() {
  3. //版本四 测试
  4. System.out.println("-------版本四:重试逻辑写在Cglib增强动态代理类中,实现类不感知重试-----");
  5. UserService cglibProxy = (UserService)new CglibDynamicProxy().getProxy(UserServiceImpl.class);
  6. cglibProxy.sendUserName2Console("abc");
  7. }

测试结果
在这里插入图片描述

缺点:不同的服务,重试的次数应该是不同的,因为服务对稳定性的要求各不相同,代理模式无法定制重试次数

模板方法模式

使用抽象模板类,在模板类中写重试逻辑,重试次数可以交给模板的子类指定。

  1. public abstract class RetryTemplate {
  2. //默认的重试次数
  3. private static final int DEFAULT_RETRY_TIME = 3;
  4. //重试次数
  5. private int retryTime = DEFAULT_RETRY_TIME;
  6. //重试的睡眠时间
  7. private int sleepTime = 0;
  8. public RetryTemplate setSleepTime(int sleepTime) {
  9. if(sleepTime <= 0) {
  10. throw new IllegalArgumentException("sleepTime should be bigger than 0");
  11. }
  12. this.sleepTime = sleepTime;
  13. return this;
  14. }
  15. public RetryTemplate setRetryTime(int retryTime) {
  16. if (retryTime <= 0) {
  17. throw new IllegalArgumentException("retryTime should be bigger than 0");
  18. }
  19. this.retryTime = retryTime;
  20. return this;
  21. }
  22. /** * 重试的业务执行代码 * 失败时请抛出一个异常 * * @return */
  23. protected abstract Object doBiz() throws Exception;
  24. public Object execute() throws InterruptedException {
  25. for (int i = 0; i < retryTime; i++) {
  26. try {
  27. return doBiz();
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. Thread.sleep(sleepTime);
  31. }
  32. }
  33. return null;
  34. }
  35. public Object submit(ExecutorService executorService) {
  36. if (executorService == null) {
  37. throw new IllegalArgumentException("please choose executorService!");
  38. }
  39. return executorService.submit((Callable) () -> execute());
  40. }
  41. }

测试代码

  1. public class RetryTemplateTest {
  2. public static void main(String[] args) throws Exception {
  3. Object ans = new RetryTemplate() {
  4. @Override
  5. protected Object doBiz() throws Exception {
  6. int temp = (int) (Math.random() * 10);
  7. System.out.println(temp);
  8. if (temp > 3) {
  9. throw new Exception("generate value bigger then 3! need retry");
  10. }
  11. return temp;
  12. }
  13. }.setRetryTime(5).setSleepTime(100).execute();
  14. System.out.println(ans);
  15. }
  16. }

测试结果
在这里插入图片描述

缺点:对业务代码有入侵,强迫对方继承模板类

注解 + AOP

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(value = { ElementType.TYPE, ElementType.METHOD})
  3. @Documented
  4. public @interface Retryable {
  5. /** * 重试条件,默认方法返回false就重试 * @return */
  6. boolean retryIfResult() default false;
  7. /** * 最大重试次数 * @return */
  8. int maxAttemptTimes() default 3;
  9. }
  10. @Component
  11. public class RetryAbleService {
  12. /** * 用注解 + AOP的形式,每个服务都可以自定义重试次数及重试条件 * 这里写死返回值为false,查看重试效果 */
  13. @Retryable(maxAttemptTimes = 5, retryIfResult = false)
  14. public boolean retryAbleAnnotatedMethod() {
  15. System.out.println("retryAbleAnnotatedMethod test");
  16. return false;
  17. }
  18. }

切面编写

  1. @Aspect
  2. @Component
  3. public class RetryAspect {
  4. /** * 匹配所有带Retrayable注解修饰的方法 */
  5. @Pointcut(value = "@annotation(com.cainiao.dms.platform.web.controller.aspect.Retryable)")
  6. public void retryPointCut() {
  7. }
  8. /** * @param joinPoint * @return * @throws Throwable */
  9. @Around(value = "retryPointCut()")
  10. public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
  11. Object[] args = joinPoint.getArgs();
  12. //Retryable retryable = joinPoint.getTarget().getClass().getAnnotation(Retryable.class);
  13. Method method = getCurrentMethod(joinPoint);
  14. Retryable retryable = method.getAnnotation(Retryable.class);
  15. // 获取最大重试次数
  16. int maxAttemptTimes = retryable.maxAttemptTimes();
  17. if (maxAttemptTimes <= 1) {
  18. return joinPoint.proceed(args);
  19. }
  20. int retryCount = 0;
  21. //得到什么结果才retry
  22. boolean retryIfResult = retryable.retryIfResult();
  23. while (retryCount < maxAttemptTimes) {
  24. boolean result = (boolean)joinPoint.proceed(args);
  25. //得到什么结果才retry
  26. if (!(retryIfResult == result)) {
  27. return result;
  28. }
  29. retryCount++;
  30. }
  31. return joinPoint.proceed(args);
  32. }
  33. private Method getCurrentMethod(ProceedingJoinPoint joinPoint) {
  34. try {
  35. Signature sig = joinPoint.getSignature();
  36. MethodSignature msig = (MethodSignature)sig;
  37. Object target = joinPoint.getTarget();
  38. return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
  39. } catch (NoSuchMethodException e) {
  40. throw new RuntimeException(e);
  41. }
  42. }
  43. }

测试代码

  1. @Test
  2. public void test() {
  3. //版本五 用注解 + AOP的形式,每个方法都可以自定义重试次数及重试条件
  4. System.out.println("--------版本五:用注解 + AOP的形式,每个方法都可以自定义重试次数及重试条件-----");
  5. retryAbleService.retryAbleAnnotatedMethod();
  6. }

测试结果
在这里插入图片描述

采用切面的方式比较清晰,在需要添加重试的方法上添加一个用于重试的自定义注解,然后在切面中实现重试的逻辑,主要的配置参数则根据注解中的选项来初始化。
优点:真正的无侵入
缺点:某些方法无法被切面拦截的场景无法覆盖,例如spring-aop无法切私有方法,final方法。直接使用aspecJ则有些小复杂;如果用spring-aop,则只能切被Spring容器管理的bean

guava-retrying

优雅的重试机制要具备的几点特征
无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用

Guava Retrying是谷歌开发的一个灵活方便的重试组件,包含了多种的重试策略,而且扩展起来非常容易。Guava retrying在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。Guava retrying也是线程安全的,入口调用逻辑采用的是Java.util.concurrent.Callable的call方法。

使用Guava retrying很简单,引入google的guava包

  1. <dependency>
  2. <groupId>com.github.rholder</groupId>
  3. <artifactId>guava-retrying</artifactId>
  4. <version>2.0.0</version>
  5. </dependency>

测试代码

  1. @Test
  2. public void test() {
  3. Callable<Boolean> task = new Callable<Boolean>() {
  4. @Override
  5. public Boolean call() throws Exception {
  6. System.out.println("Callable Task test");
  7. return false;
  8. }
  9. };
  10. System.out.println("--------版本六:用guava retryer优雅重试-----");
  11. //版本六 用guava retryer优雅重试
  12. Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
  13. .retryIfResult(Boolean.FALSE::equals)
  14. .retryIfException()
  15. //WaitStrategies.fixedWait(1, TimeUnit.SECONDS)间隔固定时间1秒之后重试
  16. //WaitStrategies.randomWait(3, TimeUnit.SECONDS)间隔随机时间后重试,比如间隔0~3中随机时间后重试。
  17. //WaitStrategies.randomWait(2, TimeUnit.SECONDS, 5, TimeUnit.SECONDS)最小值,最大值之间的随机时间。
  18. //WaitStrategies.incrementingWait增量重试,重试的次数越多,等待时间间隔越长。
  19. //WaitStrategies.fibonacciWait(100, 2, TimeUnit.SECONDS)用斐波那契数列来计算等待时间,而不是指数函数
  20. .withWaitStrategy(WaitStrategies.incrementingWait(200, TimeUnit.MILLISECONDS,500,TimeUnit.MILLISECONDS))
  21. //只重试10次
  22. .withStopStrategy(StopStrategies.stopAfterAttempt(6))
  23. .withRetryListener(new RetryListener() {
  24. @Override
  25. public <V> void onRetry(Attempt<V> attempt) {
  26. System.out.println(String.format("第【%s】次调用失败" , attempt.getAttemptNumber()));
  27. }
  28. }).build();
  29. //递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
  30. try {
  31. Boolean result = retryer.call(task);
  32. } catch (Exception e) {
  33. e.printStackTrace();
  34. }
  35. }

测试结果
在这里插入图片描述

发表评论

表情:
评论列表 (有 0 条评论,108人围观)

还没有评论,来说两句吧...

相关阅读

    相关 实现优雅

    重试的作用 对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。 远程调用超时、网络突然中断可以重试。在微服务

    相关 实现SpringIoc容器

    IoC(Inverse of Controll控制反转):指的是对象的创建方式进行了反转,传统的开发方式是程序员自己 new 对象,IoC就是将这一过程进行了反转,程序员不需要

    相关 地配置Spring

    本文旨在从一个空工程一步一步地配置Spring,空工程见上一篇文章[创建Maven父子工程][Maven]。 \\一、spring基本配置 \\\1. 添加spring依赖