Explicación detallada del proceso de inicialización automática de la base de datos cuando Spring Boot se inicia por primera vez.

Con el desarrollo de Internet, las funciones comerciales en los proyectos se vuelven cada vez más complejas. Para algunos servicios básicos, inevitablemente llamaremos a algunas interfaces de terceros o servicios proporcionados por otros proyectos dentro de la empresa. Sin embargo, la solidez de los servicios remotos y la Estabilidad de la red El sexo es un factor incontrolable.

Es posible que no haya anomalías durante la fase de prueba, pero después de conectarse, la interfaz llamada puede generar un error o devolver una excepción del sistema debido a errores internos o fluctuaciones de la red, por lo que debemos considerar agregar un mecanismo de reintento.

El mecanismo de reintento puede mejorar la solidez del sistema y reducir el impacto de la indisponibilidad temporal de los servicios dependientes debido a las fluctuaciones de la red, lo que permite que el sistema funcione de manera más estable.

1. Vuelva a intentarlo manualmente

Reintento manual: utilice la instrucción while para volver a intentar:

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

Ejecute el código anterior:

imagen

El código anterior parece resolver el problema del reintento, pero de hecho tiene algunas desventajas:

  1. Dado que no hay un intervalo de reintento, es probable que el servicio llamado remotamente no se haya recuperado de la excepción de la red, por lo que es posible que las próximas llamadas fallen.
  2. El código es demasiado intrusivo y el código de la persona que llama no es lo suficientemente elegante
  3. Puede haber muchos servicios llamados de forma remota en el proyecto, y agregar reintentos para cada uno dará como resultado una gran cantidad de código duplicado.

2. Proxy estático

El método de procesamiento anterior requiere muchas modificaciones en el código comercial y, aunque logra la función, es demasiado intrusivo para el código original y tiene poca capacidad de mantenimiento. Entonces necesitamos usar una forma más elegante sin modificar directamente el código comercial, ¿cómo hacerlo?

De hecho, es muy simple: simplemente envuelva otra capa fuera del código comercial, y aquí entra en juego el modelo proxy.

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

De esta manera, la lógica de reintento la completa la clase proxy y no es necesario modificar la lógica de la clase de negocio original.Si desea modificar la lógica de reintento en el futuro, solo necesita modificar esta clase.

Aunque el modo proxy es más elegante, si hay muchos servicios dependientes, obviamente es demasiado problemático crear una clase de proxy para cada servicio. De hecho, la lógica de reintento es similar, excepto que el número de reintentos y el retraso son diferentes. lo mismo. Si cada clase escribe una cadena tan larga de código similar, ¡obviamente no es elegante!

3. Proxy dinámico JDK

En este momento, el proxy dinámico entra en escena. Solo necesitas escribir una clase de procesamiento de proxy y estará bien.

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

}

prueba:

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

El proxy dinámico puede reunir toda la lógica de reintento, lo que obviamente es mucho más conveniente y elegante que usar la clase de proxy directamente.

Aquí se utiliza el proxy dinámico JDK, por lo que existe un defecto natural. Si la clase que desea que sea proxy no implementa ninguna interfaz, entonces no puede crear un objeto proxy para ella. Este método no funcionará.

4. Proxy dinámico CGLib

Ahora que hemos hablado del proxy dinámico JDK, debemos mencionar el proxy dinámico CGLib. El uso del proxy dinámico JDK tiene requisitos para las clases proxy. No todas las clases pueden ser proxy y el proxy dinámico CGLib simplemente resuelve este problema.

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

}

prueba:

@GetMapping("/addOrder")
public String addOrder() {
    
    
    OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);
    orderServiceProxy.addOrder();
    return "addOrder";
}

Esto es genial y resuelve perfectamente los defectos causados ​​por el proxy dinámico JDK. El índice de elegancia ha aumentado mucho.

Sin embargo, todavía hay un problema con esta solución, es decir, la lógica original debe modificarse de manera intrusiva y es necesario realizar ajustes en cada lugar donde se llama a la instancia de proxy, lo que aún traerá más modificaciones al código original.

5. Operación manual

Teniendo en cuenta que puede haber muchos métodos en el futuro que también necesiten la función de reintento, podemos implementar la función común de reintento a través de AOP: use AOP para establecer aspectos para la llamada de destino y puede agregar algo de lógica de reintento antes y después del objetivo. llamada al método.

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

Anotación personalizada:

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

}

Agregue anotaciones a los métodos que deben volver a intentarse @MyRetryable:

@Service
public class OrderServiceImpl implements OrderService {
    
    

