CompletableFuturo principio y práctica

Directorio de artículos

1 Por qué es necesaria la carga paralela

Por ejemplo: el servicio de API del comerciante de comida a domicilio es un servicio típico de uso intensivo de E/S (Bloque de E/S). Además, hay dos características principales del negocio de transacciones comerciales para llevar:

  • El servidor debe devolver todo el contenido de la tarjeta de pedido a la vez : De acuerdo con el "Protocolo de Sincronización Incremental Nota 1" entre el comerciante y el servidor, el servidor debe devolver toda la información del pedido a la vez, incluida la información principal del pedido, productos básicos, liquidación, entrega, información del usuario, información del pasajero, daños por comida, reembolsos, compensación por servicio al cliente (consulte la captura de pantalla de la tarjeta de pedido de Meituan o Ele.me), etc., necesita obtener datos de más de 30 servicios aguas abajo. Bajo ciertas condiciones, como iniciar sesión por primera vez o no iniciar sesión durante mucho tiempo, el cliente obtendrá múltiples pedidos en páginas, por lo que se iniciarán más llamadas remotas.
  • Interacciones frecuentes entre los comerciantes y los servidores : los comerciantes son sensibles a los cambios en el estado de los pedidos, y los múltiples mecanismos push-pull garantizan que cada cambio pueda llegar al comerciante, lo que genera interacciones frecuentes entre la aplicación y el servidor, y cada cambio debe extraer lo último contenido completo del pedido.

Con un tráfico tan grande en el enlace de la transacción para llevar, para garantizar la experiencia del usuario del comerciante y el alto rendimiento de la interfaz, es inevitable obtener datos del flujo descendente en paralelo.

2 Implementación de carga paralela

La obtención de datos de aguas abajo en paralelo se puede dividir en modelo síncrono y modelo asíncrono en términos de modelo IO .

2.1 Modelo de sincronización

La forma más común de obtener datos de varios servicios es llamar sincrónicamente, como se muestra en la siguiente figura:

Figura 2 Llamada síncrona

En el escenario de llamadas sincrónicas, la interfaz tarda mucho tiempo y tiene un rendimiento deficiente, y el tiempo de respuesta de la interfaz es T > T1+T2+T3+...+Tn. En este momento, para acortar el tiempo de respuesta de la interfaz, los grupos de subprocesos se utilizan generalmente para obtener datos en paralelo Comerciantes Esta es la forma en que se ensambla la tarjeta de pedido final.

Figura 3 Grupo de subprocesos paralelos

Este método conduce a una baja utilización de recursos debido a las siguientes dos razones:

  • Se desperdicia una gran cantidad de recursos de la CPU en el bloqueo y la espera , lo que da como resultado una baja utilización de los recursos de la CPU. Antes de Java 8, las devoluciones de llamada generalmente se usaban para reducir el bloqueo, pero el uso extensivo de devoluciones de llamada causó el notorio problema de devolución de llamada , que redujo en gran medida la legibilidad y el mantenimiento del código.
  • Con el fin de aumentar la concurrencia, se introducirán más grupos de subprocesos adicionales . A medida que aumenta la cantidad de subprocesos de programación de CPU, se producirá una contención de recursos más seria. Se desperdiciarán valiosos recursos de CPU en el cambio de contexto, y los subprocesos mismos también ocuparán el sistema.recursos, y no se puede aumentar indefinidamente.

En el modelo síncrono, los recursos de hardware no se pueden utilizar por completo y es probable que el rendimiento del sistema llegue al cuello de botella.

2.2 Modelo asíncrono NIO

Utilizamos principalmente los siguientes dos métodos para reducir la sobrecarga de programación y el tiempo de bloqueo del grupo de subprocesos:

  • La cantidad de subprocesos se puede reducir mediante llamadas asincrónicas RPC NIO, lo que reduce la sobrecarga de programación (cambio de contexto). Por ejemplo, para llamadas asincrónicas de Dubbo, consulte el artículo "Dubbo Caller Asynchronous " .
  • Mediante la introducción de CompletableFuture (en lo sucesivo, CF) para orquestar el proceso comercial y reducir el bloqueo entre dependencias. Este artículo describe principalmente el uso y el principio de CompletableFuture.

2.3 ¿Por qué elegir CompletableFuture?

Primero realizamos una encuesta horizontal sobre soluciones ampliamente populares en la industria, principalmente Future, CompletableFuture Note 2, RxJava y Reactor. Sus características se comparan de la siguiente manera:

Futuro CompletableFuturo RxJava Reactor
Componible (combinable) ✔️ ✔️ ✔️
Asincrónico ✔️ ✔️ ✔️ ✔️
Fusión de operadores ✔️ ✔️
Perezoso (ejecución retrasada) ✔️ ✔️
contrapresión ✔️ ✔️
  • Componible : varias operaciones dependientes se pueden organizar de diferentes maneras. Por ejemplo, CompletableFuture proporciona thenCompose, thenCombine y otros métodos que comienzan con then. Estos métodos admiten la función "combinable".
  • Operation Fusion : la combinación de múltiples operadores utilizados en un flujo de datos reduce de alguna manera la sobrecarga (tiempo, memoria).
  • Ejecución retrasada : las acciones no se ejecutan inmediatamente, sino que se activan cuando se le indica explícitamente que lo haga. Por ejemplo, Reactor solo activa operaciones cuando hay suscriptores.
  • Contrapresión : la velocidad de procesamiento de algunas etapas asincrónicas no puede mantenerse, y la falla directa conducirá a una gran cantidad de pérdida de datos, lo cual es inaceptable para el negocio. En este momento, se requiere retroalimentación al productor ascendente para reducir la cantidad de llamadas

RxJava y Reactor son obviamente más potentes. Proporcionan más métodos de llamada de funciones y admiten más funciones, pero también conllevan mayores costos de aprendizaje. Las características que más necesitamos para esta integración son "asincrónicas" y "combinables". Después de una consideración exhaustiva, elegimos CompletableFuture, que tiene un costo de aprendizaje relativamente bajo.

