Подробное объяснение процесса автоматической инициализации базы данных при первом запуске Spring Boot.

Поскольку бизнес-функции в проектах развития Интернета становятся все более и более сложными, существуют некоторые базовые сервисы, которые мы неизбежно будем называть сторонними интерфейсами или сервисами, предоставляемыми другими проектами внутри компании, но надежность удаленных сервисов и стабильность сети неконтролируемый фактор.

На этапе тестирования отклонений может не быть, но после выхода в Интернет вызываемый интерфейс может ошибаться или возвращать системное исключение из-за внутренних ошибок или колебаний сети, поэтому мы должны рассмотреть возможность добавления механизма повторных попыток.

Механизм повторных попыток может повысить надежность системы и уменьшить влияние временной недоступности зависимых служб из-за колебаний сети, позволяя системе работать более стабильно.

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("不再重试!");
             }
         }
     }
 }
}

Запустите приведенный выше код:

картина

Кажется, что приведенный выше код решает проблему повторной попытки, но на самом деле у него есть некоторые недостатки:

  1. Поскольку интервал повторных попыток отсутствует, вполне вероятно, что удаленно вызванная служба не восстановилась после сетевого исключения, поэтому возможно, что следующие несколько вызовов завершатся неудачно.
  2. Код слишком навязчив, а код вызывающей стороны недостаточно элегантен.
  3. В проекте может быть много удаленно вызываемых служб, и добавление повторных попыток для каждой из них приведет к появлению большого количества дублированного кода.

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

В это время на сцену выходит динамический прокси. Вам нужно только написать класс обработки прокси и все будет ок

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. Ручной АОП

Учитывая, что в будущем может появиться множество методов, которым также потребуется функция повтора, мы можем реализовать общую функцию повтора через АОП: используйте АОП для установки аспектов для целевого вызова, и вы можете добавить некоторую логику повтора до и после целевого вызова. вызов метода.

<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
    }
    
}

Таким образом, нет необходимости писать повторяющийся код, а реализация более элегантна: одна аннотация может реализовать повтор.

6. весенняя повторная попытка

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

@EnableRetryВключите функцию повтора: добавьте аннотации к классу запуска или классу конфигурации.

@RetryableДобавьте аннотации к методам, которые необходимо повторить.

@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. Первый интервал повтора — 2 с, затем он увеличивается в 2 раза. Второй интервал повтора — 4 с, а третий интервал повтора — это 8 с.

Механизм повтора Spring также поддерживает множество полезных функций, которые дополняются тремя аннотациями:

@Повторная попытка
@Откат
@Восстановление

Просмотрите @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 注解: Укажите резервную стратегию повторной попытки (если вызов не удается из-за колебаний сети, немедленная повторная попытка все равно может оказаться неудачной. Лучший вариант — подождать некоторое время, прежде чем повторить попытку. Метод, позволяющий решить, как долго ждать перед повторной попыткой. Для непрофессионала. условия, то есть следует ли повторять попытку немедленно или ждать некоторое время перед каждой повторной попыткой)

@Recover 注解: Выполнить последующие действия: Когда повторные попытки достигнут заданного количества раз, будет вызван указанный метод для выполнения таких операций, как ведение журнала.

Уведомление:

Метод, отмеченный аннотацией @Recover, должен принадлежать к тому же классу, что и метод, отмеченный @Retryable. Тип
исключения, создаваемого методом retry, должен соответствовать типу параметра метода восстановления().
Возвращаемое значение метода восстановления () должен совпадать с возвращаемым значением метода retry. Обеспечьте согласованность
. Исключение больше не может быть выброшено в методе Recovery(), в противном случае будет сообщено об ошибке, которая не может распознать исключение.

Еще один момент, о котором здесь следует напомнить, заключается в том, что, поскольку Spring Retry использует расширение Aspect, при использовании Aspect возникнет неизбежная ловушка — вызов внутреннего метода. Если вызывающий @Retryableи вызываемый метод аннотированного метода находятся в одном классе, то повторная попытка завершится неудачно.

С помощью приведенных выше простых конфигураций вы можете видеть, что механизм повторных попыток Spring Retry относительно хорошо продуман и гораздо более эффективен, чем написание собственной реализации AOP.

Недостатки:
Однако определенные недостатки все же имеются: механизм повтора Spring поддерживает только перехват исключений и не может проверить возвращаемое значение.

@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;
        }
    );
}

Единственное преимущество на этом этапе заключается в том, что вы можете установить несколько стратегий повтора:

NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

7. повторная попытка гуавы

По сравнению с Spring Retry, Guava Retry более гибок и может определить, следует ли повторять попытку, на основе возвращаемого значения.

<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, а затем используйте этот экземпляр для вызова метода, который необходимо повторить.Существует множество способов настроить механизм повтора:

retryIfException():对所有异常进行重试
retryIfRuntimeException():设置对指定异常进行重试
retryIfExceptionOfType():对所有 RuntimeException 进行重试
retryIfResult():对不符合预期的返回结果进行重试

Существует также пять методов, начинающиеся с withXxx, которые используются для установки стратегии повтора/стратегии ожидания/стратегии блокировки/ограничения времени выполнения одной задачи/настраиваемого прослушивателя для достижения более мощной обработки исключений:

withRetryListener():设置重试监听器,用来执行额外的处理工作
withWaitStrategy():重试等待策略
withStopStrategy():停止重试策略
withAttemptTimeLimiter:设置任务单次执行的时间限制,如果超时则抛出异常
withBlockStrategy():设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情

Подведем итог

От повторной попытки вручную до реализации самостоятельно с помощью Spring AOP, до стояния на плечах гигантов и использования особенно отличных реализаций с открытым исходным кодом Spring Retry и Google guava-retry, после введения различных методов реализации повторных попыток вы можете увидеть вышеизложенное. Этот метод в основном соответствует потребности большинства сценариев:

Если это проект на основе Spring, большинство проблем можно решить с помощью аннотаций Spring Retry.
Если проект не использует среды, связанные со Spring, подойдет повторная попытка Google guava: автономная, более гибкая и мощная в использовании. .

Guess you like

Origin blog.csdn.net/weixin_39570655/article/details/132275905