    @Override
    @MyRetryable(retryTimes = 5, retryInterval = 2)
    public void addOrder() {
    
    
        int i = 3 / 0;
        // addOrder
    }
    
}

De esta manera, no es necesario escribir código repetido y la implementación es más elegante: una anotación puede realizar un reintento.

6. reintento de primavera

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

@EnableRetryActive la función de reintento: agregue anotaciones a la clase de inicio o clase de configuración

@RetryableAgregar anotaciones a los métodos que deben volver a intentarse

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

Después de llamar a este método, se volverá a intentar. El número máximo de reintentos es 3. El primer intervalo de reintento es de 2 segundos y luego se incrementa en 2 veces el tamaño. El segundo intervalo de reintento es de 4 segundos y el tercer intervalo de reintento es 8 chelines.

El mecanismo de reintento de Spring también admite muchas funciones útiles, que se completan con tres anotaciones:

@Reintentable
@Retroceso
@Recuperar

Ver @Retryableel código fuente de la anotación: especificar tiempos y reintentos de excepción

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 注解: Especifique la estrategia alternativa de reintento (si la llamada falla debido a fluctuaciones de la red, el reintento inmediato aún puede fallar. La mejor opción es esperar un momento antes de volver a intentarlo. Un método para decidir cuánto tiempo esperar antes de volver a intentarlo. En términos sencillos términos, es decir, si se debe volver a intentar inmediatamente o esperar un período de tiempo antes de volver a intentarlo cada vez)

@Recover 注解: Realizar trabajo de seguimiento: cuando el reintento alcanza el número de veces especificado, se llamará al método especificado para realizar operaciones como el registro.

Aviso:

El método marcado con la anotación @Recover debe estar en la misma clase que el método marcado con @Retryable. El
tipo de excepción lanzada por el método de reintento debe ser consistente con el tipo de parámetro del método recovery().
El valor de retorno de la recuperación () El método debe ser el mismo que el valor de retorno del método de reintento. Garantizar la coherencia
. Ya no se pueden lanzar excepciones en el método recovery(); de lo contrario, se informará un error que no puede reconocer la excepción.

Otro punto que debe recordarse aquí es que dado que Spring Retry usa la mejora de Aspecto, habrá un problema inevitable al usar la llamada a método interno de Aspecto. Si la persona que llama y la persona que llama del método anotado están en la misma clase, entonces el reintento @Retryablefallará

A través de las configuraciones simples anteriores, puede ver que el mecanismo de reintento Spring Retry está relativamente bien pensado y es mucho más poderoso que escribir su propia implementación AOP.

Desventajas:
sin embargo, todavía existen ciertas deficiencias: el mecanismo de reintento de Spring solo admite la detección de excepciones y no puede verificar el valor de retorno.

@Retryable
public String hello() {
    
    
    long current = count.incrementAndGet();
    System.out.println("第" + current +"次被调用");
    if (current % 3 != 0) {
    
    
        log.warn("调用失败");
        return "error";
    }
    return "success";
}

Por lo tanto, incluso si se agrega @Retryable al método, no se puede lograr un reintento fallido.

Además de usar anotaciones, Spring Retry también admite reintentar directamente usando código al llamar:

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

El único beneficio en este punto es que puede establecer múltiples estrategias de reintento:

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

7. reintento de guayaba

En comparación con Spring Retry, Guava Retry es más flexible y puede determinar si se debe volver a intentar en función del valor de retorno.

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

Primero cree una instancia de Retryer y luego use esta instancia para llamar al método que debe volver a intentarse. Hay muchas formas de configurar el mecanismo de reintento:

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

También hay cinco métodos que comienzan con Xxx, que se utilizan para establecer la estrategia de reintento/estrategia de espera/estrategia de bloqueo/límite de tiempo de ejecución de tarea única/escucha personalizada para lograr un manejo de excepciones más potente:

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

Resumir

Desde el reintento manual, hasta implementarlo usted mismo usando Spring AOP, hasta pararse sobre los hombros de gigantes y usar implementaciones de código abierto particularmente excelentes Spring Retry y Google guava-retrying, después de introducir varios métodos de implementación de reintento, puede ver lo anterior Este método básicamente cumple las necesidades de la mayoría de los escenarios:

Si se trata de un proyecto basado en Spring, la mayoría de los problemas se pueden resolver utilizando anotaciones Spring Retry.
Si el proyecto no utiliza marcos relacionados con Spring, es adecuado utilizar Google guava-retrying: autónomo, más flexible y potente de usar. .

Supongo que te gusta

Origin blog.csdn.net/weixin_39570655/article/details/132275905
Recomendado
Clasificación