3 CompletableUso futuro y principio

3.1 Antecedentes y definición de CompletableFuture

3.1.1 Problemas resueltos por CompletableFuture

CompletableFuture fue introducido por Java 8. Antes de Java8, generalmente implementábamos la asincronía a través de Future.

  • El futuro se utiliza para representar los resultados de los cálculos asincrónicos. Los resultados solo se pueden obtener mediante el bloqueo o el sondeo, y no es compatible con la configuración de métodos de devolución de llamada. Antes de Java 8, si desea configurar devoluciones de llamada, generalmente usaba ListenableFuture de Guava. La introducción de devoluciones de llamada conducirá a un notorio infierno de devolución de llamada (el siguiente ejemplo se demostrará específicamente mediante el uso de ListenableFuture).
  • CompletableFuture amplía Future, que puede procesar los resultados de los cálculos mediante la configuración de devoluciones de llamadas. También admite operaciones combinadas y una mayor orquestación, y al mismo tiempo resuelve el problema de las devoluciones de llamadas hasta cierto punto.

Lo siguiente ilustrará la diferencia asíncrona a través de ListenableFuture y CompletableFuture. Suponga que hay tres operaciones step1, step2 y step3 que tienen dependencias, y la ejecución de step3 depende de los resultados de step1 y step2.

La implementación de Future(ListenableFuture) (infierno de devolución de llamada) es la siguiente:

ExecutorService executor = Executors.newFixedThreadPool(5);
ListeningExecutorService guavaExecutor = MoreExecutors.listeningDecorator(executor);
ListenableFuture<String> future1 = guavaExecutor.submit(() -> {
    
    
    //step 1
    System.out.println("执行step 1");
    return "step1 result";
});
ListenableFuture<String> future2 = guavaExecutor.submit(() -> {
    
    
    //step 2
    System.out.println("执行step 2");
    return "step2 result";
});
ListenableFuture<List<String>> future1And2 = Futures.allAsList(future1, future2);
Futures.addCallback(future1And2, new FutureCallback<List<String>>() {
    
    
    @Override
    public void onSuccess(List<String> result) {
    
    
        System.out.println(result);
        ListenableFuture<String> future3 = guavaExecutor.submit(() -> {
    
    
            System.out.println("执行step 3");
            return "step3 result";
        });
        Futures.addCallback(future3, new FutureCallback<String>() {
    
    
            @Override
            public void onSuccess(String result) {
    
    
                System.out.println(result);
            }        
            @Override
            public void onFailure(Throwable t) {
    
    
            }
        }, guavaExecutor);
    }

    @Override
    public void onFailure(Throwable t) {
    
    
    }}, guavaExecutor);

CompletableFuture se implementa de la siguiente manera:

ExecutorService executor = Executors.newFixedThreadPool(5);
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
    
    
    System.out.println("执行step 1");
    return "step1 result";
}, executor);
CompletableFuture<String> cf2 = CompletableFuture.supplyAsync(() -> {
    
    
    System.out.println("执行step 2");
    return "step2 result";
});
cf1.thenCombine(cf2, (result1, result2) -> {
    
    
    System.out.println(result1 + " , " + result2);
    System.out.println("执行step 3");
    return "step3 result";
}).thenAccept(result3 -> System.out.println(result3));

Obviamente, la implementación de CompletableFuture es más concisa y más legible.

3.1.2 Definición de Futuro Completable

Figura 4 Definición de CompletableFuture

CompletableFuture implementa dos interfaces (como se muestra en la figura anterior): Future y CompletionStage. Future representa el resultado del cálculo asincrónico, y CompletionStage se usa para representar un paso (Etapa) en el proceso de ejecución asincrónica. Este paso puede ser desencadenado por otra CompletionStage. Con la finalización del paso actual, también puede desencadenar la ejecución de un serie de otras CompletationStages. Por lo tanto, podemos organizar y combinar estos pasos en una variedad de formas de acuerdo con el negocio real. La interfaz CompletionStage define tales capacidades. Podemos combinar y organizar estos pasos a través de métodos de programación funcional como thenAppy y thenCompose proporcionados por ella.

3.2 El uso de CompletableFuture

Usemos un ejemplo para explicar cómo usar CompletableFuture.Usar CompletableFuture también es el proceso de construir un árbol de dependencia. La finalización de un CompletableFuture desencadena la ejecución de una serie de otros CompletableFutures que dependen de él:

Figura 5 proceso de ejecución de solicitud

Como se muestra en la figura anterior, aquí hay un flujo de una interfaz comercial, que incluye 5 pasos de CF1\CF2\CF3\CF4\CF5, y muestra las dependencias entre estos pasos, cada paso puede ser una llamada RPC, una operación de base de datos o una llamada de método local, etc., cuando se usa CompletableFuture para la programación asíncrona, cada paso en el diagrama generará un objeto CompletableFuture, y el resultado final también estará representado por un CompletableFuture.

Según el número de dependencias de CompletableFuture, se puede dividir en las siguientes categorías: dependencias cero, dependencias unarias, dependencias binarias y dependencias múltiples.

3.2.1 Dependencia cero: creación de CompletableFuture

Primero veamos cómo crear un nuevo CompletableFuture sin depender de otros CompletableFutures:

Figura 6 Cero dependencias

Como se muestra en el enlace rojo de la figura anterior, después de que la interfaz recibe la solicitud, primero inicia dos llamadas asíncronas CF1 y CF2. Existen tres métodos principales:

ExecutorService executor = Executors.newFixedThreadPool(5);
//1、使用runAsync或supplyAsync发起异步调用
CompletableFuture<String> cf1 = CompletableFuture.supplyAsync(() -> {
    
    
  return "result1";
}, executor);
//2、CompletableFuture.completedFuture()直接创建一个已完成状态的CompletableFuture
CompletableFuture<String> cf2 = CompletableFuture.completedFuture("result2");
//3、先初始化一个未完成的CompletableFuture,然后通过complete()、completeExceptionally(),完成该CompletableFuture
CompletableFuture<String> cf = new CompletableFuture<>();
cf.complete("success");

Un escenario de uso típico del tercer método es convertir el método de devolución de llamada a CompletableFuture y luego confiar en la capacidad de CompletableFure para la organización de llamadas. El ejemplo es el siguiente:

@FunctionalInterface
public interface ThriftAsyncCall {
    
    
    void invoke() throws TException;
}
 /**
  * 该方法为rpc注册监听的封装,可以作为其他实现的参照
  * OctoThriftCallback 为thrift回调方法
  * ThriftAsyncCall 为自定义函数,用来表示一次thrift调用(定义如上)
  */
  public static <T> CompletableFuture<T> toCompletableFuture(final OctoThriftCallback<?,T> callback , ThriftAsyncCall thriftCall) {
    
    
   //新建一个未完成的CompletableFuture
   CompletableFuture<T> resultFuture = new CompletableFuture<>();
   //监听回调的完成,并且与CompletableFuture同步状态
   callback.addObserver(new OctoObserver<T>() {
    
    
       @Override
       public void onSuccess(T t) {
    
    
           resultFuture.complete(t);
       }
       @Override
       public void onFailure(Throwable throwable) {
    
    
           resultFuture.completeExceptionally(throwable);
       }
   });
   if (thriftCall != null) {
    
    
       try {
    
    
           thriftCall.invoke();
       } catch (TException e) {
    
    
           resultFuture.completeExceptionally(e);
       }
   }
   return resultFuture;
  }

3.2.2 Dependencia unaria: depender de un CF

Figura 7 Dependencia unaria

Como se muestra en el enlace rojo anterior, CF3 y CF5 dependen de CF1 y CF2 respectivamente. Esta dependencia de un solo CompletableFuture se puede realizar a través de métodos como luego Aplicar, luego Aceptar, luego Componer, y el código es el siguiente:

CompletableFuture<String> cf3 = cf1.thenApply(result1 -> {
    
    
  //result1为CF1的结果
  //......
  return "result3";
});
CompletableFuture<String> cf5 = cf2.thenApply(result2 -> {
    
    
  //result2为CF2的结果
  //......
  return "result5";
});

3.2.3 Dependencia binaria: depende de dos FC

Figura 8 Dependencias binarias

Como se muestra en el enlace rojo de la figura anterior, CF4 depende de dos CF1 y CF2 al mismo tiempo. Esta dependencia binaria se puede realizar a través de devoluciones de llamada como theCombine, como se muestra en el siguiente código:

CompletableFuture<String> cf4 = cf1.thenCombine(cf2, (result1, result2) -> {
    
    
  //result1和result2分别为cf1和cf2的结果
  return "result4";
});

3.2.4 Múltiples dependencias: confiar en múltiples CF

Figura 9 Múltiples dependencias

Como se muestra en el enlace rojo de la figura anterior, el final de todo el proceso depende de los tres pasos CF3, CF4 y CF5. Esta dependencia múltiple se puede lograr mediante el método o. La diferencia es que se usa cuando hay varias allOfdependencias anyOf. Se requiere que se completen allOfCuando múltiples dependencias Cualquiera de ellos se puede usar cuando esté listo anyOf, como se muestra en el siguiente código:

CompletableFuture<Void> cf6 = CompletableFuture.allOf(cf3, cf4, cf5);
CompletableFuture<String> result = cf6.thenApply(v -> {
    
    
  //这里的join并不会阻塞,因为传给thenApply的函数是在CF3、CF4、CF5全部完成时,才会执行 。
  result3 = cf3.join();
  result4 = cf4.join();
  result5 = cf5.join();
  //根据result3、result4、result5组装最终result;
  return "result";
});

3.3 Principio de Futuro Completable

CompletableFuture contiene dos campos: resultado y pila . result se usa para almacenar los resultados del CF actual, y stack (Finalización) indica las Acciones de dependencia (Acciones de dependencia) que deben activarse después de la finalización del CF actual para activar el cálculo del CF que depende de él. pueden ser múltiples acciones dependientes (lo que indica que hay múltiples dependencias en él CF), almacenadas en forma de pila ( Treiber stack ), y stack representa el elemento superior de la pila.

Figura 10 Estructura básica de CF

Este método es similar al "patrón de observador", y las acciones dependientes (acción de dependencia) se encapsulan en una subclase de finalización separada. El siguiente es el diagrama de estructura de relación de clase de finalización. Cada método en CompletableFuture corresponde a una subclase de Completion en el diagrama, y ​​Completion en sí es la clase base de los observadores .

  • UniCompletion hereda Completion y es la clase base de las dependencias unarias. Por ejemplo, UniApply, la clase de implementación de thenApply, hereda de UniCompletion.
  • BiCompletion hereda UniCompletion, que es la clase base de dependencias binarias y también la clase base de dependencias múltiples. Por ejemplo, la clase de implementación BiRelay de thenCombine hereda de BiCompletion.

Figura 11 Diagrama de clase CF

3.3.1 La idea de diseño de CompletableFuture

De acuerdo con la idea de diseño similar al "modo observador", el análisis principal puede comenzar desde dos aspectos: "observador" y "observado". Dado que hay muchos tipos de devoluciones de llamada, pero las diferencias estructurales no son grandes, aquí solo tomamos como ejemplo theApply en la dependencia unaria, y no enumeramos todos los tipos de devoluciones de llamada. Como se muestra abajo:

Figura 12 luego Aplicar croquis

3.3.1.1 Observado

  1. Cada CompletableFuture se puede considerar como un observador, y hay una pila de variables miembro de lista enlazada de tipo Finalización dentro de él, que se utiliza para almacenar todos los observadores registrados en él. Cuando el objeto observado complete la ejecución, la propiedad de la pila aparecerá y los observadores registrados en ella serán notificados a su vez. En el ejemplo anterior, el paso fn2 está encapsulado en UniApply como observador.
  2. El atributo de resultado en el CF observado se utiliza para almacenar los datos de resultados devueltos. Este puede ser el valor de retorno de una llamada RPC, o cualquier objeto, que corresponda al resultado de la ejecución del paso fn1 en el ejemplo anterior.

3.3.1.2 Observadores

CompletableFuture admite muchos métodos de devolución de llamada, como luego Aceptar, luego Aplicar, excepcionalmente, etc. Estos métodos reciben un parámetro f de tipo función, generan un objeto de tipo Finalización (es decir, observador) y asignan la función de entrada f a la variable miembro fn de Finalización , luego verifique si el CF actual está en el estado completado (es decir, ¡resultado! = nulo), si está completo, active fn directamente; de ​​lo contrario, agregue el Completion del observador a la pila de la cadena del observador de CF e intente activar nuevamente , si el objeto observado no ha terminado de ejecutarse Entonces, la notificación se activa una vez que se completa su ejecución.

  1. El atributo dep en el observador: apunta a su correspondiente CompletableFuture En el ejemplo anterior, dep apunta a CF2.
  2. El atributo src en el observador: apunta al Futuro Completable del que depende. En el ejemplo anterior, src apunta a CF1.
  3. El atributo fn en la finalización del observador: se utiliza para almacenar la función específica que espera ser devuelta. Cabe señalar aquí que diferentes métodos de devolución de llamada (thenAccept, thenApply, excepcionalmente, etc.) reciben diferentes tipos de funciones, es decir, hay muchos tipos de fn, y en el ejemplo anterior, fn apunta a fn2.

3.3.2 Proceso general

3.3.2.1 Dependencias unarias

Aquí todavía tome thenApply como ejemplo para ilustrar el proceso de dependencia unaria:

  1. Registre la Finalización del observador en CF1, y CF1 empuja la Finalización en la pila.
  2. Cuando se complete la operación de CF1, el resultado se asignará al atributo de resultado en CF1.
  3. Explota la pila una por una para notificar al observador que intente correr.

Figura 13 Breve descripción del proceso de ejecución

El diseño preliminar del proceso se muestra en la figura anterior. Aquí hay algunos problemas concurrentes sobre el registro y la notificación. Puede pensar en ello:

P1 : Antes de que se registre el observador, si se ha ejecutado el CF y se ha emitido la notificación, ¿nunca se activará el observador porque se perdió la notificación? A1 : no. Al registrarse, verifique si el CF dependiente se ha completado. Si no se completa (resultado == nulo), el observador se colocará en la pila y, si se completa (resultado != nulo), la operación del observador se activará directamente.

P2 : Habrá un juicio de "resultado == nulo" antes de "empujar en la pila". Estas dos operaciones son operaciones no atómicas. La implementación de CompletableFufure no bloquea las dos operaciones, y el tiempo de finalización está entre estas dos operaciones. , todavía no se notifica al observador, ¿todavía no se dispara?

Figura 14 Cheque de presión

A2 : no. Después de ser empujado a la pila, verifique si el CF se completó nuevamente y dispare si se completó.

P3 : Cuando se confía en varios CF, el observador se colocará en la pila de todos los CF dependientes y se realizará cuando se complete cada CF. ¿Hará que una operación se ejecute varias veces? Como se muestra en la siguiente figura, cómo evitar que CF3 se active varias veces cuando CF1 y CF2 se completan al mismo tiempo.

Figura 15 Disparadores múltiples

R3 : La implementación de CompletableFuture resuelve este problema de esta manera: antes de la ejecución, el observador primero establecerá un bit de estado a través de la operación CAS y cambiará el estado de 0 a 1. Si el observador ya se ha ejecutado, la operación CAS fallará y se cancelará la ejecución.

A través del análisis de los tres problemas anteriores, se puede ver que cuando CompletableFuture maneja problemas paralelos, no hay una operación de bloqueo en todo el proceso, lo que mejora en gran medida la eficiencia de ejecución del programa. Después de considerar los problemas paralelos, podemos obtener un diagrama de flujo general completo de la siguiente manera:

Figura 16 proceso completo

Los métodos de devolución de llamada admitidos por CompletableFuture son muy completos, pero como se describe en el diagrama de flujo general del capítulo anterior, su proceso general es consistente. Todas las devoluciones de llamada reutilizan la misma arquitectura de proceso, y los diferentes oyentes de devolución de llamada se diferencian a través de patrones de estrategia .

3.3.2.2 Dependencias binarias

Tomemos a thenCombine como ejemplo para ilustrar las dependencias binarias:

Figura 17 Estructura de datos de dependencia binaria

La operación thenCombine expresa una dependencia de dos CompletableFutures. Su clase de implementación de observador es BiApply.Como se muestra en la figura anterior, BiApply asocia los dos CF dependientes a través de los atributos src y snd, y el tipo del atributo fn es BiFunction. A diferencia de una sola dependencia, si el CF dependiente no se completa, Combine intentará insertar BiApply en la pila de los dos CF dependientes, y cada CF dependiente intentará activar el BiApply del observador cuando se complete. las dependencias están completas, y si es así, comience la ejecución. Para resolver el problema de los disparos repetidos, también se utiliza la operación CAS mencionada en el capítulo anterior. Al ejecutar, el bit de estado se establecerá a través de CAS para evitar disparos repetidos.

3.3.2.3 Dependencias múltiples

Los métodos de devolución de llamada que dependen de múltiples CompletableFutures incluyen allOf, anyOfy la diferencia es que allOfla clase de implementación del observador es BiRelay, y la devolución de llamada solo se ejecutará después de que se completen todos los CF dependientes; mientras que la anyOfclase de implementación del observador es OrRelay, y se completará cualquiera de los CF dependientes. Ambos se implementan mediante la creación de múltiples CF dependientes en un árbol binario equilibrado y la notificación capa por capa de los resultados de la ejecución hasta que el nodo raíz activa el monitoreo de devolución de llamada.

Figura 18 Árbol de estructura de dependencia multivariante

3.3.3 Resumen

Este capítulo es una divulgación científica del principio de implementación de CompletableFuture, cuyo objetivo es explicar claramente el principio de implementación de CompletableFuture a través de diagramas estructurales, diagramas de flujo y descripciones de texto sin pegar el código fuente. Traduzca el oscuro código fuente al diagrama de flujo del capítulo "Proceso general" e integre la lógica del procesamiento concurrente para que todos la entiendan.

4 Resumen de la práctica

En el proceso de sincronización de la API del lado del comerciante, encontramos algunos problemas, algunos de los cuales pueden estar ocultos, y la experiencia de lidiar con estos problemas se resuelve a continuación. Espero ayudar a más estudiantes, y todos pueden evitar algunas trampas.

4.1 Problema de bloqueo de subprocesos

4.1.1 ¿Sobre qué hilo se ejecuta el código?

Para administrar razonablemente los recursos de subprocesos, el requisito previo más básico es saber claramente en qué subproceso se ejecutará cada línea de código al escribir código. Echemos un vistazo al hilo de ejecución de CompletableFuture.

CompletableFuture implementa la interfaz CompletionStage, admite varias operaciones de combinación a través de métodos de devolución de llamada enriquecidos, y cada escena de combinación tiene dos métodos: sincrónico y asincrónico.

Los métodos sincrónicos (es decir, los métodos sin el sufijo Async) tienen dos casos.

  • Si la operación dependiente se ha ejecutado durante el registro, el subproceso actual la ejecutará directamente.
  • Si la operación dependiente no se ha ejecutado en el momento del registro, será ejecutada por el subproceso de devolución de llamada.

Método asíncrono (es decir, el método con el sufijo Async): puede elegir pasar el parámetro del grupo de subprocesos Executor para que se ejecute en el grupo de subprocesos especificado; cuando no se pasa el Executor, el grupo de subprocesos compartidos CommonPool en ForkJoinPool será utilizado (el tamaño de CommonPool es la cantidad de núcleos de CPU: 1. Si se trata de una aplicación intensiva de IO, la cantidad de subprocesos puede convertirse en un cuello de botella).

Por ejemplo:

ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    
    
    System.out.println("supplyAsync 执行线程:" + Thread.currentThread().getName());
    //业务操作
    return "";
}, threadPool1);
//此时,如果future1中的业务操作已经执行完毕并返回,则该thenApply直接由当前main线程执行;否则,将会由执行以上业务操作的threadPool1中的线程执行。
future1.thenApply(value -> {
    
    
    System.out.println("thenApply 执行线程:" + Thread.currentThread().getName());
    return value + "1";
});
//使用ForkJoinPool中的共用线程池CommonPool
future1.thenApplyAsync(value -> {
    
    
//do something
  return value + "1";
});
//使用指定线程池
future1.thenApplyAsync(value -> {
    
    
//do something
  return value + "1";
}, threadPool1);

4.2 Notas sobre el grupo de subprocesos

4.2.1 Las devoluciones de llamada asincrónicas deben pasarse al grupo de subprocesos

Como se mencionó anteriormente, el método de devolución de llamada asincrónica puede elegir si pasar el parámetro del grupo de subprocesos Executor.Aquí recomendamos forzar el paso del grupo de subprocesos, y el grupo de subprocesos se aísla de acuerdo con la situación real .

Cuando no se pasa el grupo de subprocesos, se usará el grupo de subprocesos comunes CommonPool en ForkJoinPool. Todas las llamadas aquí compartirán el grupo de subprocesos. El número de subprocesos centrales = el número de procesadores - 1 (el número de subprocesos centrales de un solo núcleo es 1), y todas las devoluciones de llamada asincrónicas se compartirán. En CommonPool, las empresas centrales y no centrales compiten por subprocesos en el mismo grupo, lo que puede convertirse fácilmente en un cuello de botella del sistema. Pasar manualmente los parámetros del grupo de subprocesos puede facilitar el ajuste de los parámetros, y se pueden asignar diferentes grupos de subprocesos a diferentes negocios para aislar recursos y reducir la interferencia mutua entre diferentes negocios.

4.2.2 La referencia circular del grupo de subprocesos puede provocar un interbloqueo

public Object doGet() {
    
    
  ExecutorService threadPool1 = new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(100));
  CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> {
    
    
  //do sth
    return CompletableFuture.supplyAsync(() -> {
    
    
        System.out.println("child");
        return "child";
      }, threadPool1).join();//子任务
    }, threadPool1);
  return cf1.join();
}

Como se muestra en el bloque de código anterior, la tercera línea del método doGet solicita subprocesos de threadPool1 a través de supplyAsync, y las subtareas internas solicitan subprocesos de threadPool1. El tamaño de threadPool1 es 10. Cuando lleguen 10 solicitudes al mismo tiempo, threadPool1 estará lleno. Cuando las subtareas soliciten subprocesos, entrarán en la cola de bloqueo, pero la finalización de la tarea principal depende de las subtareas. En este momento, el las subtareas no pueden obtener subprocesos. , la tarea principal no se pudo completar. El subproceso principal ejecuta cf1.join() en un estado bloqueado y nunca puede recuperarse.

Para solucionar este problema, es necesario aislar el grupo de subprocesos de la tarea principal y la tarea secundaria, y las dos tareas solicitan diferentes grupos de subprocesos para evitar el bloqueo causado por dependencias circulares.

4.2.3 Llamada RPC asíncrona Tenga cuidado de no bloquear el grupo de subprocesos de E/S

Después de que el servicio esté asincronizado, muchos pasos dependerán del resultado de la llamada RPC asíncrona. En este momento, se debe prestar especial atención. Si se utiliza el RPC asíncrono basado en NIO (como Netty), el resultado de retorno se establece mediante el subproceso IO, es decir, el método de devolución de llamada se establece mediante activadores de subprocesos IO, devolución de llamada síncrona CompletableFuture (como luego Aplicar, luego Aceptar y otros métodos sin el sufijo Async), si el resultado devuelto de la llamada RPC asíncrona dependiente, entonces estas devoluciones de llamada síncronas se ejecuta en el subproceso IO, y todo el servicio tiene solo un grupo de subprocesos IO, lo que significa que es necesario asegurarse de que no haya una lógica que consuma mucho tiempo, como el bloqueo en la devolución de llamada síncrona, de lo contrario, el subproceso IO estará ocupado hasta la ejecución de estas lógicas se completa, afectando la respuesta de todo el servicio.

4.3 Otros

4.3.1 Manejo de excepciones

Dado que las tareas ejecutadas de forma asincrónica se ejecutan en otros subprocesos y la información de excepción se almacena en la pila de subprocesos, el subproceso actual no puede detectar excepciones a través de try\catch a menos que esté bloqueado y espere a que se devuelva el resultado. CompletableFuture proporciona excepcionalmente una devolución de llamada de captura de excepción, lo que es equivalente a try\catch en una llamada síncrona. El método de uso es el siguiente:

@Autowired
private WmOrderAdditionInfoThriftService wmOrderAdditionInfoThriftService;//内部接口
public CompletableFuture<Integer> getCancelTypeAsync(long orderId) {
    
    
    CompletableFuture<WmOrderOpRemarkResult> remarkResultFuture = wmOrderAdditionInfoThriftService.findOrderCancelledRemarkByOrderIdAsync(orderId);//业务方法,内部会发起异步rpc调用
    return remarkResultFuture
      .exceptionally(err -> {
    
    //通过exceptionally 捕获异常,打印日志并返回默认值
         log.error("WmOrderRemarkService.getCancelTypeAsync Exception orderId={}", orderId, err);
         return 0;
      });
}

Una cosa a tener en cuenta es que CompletableFuture envuelve la excepción en el método de devolución de llamada. La mayoría de las excepciones se encapsulan en CompletionException y se lanzan, y la excepción real se almacena en el atributo de causa. Por lo tanto, si la cadena de llamadas ha sido procesada por un método de devolución de llamada, debe usar el método Throwable.getCause() para extraer la excepción real. . Sin embargo, en algunos casos, la excepción real se devolverá directamente ( Discusión de desbordamiento de pila ), es mejor usar la clase de herramienta para extraer la excepción, como se muestra en el siguiente código:

@Autowired
private WmOrderAdditionInfoThriftService wmOrderAdditionInfoThriftService;//内部接口
public CompletableFuture<Integer> getCancelTypeAsync(long orderId) {
    
    
    CompletableFuture<WmOrderOpRemarkResult> remarkResultFuture = wmOrderAdditionInfoThriftService.findOrderCancelledRemarkByOrderIdAsync(orderId);//业务方法,内部会发起异步rpc调用
    return remarkResultFuture
          .thenApply(result -> {
    
    //这里增加了一个回调方法thenApply,如果发生异常thenApply内部会通过new CompletionException(throwable) 对异常进行包装
      //这里是一些业务操作
        })
      .exceptionally(err -> {
    
    //通过exceptionally 捕获异常,这里的err已经被thenApply包装过,因此需要通过Throwable.getCause()提取异常
         log.error("WmOrderRemarkService.getCancelTypeAsync Exception orderId={}", orderId, ExceptionUtils.extractRealException(err));
         return 0;
      });
}

Una clase de herramienta personalizada ExceptionUtils se usa en el código anterior para extraer las excepciones de CompletableFuture.Cuando se usa CompletableFuture para la programación asíncrona, puede usar directamente esta clase de herramienta para manejar las excepciones. El código de implementación es el siguiente:

public class ExceptionUtils {
    
    
    public static Throwable extractRealException(Throwable throwable) {
    
    
          //这里判断异常类型是否为CompletionException、ExecutionException,如果是则进行提取,否则直接返回。
        if (throwable instanceof CompletionException || throwable instanceof ExecutionException) {
    
    
            if (throwable.getCause() != null) {
    
    
                return throwable.getCause();
            }
        }
        return throwable;
    }
}

4.3.2 Introducción de herramientas y métodos de precipitación

En el proceso de práctica, hemos acumulado algunas herramientas y métodos comunes, que se pueden usar directamente al desarrollar con CompletableFuture. Para obtener más detalles, consulte el "Apéndice".

5 beneficios asincrónicos

A través de la transformación asíncrona, el rendimiento del sistema API ha mejorado significativamente y los beneficios en comparación con los anteriores a la transformación son los siguientes:

  • El rendimiento de la interfaz central ha mejorado mucho. Antes de la transformación de la interfaz de sondeo de pedidos, el TP99 era de 754 ms, pero después de la transformación, se redujo a 408 ms.
  • El número de servidores se reduce en 1/3.

6 Referencias

  1. CompletableFuture (Plataforma Java SE 8)
  2. java - ¿CompletionStage siempre envuelve excepciones en CompletionException? - Desbordamiento de pila
  3. excepción: comportamiento sorprendente del método Java 8 CompletableFuture excepcionalmente
  4. Documentación | Apache Dubbo

7 Glosario y comentarios

Nota 1: "Sincronización incremental" se refiere al protocolo de sincronización de datos incrementales de pedidos entre el cliente comercio y el servidor.El cliente utiliza este protocolo para obtener nuevos pedidos y pedidos cuyo estado ha cambiado.

Nota 2: La versión de Java de la que dependen todos los puntos técnicos involucrados en este artículo es JDK 8, y el análisis de funciones admitido por CompletableFuture también se basa en esta versión.

apéndice

función personalizada

@FunctionalInterface
public interface ThriftAsyncCall {
    
    
    void invoke() throws TException ;
}

Clase de herramienta de procesamiento CompletableFuture

/**
 * CompletableFuture封装工具类
 */
@Slf4j
public class FutureUtils {
    
    
/**
 * 该方法为rpc注册监听的封装,可以作为其他实现的参照
 * OctoThriftCallback 为thrift回调方法
 * ThriftAsyncCall 为自定义函数,用来表示一次thrift调用(定义如上)
 */
public static <T> CompletableFuture<T> toCompletableFuture(final OctoThriftCallback<?,T> callback , ThriftAsyncCall thriftCall) {
    
    
    CompletableFuture<T> thriftResultFuture = new CompletableFuture<>();
    callback.addObserver(new OctoObserver<T>() {
    
    
        @Override
        public void onSuccess(T t) {
    
    
            thriftResultFuture.complete(t);
        }
        @Override
        public void onFailure(Throwable throwable) {
    
    
            thriftResultFuture.completeExceptionally(throwable);
        }
    });
    if (thriftCall != null) {
    
    
        try {
    
    
            thriftCall.invoke();
        } catch (TException e) {
    
    
            thriftResultFuture.completeExceptionally(e);
        }
    }
    return thriftResultFuture;
}
  /**
   * 设置CF状态为失败
   */
  public static <T> CompletableFuture<T> failed(Throwable ex) {
    
    
   CompletableFuture<T> completableFuture = new CompletableFuture<>();
   completableFuture.completeExceptionally(ex);
   return completableFuture;
  }
  /**
   * 设置CF状态为成功
   */
  public static <T> CompletableFuture<T> success(T result) {
    
    
   CompletableFuture<T> completableFuture = new CompletableFuture<>();
   completableFuture.complete(result);
   return completableFuture;
  }
  /**
   * 将List<CompletableFuture<T>> 转为 CompletableFuture<List<T>>
   */
  public static <T> CompletableFuture<List<T>> sequence(Collection<CompletableFuture<T>> completableFutures) {
    
    
   return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream()
                   .map(CompletableFuture::join)
                   .collect(Collectors.toList())
           );
  }
  /**
   * 将List<CompletableFuture<List<T>>> 转为 CompletableFuture<List<T>>
   * 多用于分页查询的场景
   */
  public static <T> CompletableFuture<List<T>> sequenceList(Collection<CompletableFuture<List<T>>> completableFutures) {
    
    
   return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream()
                   .flatMap( listFuture -> listFuture.join().stream())
                   .collect(Collectors.toList())
           );
  }
  /*
   * 将List<CompletableFuture<Map<K, V>>> 转为 CompletableFuture<Map<K, V>>
   * @Param mergeFunction 自定义key冲突时的merge策略
   */
  public static <K, V> CompletableFuture<Map<K, V>> sequenceMap(
       Collection<CompletableFuture<Map<K, V>>> completableFutures, BinaryOperator<V> mergeFunction) {
    
    
   return CompletableFuture
           .allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream().map(CompletableFuture::join)
                   .flatMap(map -> map.entrySet().stream())
                   .collect(Collectors.toMap(Entry::getKey, Entry::getValue, mergeFunction)));
  }
  /**
   * 将List<CompletableFuture<T>> 转为 CompletableFuture<List<T>>,并过滤调null值
   */
  public static <T> CompletableFuture<List<T>> sequenceNonNull(Collection<CompletableFuture<T>> completableFutures) {
    
    
   return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream()
                   .map(CompletableFuture::join)
                   .filter(e -> e != null)
                   .collect(Collectors.toList())
           );
  }
  /**
   * 将List<CompletableFuture<List<T>>> 转为 CompletableFuture<List<T>>,并过滤调null值
   * 多用于分页查询的场景
   */
  public static <T> CompletableFuture<List<T>> sequenceListNonNull(Collection<CompletableFuture<List<T>>> completableFutures) {
    
    
   return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream()
                   .flatMap( listFuture -> listFuture.join().stream().filter(e -> e != null))
                   .collect(Collectors.toList())
           );
  }
  /**
   * 将List<CompletableFuture<Map<K, V>>> 转为 CompletableFuture<Map<K, V>>
   * @Param filterFunction 自定义过滤策略
   */
  public static <T> CompletableFuture<List<T>> sequence(Collection<CompletableFuture<T>> completableFutures,
                                                     Predicate<? super T> filterFunction) {
    
    
   return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream()
                   .map(CompletableFuture::join)
                   .filter(filterFunction)
                   .collect(Collectors.toList())
           );
  }
  /**
   * 将List<CompletableFuture<List<T>>> 转为 CompletableFuture<List<T>>
   * @Param filterFunction 自定义过滤策略
   */
  public static <T> CompletableFuture<List<T>> sequenceList(Collection<CompletableFuture<List<T>>> completableFutures,
                                                         Predicate<? super T> filterFunction) {
    
    
   return CompletableFuture.allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream()
                   .flatMap( listFuture -> listFuture.join().stream().filter(filterFunction))
                   .collect(Collectors.toList())
           );
  }
/**
 * 将CompletableFuture<Map<K,V>>的list转为 CompletableFuture<Map<K,V>>。 多个map合并为一个map。 如果key冲突,采用新的value覆盖。
 */
  public static <K, V> CompletableFuture<Map<K, V>> sequenceMap(
       Collection<CompletableFuture<Map<K, V>>> completableFutures) {
    
    
   return CompletableFuture
           .allOf(completableFutures.toArray(new CompletableFuture<?>[0]))
           .thenApply(v -> completableFutures.stream().map(CompletableFuture::join)
                   .flatMap(map -> map.entrySet().stream())
                   .collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a, b) -> b)));
  }}

Clase de herramienta de extracción de excepciones

  public class ExceptionUtils {
    
    
   /**
    * 提取真正的异常
    */
   public static Throwable extractRealException(Throwable throwable) {
    
    
       if (throwable instanceof CompletionException || throwable instanceof ExecutionException) {
    
    
           if (throwable.getCause() != null) {
    
    
               return throwable.getCause();
           }
       }
       return throwable;
   }
  }

registro de impresión

  @Slf4j
  public abstract class AbstractLogAction<R> {
    
    
  protected final String methodName;
  protected final Object[] args;
public AbstractLogAction(String methodName, Object... args) {
    
    
    this.methodName = methodName;
    this.args = args;
}
protected void logResult(R result, Throwable throwable) {
    
    
    if (throwable != null) {
    
    
        boolean isBusinessError = throwable instanceof TBase || (throwable.getCause() != null && throwable
                .getCause() instanceof TBase);
        if (isBusinessError) {
    
    
            logBusinessError(throwable);
        } else if (throwable instanceof DegradeException || throwable instanceof DegradeRuntimeException) {
    
    //这里为内部rpc框架抛出的异常,使用时可以酌情修改
            if (RhinoSwitch.getBoolean("isPrintDegradeLog", false)) {
    
    
                log.error("{} degrade exception, param:{} , error:{}", methodName, args, throwable);
            }
        } else {
    
    
            log.error("{} unknown error, param:{} , error:{}", methodName, args, ExceptionUtils.extractRealException(throwable));
        }
    } else {
    
    
        if (isLogResult()) {
    
    
            log.info("{} param:{} , result:{}", methodName, args, result);
        } else {
    
    
            log.info("{} param:{}", methodName, args);
        }
    }
}
private void logBusinessError(Throwable throwable) {
    
    
    log.error("{} business error, param:{} , error:{}", methodName, args, throwable.toString(), ExceptionUtils.extractRealException(throwable));
}
private boolean isLogResult() {
    
    
      //这里是动态配置开关,用于动态控制日志打印,开源动态配置中心可以使用nacos、apollo等,如果项目没有使用配置中心则可以删除
    return RhinoSwitch.getBoolean(methodName + "_isLogResult", false);
}}

Clase de implementación de procesamiento de registro

/**
 * 发生异常时,根据是否为业务异常打印日志。
 * 跟CompletableFuture.whenComplete配合使用,不改变completableFuture的结果(正常OR异常)
 */
@Slf4j
public class LogErrorAction<R> extends AbstractLogAction<R> implements BiConsumer<R, Throwable> {
    
    
public LogErrorAction(String methodName, Object... args) {
    
    
    super(methodName, args);
}
@Override
public void accept(R result, Throwable throwable) {
    
    
    logResult(result, throwable);
}
}

modo de registro de impresión

completableFuture
.whenComplete(
  new LogErrorAction<>("orderService.getOrder", params));

La excepción devuelve el valor predeterminado

/**
 * 当发生异常时返回自定义的值
 */
public class DefaultValueHandle<R> extends AbstractLogAction<R> implements BiFunction<R, Throwable, R> {
    
    
    private final R defaultValue;
/**
 * 当返回值为空的时候是否替换为默认值
 */
private final boolean isNullToDefault;
/**
 * @param methodName      方法名称
 * @param defaultValue 当异常发生时自定义返回的默认值
 * @param args            方法入参
 */
  public DefaultValueHandle(String methodName, R defaultValue, Object... args) {
    
    
   super(methodName, args);
   this.defaultValue = defaultValue;
   this.isNullToDefault = false;
  }
/**
 * @param isNullToDefault
 * @param defaultValue 当异常发生时自定义返回的默认值
 * @param methodName      方法名称
 * @param args            方法入参
 */
  public DefaultValueHandle(boolean isNullToDefault, R defaultValue, String methodName, Object... args) {
    
    
   super(methodName, args);
   this.defaultValue = defaultValue;
   this.isNullToDefault = isNullToDefault;
  }
@Override
public R apply(R result, Throwable throwable) {
    
    
    logResult(result, throwable);
    if (throwable != null) {
    
    
        return defaultValue;
    }
    if (result == null && isNullToDefault) {
    
    
        return defaultValue;
    }
    return result;
}
public static <R> DefaultValueHandle.DefaultValueHandleBuilder<R> builder() {
    
    
    return new DefaultValueHandle.DefaultValueHandleBuilder<>();
}
public static class DefaultValueHandleBuilder<R> {
    
    
    private boolean isNullToDefault;
    private R defaultValue;
    private String methodName;
    private Object[] args;
    DefaultValueHandleBuilder() {
    
    
    }
    public DefaultValueHandle.DefaultValueHandleBuilder<R> isNullToDefault(final boolean isNullToDefault) {
    
    
        this.isNullToDefault = isNullToDefault;
        return this;
    }
    public DefaultValueHandle.DefaultValueHandleBuilder<R> defaultValue(final R defaultValue) {
    
    
        this.defaultValue = defaultValue;
        return this;
    }
    public DefaultValueHandle.DefaultValueHandleBuilder<R> methodName(final String methodName) {
    
    
        this.methodName = methodName;
        return this;
    }
    public DefaultValueHandle.DefaultValueHandleBuilder<R> args(final Object... args) {
    
    
        this.args = args;
        return this;
    }
    public DefaultValueHandle<R> build() {
    
    
        return new DefaultValueHandle<R>(this.isNullToDefault, this.defaultValue, this.methodName, this.args);
    }
    public String toString() {
    
    
        return "DefaultValueHandle.DefaultValueHandleBuilder(isNullToDefault=" + this.isNullToDefault + ", defaultValue=" + this.defaultValue + ", methodName=" + this.methodName + ", args=" + Arrays.deepToString(this.args) + ")";
    }
}

Ejemplo de aplicación de valor de retorno predeterminado

completableFuture.handle(new DefaultValueHandle<>("orderService.getOrder", Collections.emptyMap(), params));

Supongo que te gusta

Origin blog.csdn.net/weixin_42469135/article/details/132090654
Recomendado
Clasificación